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. 65 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 66 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 67 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 68 * @param {Number} zoomX Zoom factor in x-axis direction 69 * @param {Number} zoomY Zoom factor in y-axis direction 70 * @param {Number} unitX Units in x-axis direction 71 * @param {Number} unitY Units in y-axis direction 72 * @param {Number} canvasWidth The width of canvas 73 * @param {Number} canvasHeight The height of canvas 74 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard} 75 * @borrows JXG.EventEmitter#on as this.on 76 * @borrows JXG.EventEmitter#off as this.off 77 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 78 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 79 */ 80 JXG.Board = function (container, renderer, id, 81 origin, zoomX, zoomY, unitX, unitY, 82 canvasWidth, canvasHeight, attributes) { 83 /** 84 * Board is in no special mode, objects are highlighted on mouse over and objects may be 85 * clicked to start drag&drop. 86 * @type Number 87 * @constant 88 */ 89 this.BOARD_MODE_NONE = 0x0000; 90 91 /** 92 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 93 * {@link JXG.Board#mouse} is updated on mouse movement. 94 * @type Number 95 * @constant 96 */ 97 this.BOARD_MODE_DRAG = 0x0001; 98 99 /** 100 * In this mode a mouse move changes the origin's screen coordinates. 101 * @type Number 102 * @constant 103 */ 104 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 105 106 /** 107 * Update is made with high quality, e.g. graphs are evaluated at much more points. 108 * @type Number 109 * @constant 110 * @see JXG.Board#updateQuality 111 */ 112 this.BOARD_MODE_ZOOM = 0x0011; 113 114 /** 115 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 116 * @type Number 117 * @constant 118 * @see JXG.Board#updateQuality 119 */ 120 this.BOARD_QUALITY_LOW = 0x1; 121 122 /** 123 * Update is made with high quality, e.g. graphs are evaluated at much more points. 124 * @type Number 125 * @constant 126 * @see JXG.Board#updateQuality 127 */ 128 this.BOARD_QUALITY_HIGH = 0x2; 129 130 /** 131 * Pointer to the document element containing the board. 132 * @type Object 133 */ 134 if (Type.exists(attributes.document) && attributes.document !== false) { 135 this.document = attributes.document; 136 } else if (Env.isBrowser) { 137 this.document = document; 138 } 139 140 /** 141 * The html-id of the html element containing the board. 142 * @type String 143 */ 144 this.container = ''; // container 145 146 /** 147 * Pointer to the html element containing the board. 148 * @type Object 149 */ 150 this.containerObj = null; // (Env.isBrowser ? this.document.getElementById(this.container) : null); 151 152 // Set this.container and this.containerObj 153 if (Type.isString(container)) { 154 // Hosting div is given as string 155 this.container = container; // container 156 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 157 } else if (Env.isBrowser) { 158 // Hosting div is given as object pointer 159 this.containerObj = container; 160 this.container = this.containerObj.getAttribute('id'); 161 if (this.container === null) { 162 // Set random id to this.container, 163 // but not to the DOM element 164 this.container = 'null' + parseInt(Math.random() * 100000000).toString(); 165 } 166 } 167 168 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 169 throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.'); 170 } 171 172 /** 173 * A reference to this boards renderer. 174 * @type JXG.AbstractRenderer 175 * @name JXG.Board#renderer 176 * @private 177 * @ignore 178 */ 179 this.renderer = renderer; 180 181 /** 182 * Grids keeps track of all grids attached to this board. 183 * @type Array 184 * @private 185 */ 186 this.grids = []; 187 188 /** 189 * Some standard options 190 * @type JXG.Options 191 */ 192 this.options = Type.deepCopy(Options); 193 194 /** 195 * Board attributes 196 * @type Object 197 */ 198 this.attr = attributes; 199 200 if (this.attr.theme !== 'default' && Type.exists(JXG.themes[this.attr.theme])) { 201 Type.mergeAttr(this.options, JXG.themes[this.attr.theme], true); 202 } 203 204 /** 205 * Dimension of the board. 206 * @default 2 207 * @type Number 208 */ 209 this.dimension = 2; 210 211 this.jc = new JessieCode(); 212 this.jc.use(this); 213 214 /** 215 * Coordinates of the boards origin. This a object with the two properties 216 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 217 * stores the boards origin in homogeneous screen coordinates. 218 * @type Object 219 * @private 220 */ 221 this.origin = {}; 222 this.origin.usrCoords = [1, 0, 0]; 223 this.origin.scrCoords = [1, origin[0], origin[1]]; 224 225 /** 226 * Zoom factor in X direction. It only stores the zoom factor to be able 227 * to get back to 100% in zoom100(). 228 * @name JXG.Board.zoomX 229 * @type Number 230 * @private 231 * @ignore 232 */ 233 this.zoomX = zoomX; 234 235 /** 236 * Zoom factor in Y direction. It only stores the zoom factor to be able 237 * to get back to 100% in zoom100(). 238 * @name JXG.Board.zoomY 239 * @type Number 240 * @private 241 * @ignore 242 */ 243 this.zoomY = zoomY; 244 245 /** 246 * The number of pixels which represent one unit in user-coordinates in x direction. 247 * @type Number 248 * @private 249 */ 250 this.unitX = unitX * this.zoomX; 251 252 /** 253 * The number of pixels which represent one unit in user-coordinates in y direction. 254 * @type Number 255 * @private 256 */ 257 this.unitY = unitY * this.zoomY; 258 259 /** 260 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 261 * width/height ratio of the canvas. 262 * @type Boolean 263 * @private 264 */ 265 this.keepaspectratio = false; 266 267 /** 268 * Canvas width. 269 * @type Number 270 * @private 271 */ 272 this.canvasWidth = canvasWidth; 273 274 /** 275 * Canvas Height 276 * @type Number 277 * @private 278 */ 279 this.canvasHeight = canvasHeight; 280 281 // If the given id is not valid, generate an unique id 282 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 283 this.id = id; 284 } else { 285 this.id = this.generateId(); 286 } 287 288 EventEmitter.eventify(this); 289 290 this.hooks = []; 291 292 /** 293 * An array containing all other boards that are updated after this board has been updated. 294 * @type Array 295 * @see JXG.Board#addChild 296 * @see JXG.Board#removeChild 297 */ 298 this.dependentBoards = []; 299 300 /** 301 * During the update process this is set to false to prevent an endless loop. 302 * @default false 303 * @type Boolean 304 */ 305 this.inUpdate = false; 306 307 /** 308 * 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. 309 * @type Object 310 */ 311 this.objects = {}; 312 313 /** 314 * An array containing all geometric objects on the board in the order of construction. 315 * @type Array 316 */ 317 this.objectsList = []; 318 319 /** 320 * 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. 321 * @type Object 322 */ 323 this.groups = {}; 324 325 /** 326 * Stores all the objects that are currently running an animation. 327 * @type Object 328 */ 329 this.animationObjects = {}; 330 331 /** 332 * An associative array containing all highlighted elements belonging to the board. 333 * @type Object 334 */ 335 this.highlightedObjects = {}; 336 337 /** 338 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 339 * @type Number 340 */ 341 this.numObjects = 0; 342 343 /** 344 * 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. 345 * @type Object 346 */ 347 this.elementsByName = {}; 348 349 /** 350 * The board mode the board is currently in. Possible values are 351 * <ul> 352 * <li>JXG.Board.BOARD_MODE_NONE</li> 353 * <li>JXG.Board.BOARD_MODE_DRAG</li> 354 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 355 * </ul> 356 * @type Number 357 */ 358 this.mode = this.BOARD_MODE_NONE; 359 360 /** 361 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 362 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 363 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 364 * evaluation points when plotting functions. Possible values are 365 * <ul> 366 * <li>BOARD_QUALITY_LOW</li> 367 * <li>BOARD_QUALITY_HIGH</li> 368 * </ul> 369 * @type Number 370 * @see JXG.Board#mode 371 */ 372 this.updateQuality = this.BOARD_QUALITY_HIGH; 373 374 /** 375 * If true updates are skipped. 376 * @type Boolean 377 */ 378 this.isSuspendedRedraw = false; 379 380 this.calculateSnapSizes(); 381 382 /** 383 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 384 * @type Number 385 * @see JXG.Board#drag_dy 386 */ 387 this.drag_dx = 0; 388 389 /** 390 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 391 * @type Number 392 * @see JXG.Board#drag_dx 393 */ 394 this.drag_dy = 0; 395 396 /** 397 * The last position where a drag event has been fired. 398 * @type Array 399 * @see JXG.Board#moveObject 400 */ 401 this.drag_position = [0, 0]; 402 403 /** 404 * References to the object that is dragged with the mouse on the board. 405 * @type JXG.GeometryElement 406 * @see JXG.Board#touches 407 */ 408 this.mouse = {}; 409 410 /** 411 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 412 * @type Array 413 * @see JXG.Board#mouse 414 */ 415 this.touches = []; 416 417 /** 418 * A string containing the XML text of the construction. 419 * This is set in {@link JXG.FileReader.parseString}. 420 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 421 * @type String 422 */ 423 this.xmlString = ''; 424 425 /** 426 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 427 * @type Array 428 */ 429 this.cPos = []; 430 431 /** 432 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 433 * touchStart because Android's Webkit browser fires too much of them. 434 * @type Number 435 */ 436 this.touchMoveLast = 0; 437 438 /** 439 * Contains the pointerId of the last touchMove event which was not thrown away or since 440 * touchStart because Android's Webkit browser fires too much of them. 441 * @type Number 442 */ 443 this.touchMoveLastId = Infinity; 444 445 /** 446 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 447 * @type Number 448 */ 449 this.positionAccessLast = 0; 450 451 /** 452 * Collects all elements that triggered a mouse down event. 453 * @type Array 454 */ 455 this.downObjects = []; 456 this.clickObjects = {}; 457 458 /** 459 * Collects all elements that have keyboard focus. Should be either one or no element. 460 * Elements are stored with their id. 461 * @type Array 462 */ 463 this.focusObjects = []; 464 465 if (this.attr.showcopyright) { 466 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 467 } 468 469 /** 470 * Full updates are needed after zoom and axis translates. This saves some time during an update. 471 * @default false 472 * @type Boolean 473 */ 474 this.needsFullUpdate = false; 475 476 /** 477 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 478 * elements are updated during mouse move. On mouse up the whole construction is 479 * updated. This enables us to be fast even on very slow devices. 480 * @type Boolean 481 * @default false 482 */ 483 this.reducedUpdate = false; 484 485 /** 486 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 487 * at the moment, it's value is 'none'. 488 */ 489 this.currentCBDef = 'none'; 490 491 /** 492 * If GEONExT constructions are displayed, then this property should be set to true. 493 * At the moment there should be no difference. But this may change. 494 * This is set in {@link JXG.GeonextReader#readGeonext}. 495 * @type Boolean 496 * @default false 497 * @see JXG.GeonextReader#readGeonext 498 */ 499 this.geonextCompatibilityMode = false; 500 501 if (this.options.text.useASCIIMathML && translateASCIIMath) { 502 init(); 503 } else { 504 this.options.text.useASCIIMathML = false; 505 } 506 507 /** 508 * A flag which tells if the board registers mouse events. 509 * @type Boolean 510 * @default false 511 */ 512 this.hasMouseHandlers = false; 513 514 /** 515 * A flag which tells if the board registers touch events. 516 * @type Boolean 517 * @default false 518 */ 519 this.hasTouchHandlers = false; 520 521 /** 522 * A flag which stores if the board registered pointer events. 523 * @type Boolean 524 * @default false 525 */ 526 this.hasPointerHandlers = false; 527 528 /** 529 * A flag which stores if the board registered zoom events, i.e. mouse wheel scroll events. 530 * @type Boolean 531 * @default false 532 */ 533 this.hasWheelHandlers = false; 534 535 /** 536 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 537 * @type Boolean 538 * @default false 539 */ 540 this.hasMouseUp = false; 541 542 /** 543 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 544 * @type Boolean 545 * @default false 546 */ 547 this.hasTouchEnd = false; 548 549 /** 550 * A flag which tells us if the board has a pointerUp event registered at the moment. 551 * @type Boolean 552 * @default false 553 */ 554 this.hasPointerUp = false; 555 556 /** 557 * Array containing the events related to resizing that have event listeners. 558 * @type Array 559 * @default [] 560 */ 561 this.resizeHandlers = []; 562 563 /** 564 * Offset for large coords elements like images 565 * @type Array 566 * @private 567 * @default [0, 0] 568 */ 569 this._drag_offset = [0, 0]; 570 571 /** 572 * Stores the input device used in the last down or move event. 573 * @type String 574 * @private 575 * @default 'mouse' 576 */ 577 this._inputDevice = 'mouse'; 578 579 /** 580 * Keeps a list of pointer devices which are currently touching the screen. 581 * @type Array 582 * @private 583 */ 584 this._board_touches = []; 585 586 /** 587 * A flag which tells us if the board is in the selecting mode 588 * @type Boolean 589 * @default false 590 */ 591 this.selectingMode = false; 592 593 /** 594 * A flag which tells us if the user is selecting 595 * @type Boolean 596 * @default false 597 */ 598 this.isSelecting = false; 599 600 /** 601 * A flag which tells us if the user is scrolling the viewport 602 * @type Boolean 603 * @private 604 * @default false 605 * @see JXG.Board#scrollListener 606 */ 607 this._isScrolling = false; 608 609 /** 610 * A flag which tells us if a resize is in process 611 * @type Boolean 612 * @private 613 * @default false 614 * @see JXG.Board#resizeListener 615 */ 616 this._isResizing = false; 617 618 /** 619 * A bounding box for the selection 620 * @type Array 621 * @default [ [0,0], [0,0] ] 622 */ 623 this.selectingBox = [[0, 0], [0, 0]]; 624 625 /** 626 * Array to log user activity. 627 * Entries are objects of the form '{type, id, start, end}' notifying 628 * the start time as well as the last time of a single event of type 'type' 629 * on a JSXGraph element of id 'id'. 630 * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC 631 * and the time the event happened. 632 * <p> 633 * For the time being (i.e. v1.5.0) the only supported type is 'drag'. 634 * @type Array 635 */ 636 this.userLog = []; 637 638 this.mathLib = Math; // Math or JXG.Math.IntervalArithmetic 639 this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic 640 641 if (this.attr.registerevents === true) { 642 this.attr.registerevents = { 643 fullscreen: true, 644 keyboard: true, 645 pointer: true, 646 resize: true, 647 wheel: true 648 }; 649 } else if (typeof this.attr.registerevents === 'object') { 650 if (!Type.exists(this.attr.registerevents.fullscreen)) { 651 this.attr.registerevents.fullscreen = true; 652 } 653 if (!Type.exists(this.attr.registerevents.keyboard)) { 654 this.attr.registerevents.keyboard = true; 655 } 656 if (!Type.exists(this.attr.registerevents.pointer)) { 657 this.attr.registerevents.pointer = true; 658 } 659 if (!Type.exists(this.attr.registerevents.resize)) { 660 this.attr.registerevents.resize = true; 661 } 662 if (!Type.exists(this.attr.registerevents.wheel)) { 663 this.attr.registerevents.wheel = true; 664 } 665 } 666 if (this.attr.registerevents !== false) { 667 if (this.attr.registerevents.fullscreen) { 668 this.addFullscreenEventHandlers(); 669 } 670 if (this.attr.registerevents.keyboard) { 671 this.addKeyboardEventHandlers(); 672 } 673 if (this.attr.registerevents.pointer) { 674 this.addEventHandlers(); 675 } 676 if (this.attr.registerevents.resize) { 677 this.addResizeEventHandlers(); 678 } 679 if (this.attr.registerevents.wheel) { 680 this.addWheelEventHandlers(); 681 } 682 } 683 684 this.methodMap = { 685 update: 'update', 686 fullUpdate: 'fullUpdate', 687 on: 'on', 688 off: 'off', 689 trigger: 'trigger', 690 setAttribute: 'setAttribute', 691 setBoundingBox: 'setBoundingBox', 692 setView: 'setBoundingBox', 693 getBoundingBox: 'getBoundingBox', 694 BoundingBox: 'getBoundingBox', 695 getView: 'getBoundingBox', 696 View: 'getBoundingBox', 697 migratePoint: 'migratePoint', 698 colorblind: 'emulateColorblindness', 699 suspendUpdate: 'suspendUpdate', 700 unsuspendUpdate: 'unsuspendUpdate', 701 clearTraces: 'clearTraces', 702 left: 'clickLeftArrow', 703 right: 'clickRightArrow', 704 up: 'clickUpArrow', 705 down: 'clickDownArrow', 706 zoomIn: 'zoomIn', 707 zoomOut: 'zoomOut', 708 zoom100: 'zoom100', 709 zoomElements: 'zoomElements', 710 remove: 'removeObject', 711 removeObject: 'removeObject' 712 }; 713 }; 714 715 JXG.extend( 716 JXG.Board.prototype, 717 /** @lends JXG.Board.prototype */ { 718 /** 719 * Generates an unique name for the given object. The result depends on the objects type, if the 720 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 721 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 722 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 723 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 724 * chars prefixed with s_ is used. 725 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 726 * @returns {String} Unique name for the object. 727 */ 728 generateName: function (object) { 729 var possibleNames, i, 730 maxNameLength = this.attr.maxnamelength, 731 pre = '', 732 post = '', 733 indices = [], 734 name = ''; 735 736 if (object.type === Const.OBJECT_TYPE_TICKS) { 737 return ''; 738 } 739 740 if (Type.isPoint(object) || Type.isPoint3D(object)) { 741 // points have capital letters 742 possibleNames = [ 743 '', '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' 744 ]; 745 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 746 possibleNames = [ 747 '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 748 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω' 749 ]; 750 } else { 751 // all other elements get lowercase labels 752 possibleNames = [ 753 '', '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' 754 ]; 755 } 756 757 if ( 758 !Type.isPoint(object) && 759 !Type.isPoint3D(object) && 760 object.elementClass !== Const.OBJECT_CLASS_LINE && 761 object.type !== Const.OBJECT_TYPE_ANGLE 762 ) { 763 if (object.type === Const.OBJECT_TYPE_POLYGON) { 764 pre = 'P_{'; 765 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 766 pre = 'k_{'; 767 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 768 pre = 't_{'; 769 } else { 770 pre = 's_{'; 771 } 772 post = '}'; 773 } 774 775 for (i = 0; i < maxNameLength; i++) { 776 indices[i] = 0; 777 } 778 779 while (indices[maxNameLength - 1] < possibleNames.length) { 780 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 781 name = pre; 782 783 for (i = maxNameLength; i > 0; i--) { 784 name += possibleNames[indices[i - 1]]; 785 } 786 787 if (!Type.exists(this.elementsByName[name + post])) { 788 return name + post; 789 } 790 } 791 indices[0] = possibleNames.length; 792 793 for (i = 1; i < maxNameLength; i++) { 794 if (indices[i - 1] === possibleNames.length) { 795 indices[i - 1] = 1; 796 indices[i] += 1; 797 } 798 } 799 } 800 801 return ''; 802 }, 803 804 /** 805 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 806 * @returns {String} Unique id for a board. 807 */ 808 generateId: function () { 809 var r = 1; 810 811 // as long as we don't have a unique id generate a new one 812 while (Type.exists(JXG.boards['jxgBoard' + r])) { 813 r = Math.round(Math.random() * 65535); 814 } 815 816 return 'jxgBoard' + r; 817 }, 818 819 /** 820 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 821 * object type. As a side effect {@link JXG.Board#numObjects} 822 * is updated. 823 * @param {Object} obj Reference of an geometry object that needs an id. 824 * @param {Number} type Type of the object. 825 * @returns {String} Unique id for an element. 826 */ 827 setId: function (obj, type) { 828 var randomNumber, 829 num = this.numObjects, 830 elId = obj.id; 831 832 this.numObjects += 1; 833 834 // If no id is provided or id is empty string, a new one is chosen 835 if (elId === '' || !Type.exists(elId)) { 836 elId = this.id + type + num; 837 while (Type.exists(this.objects[elId])) { 838 randomNumber = Math.round(Math.random() * 65535); 839 elId = this.id + type + num + '-' + randomNumber; 840 } 841 } 842 843 obj.id = elId; 844 this.objects[elId] = obj; 845 obj._pos = this.objectsList.length; 846 this.objectsList[this.objectsList.length] = obj; 847 848 return elId; 849 }, 850 851 /** 852 * After construction of the object the visibility is set 853 * and the label is constructed if necessary. 854 * @param {Object} obj The object to add. 855 */ 856 finalizeAdding: function (obj) { 857 if (Type.evaluate(obj.visProp.visible) === false) { 858 this.renderer.display(obj, false); 859 } 860 }, 861 862 finalizeLabel: function (obj) { 863 if ( 864 obj.hasLabel && 865 !Type.evaluate(obj.label.visProp.islabel) && 866 Type.evaluate(obj.label.visProp.visible) === false 867 ) { 868 this.renderer.display(obj.label, false); 869 } 870 }, 871 872 /********************************************************** 873 * 874 * Event Handler helpers 875 * 876 **********************************************************/ 877 878 /** 879 * Returns false if the event has been triggered faster than the maximum frame rate. 880 * 881 * @param {Event} evt Event object given by the browser (unused) 882 * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned. 883 * @private 884 * @see JXG.Board#pointerMoveListener 885 * @see JXG.Board#touchMoveListener 886 * @see JXG.Board#mouseMoveListener 887 */ 888 checkFrameRate: function (evt) { 889 var handleEvt = false, 890 time = new Date().getTime(); 891 892 if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) { 893 handleEvt = true; 894 this.touchMoveLastId = evt.pointerId; 895 } 896 if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) { 897 handleEvt = true; 898 } 899 if (handleEvt) { 900 this.touchMoveLast = time; 901 } 902 return handleEvt; 903 }, 904 905 /** 906 * Calculates mouse coordinates relative to the boards container. 907 * @returns {Array} Array of coordinates relative the boards container top left corner. 908 */ 909 getCoordsTopLeftCorner: function () { 910 var cPos, 911 doc, 912 crect, 913 // In ownerDoc we need the 'real' document object. 914 // The first version is used in the case of shadowDom, 915 // the second case in the 'normal' case. 916 ownerDoc = this.document.ownerDocument || this.document, 917 docElement = ownerDoc.documentElement || this.document.body.parentNode, 918 docBody = ownerDoc.body, 919 container = this.containerObj, 920 // viewport, content, 921 zoom, 922 o; 923 924 /** 925 * During drags and origin moves the container element is usually not changed. 926 * Check the position of the upper left corner at most every 1000 msecs 927 */ 928 if ( 929 this.cPos.length > 0 && 930 (this.mode === this.BOARD_MODE_DRAG || 931 this.mode === this.BOARD_MODE_MOVE_ORIGIN || 932 new Date().getTime() - this.positionAccessLast < 1000) 933 ) { 934 return this.cPos; 935 } 936 this.positionAccessLast = new Date().getTime(); 937 938 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 939 // even CSS3D transformations etc. 940 // Supported by all browsers but IE 6, 7. 941 if (container.getBoundingClientRect) { 942 crect = container.getBoundingClientRect(); 943 944 zoom = 1.0; 945 // Recursively search for zoom style entries. 946 // This is necessary for reveal.js on webkit. 947 // It fails if the user does zooming 948 o = container; 949 while (o && Type.exists(o.parentNode)) { 950 if ( 951 Type.exists(o.style) && 952 Type.exists(o.style.zoom) && 953 o.style.zoom !== '' 954 ) { 955 zoom *= parseFloat(o.style.zoom); 956 } 957 o = o.parentNode; 958 } 959 cPos = [crect.left * zoom, crect.top * zoom]; 960 961 // add border width 962 cPos[0] += Env.getProp(container, 'border-left-width'); 963 cPos[1] += Env.getProp(container, 'border-top-width'); 964 965 // vml seems to ignore paddings 966 if (this.renderer.type !== 'vml') { 967 // add padding 968 cPos[0] += Env.getProp(container, 'padding-left'); 969 cPos[1] += Env.getProp(container, 'padding-top'); 970 } 971 972 this.cPos = cPos.slice(); 973 return this.cPos; 974 } 975 976 // 977 // OLD CODE 978 // IE 6-7 only: 979 // 980 cPos = Env.getOffset(container); 981 doc = this.document.documentElement.ownerDocument; 982 983 if (!this.containerObj.currentStyle && doc.defaultView) { 984 // Non IE 985 // this is for hacks like this one used in wordpress for the admin bar: 986 // html { margin-top: 28px } 987 // seems like it doesn't work in IE 988 989 cPos[0] += Env.getProp(docElement, 'margin-left'); 990 cPos[1] += Env.getProp(docElement, 'margin-top'); 991 992 cPos[0] += Env.getProp(docElement, 'border-left-width'); 993 cPos[1] += Env.getProp(docElement, 'border-top-width'); 994 995 cPos[0] += Env.getProp(docElement, 'padding-left'); 996 cPos[1] += Env.getProp(docElement, 'padding-top'); 997 } 998 999 if (docBody) { 1000 cPos[0] += Env.getProp(docBody, 'left'); 1001 cPos[1] += Env.getProp(docBody, 'top'); 1002 } 1003 1004 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 1005 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 1006 // available version so we're doing it the hacky way: Add a fixed offset. 1007 // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J 1008 if (typeof google === 'object' && google.translate) { 1009 cPos[0] += 10; 1010 cPos[1] += 25; 1011 } 1012 1013 // add border width 1014 cPos[0] += Env.getProp(container, 'border-left-width'); 1015 cPos[1] += Env.getProp(container, 'border-top-width'); 1016 1017 // vml seems to ignore paddings 1018 if (this.renderer.type !== 'vml') { 1019 // add padding 1020 cPos[0] += Env.getProp(container, 'padding-left'); 1021 cPos[1] += Env.getProp(container, 'padding-top'); 1022 } 1023 1024 cPos[0] += this.attr.offsetx; 1025 cPos[1] += this.attr.offsety; 1026 1027 this.cPos = cPos.slice(); 1028 return this.cPos; 1029 }, 1030 1031 /** 1032 * This function divides the board into 9 sections and returns an array <tt>[u,v]</tt> which symbolizes the location of <tt>position</tt>. 1033 * Optional a <tt>margin</tt> to the inner of the board is respected.<br> 1034 * 1035 * @name Board#getPointLoc 1036 * @param {Array} position Array of requested position <tt>[x, y]</tt> or <tt>[w, x, y]</tt>. 1037 * @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>. 1038 * @returns {Array} [u,v] with the following meanings: 1039 * <pre> 1040 * v u > | -1 | 0 | 1 | 1041 * ------------------------------------------ 1042 * 1 | [-1,1] | [0,1] | [1,1] | 1043 * ------------------------------------------ 1044 * 0 | [-1,0] | Board | [1,0] | 1045 * ------------------------------------------ 1046 * -1 | [-1,-1] | [0,-1] | [1,-1] | 1047 * </pre> 1048 * Positions inside the board (minus margin) return the value <tt>[0,0]</tt>. 1049 * 1050 * @example 1051 * var point1, point2, point3, point4, margin, 1052 * p1Location, p2Location, p3Location, p4Location, 1053 * helppoint1, helppoint2, helppoint3, helppoint4; 1054 * 1055 * // margin to make the boundingBox virtually smaller 1056 * margin = [2,2,2,2]; 1057 * 1058 * // Points which are seen on screen 1059 * point1 = board.create('point', [0,0]); 1060 * point2 = board.create('point', [0,7]); 1061 * point3 = board.create('point', [7,7]); 1062 * point4 = board.create('point', [-7,-5]); 1063 * 1064 * p1Location = board.getPointLoc(point1.coords.usrCoords, margin); 1065 * p2Location = board.getPointLoc(point2.coords.usrCoords, margin); 1066 * p3Location = board.getPointLoc(point3.coords.usrCoords, margin); 1067 * p4Location = board.getPointLoc(point4.coords.usrCoords, margin); 1068 * 1069 * // Text seen on screen 1070 * board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"]) 1071 * board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"]) 1072 * board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"]) 1073 * board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"]) 1074 * 1075 * 1076 * // Helping points that are used to create the helping lines 1077 * helppoint1 = board.create('point', [(function (){ 1078 * var bbx = board.getBoundingBox(); 1079 * return [bbx[2] - 2, bbx[1] -2]; 1080 * })], { 1081 * visible: false, 1082 * }) 1083 * 1084 * helppoint2 = board.create('point', [(function (){ 1085 * var bbx = board.getBoundingBox(); 1086 * return [bbx[0] + 2, bbx[1] -2]; 1087 * })], { 1088 * visible: false, 1089 * }) 1090 * 1091 * helppoint3 = board.create('point', [(function (){ 1092 * var bbx = board.getBoundingBox(); 1093 * return [bbx[0]+ 2, bbx[3] + 2]; 1094 * })],{ 1095 * visible: false, 1096 * }) 1097 * 1098 * helppoint4 = board.create('point', [(function (){ 1099 * var bbx = board.getBoundingBox(); 1100 * return [bbx[2] -2, bbx[3] + 2]; 1101 * })], { 1102 * visible: false, 1103 * }) 1104 * 1105 * // Helping lines to visualize the 9 sectors and the margin 1106 * board.create('line', [helppoint1, helppoint2]); 1107 * board.create('line', [helppoint2, helppoint3]); 1108 * board.create('line', [helppoint3, helppoint4]); 1109 * board.create('line', [helppoint4, helppoint1]); 1110 * 1111 * </pre><div id="JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70" class="jxgbox" style="width: 500px; height: 500px;"></div> 1112 * <script type="text/javascript"> 1113 * (function() { 1114 * var board = JXG.JSXGraph.initBoard('JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70', 1115 * {boundingbox: [-8, 8, 8,-8], maxboundingbox: [-7.5,7.5,7.5,-7.5], axis: true, showcopyright: false, shownavigation: false, showZoom: false}); 1116 * var point1, point2, point3, point4, margin, 1117 * p1Location, p2Location, p3Location, p4Location, 1118 * helppoint1, helppoint2, helppoint3, helppoint4; 1119 * 1120 * // margin to make the boundingBox virtually smaller 1121 * margin = [2,2,2,2]; 1122 * 1123 * // Points which are seen on screen 1124 * point1 = board.create('point', [0,0]); 1125 * point2 = board.create('point', [0,7]); 1126 * point3 = board.create('point', [7,7]); 1127 * point4 = board.create('point', [-7,-5]); 1128 * 1129 * p1Location = board.getPointLoc(point1.coords.usrCoords, margin); 1130 * p2Location = board.getPointLoc(point2.coords.usrCoords, margin); 1131 * p3Location = board.getPointLoc(point3.coords.usrCoords, margin); 1132 * p4Location = board.getPointLoc(point4.coords.usrCoords, margin); 1133 * 1134 * // Text seen on screen 1135 * board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"]) 1136 * board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"]) 1137 * board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"]) 1138 * board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"]) 1139 * 1140 * 1141 * // Helping points that are used to create the helping lines 1142 * helppoint1 = board.create('point', [(function (){ 1143 * var bbx = board.getBoundingBox(); 1144 * return [bbx[2] - 2, bbx[1] -2]; 1145 * })], { 1146 * visible: false, 1147 * }) 1148 * 1149 * helppoint2 = board.create('point', [(function (){ 1150 * var bbx = board.getBoundingBox(); 1151 * return [bbx[0] + 2, bbx[1] -2]; 1152 * })], { 1153 * visible: false, 1154 * }) 1155 * 1156 * helppoint3 = board.create('point', [(function (){ 1157 * var bbx = board.getBoundingBox(); 1158 * return [bbx[0]+ 2, bbx[3] + 2]; 1159 * })],{ 1160 * visible: false, 1161 * }) 1162 * 1163 * helppoint4 = board.create('point', [(function (){ 1164 * var bbx = board.getBoundingBox(); 1165 * return [bbx[2] -2, bbx[3] + 2]; 1166 * })], { 1167 * visible: false, 1168 * }) 1169 * 1170 * // Helping lines to visualize the 9 sectors and the margin 1171 * board.create('line', [helppoint1, helppoint2]); 1172 * board.create('line', [helppoint2, helppoint3]); 1173 * board.create('line', [helppoint3, helppoint4]); 1174 * board.create('line', [helppoint4, helppoint1]); 1175 * })(); 1176 * 1177 * </script><pre> 1178 * 1179 */ 1180 getPointLoc: function (position, margin) { 1181 var bbox, pos, res, marg; 1182 1183 bbox = this.getBoundingBox(); 1184 pos = position; 1185 if (pos.length === 2) { 1186 pos.unshift(undefined); 1187 } 1188 res = [0, 0]; 1189 marg = margin || 0; 1190 if (Type.isNumber(marg)) { 1191 marg = [marg, marg, marg, marg]; 1192 } 1193 1194 if (pos[1] > (bbox[2] - marg[1])) { 1195 res[0] = 1; 1196 } 1197 if (pos[1] < (bbox[0] + marg[3])) { 1198 res[0] = -1; 1199 } 1200 1201 if (pos[2] > (bbox[1] - marg[0])) { 1202 res[1] = 1; 1203 } 1204 if (pos[2] < (bbox[3] + marg[2])) { 1205 res[1] = -1; 1206 } 1207 1208 return res; 1209 }, 1210 1211 /** 1212 * This function calculates where the origin is located (@link Board#getPointLoc). 1213 * Optional a <tt>margin</tt> to the inner of the board is respected.<br> 1214 * 1215 * @name Board#getLocationOrigin 1216 * @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>. 1217 * @returns {Array} [u,v] which shows where the origin is located (@link Board#getPointLoc). 1218 */ 1219 getLocationOrigin: function (margin) { 1220 return this.getPointLoc([0, 0], margin); 1221 }, 1222 1223 /** 1224 * Get the position of the pointing device in screen coordinates, relative to the upper left corner 1225 * of the host tag. 1226 * @param {Event} e Event object given by the browser. 1227 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 1228 * for mouseevents. 1229 * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords} 1230 */ 1231 getMousePosition: function (e, i) { 1232 var cPos = this.getCoordsTopLeftCorner(), 1233 absPos, 1234 v; 1235 1236 // Position of cursor using clientX/Y 1237 absPos = Env.getPosition(e, i, this.document); 1238 1239 // Old: 1240 // This seems to be obsolete anyhow: 1241 // "In case there has been no down event before." 1242 // if (!Type.exists(this.cssTransMat)) { 1243 // this.updateCSSTransforms(); 1244 // } 1245 // New: 1246 // We have to update the CSS transform matrix all the time, 1247 // since libraries like ZIMJS do not notify JSXGraph about a change. 1248 // In particular, sending a resize event event to JSXGraph 1249 // would be necessary. 1250 this.updateCSSTransforms(); 1251 1252 // Position relative to the top left corner 1253 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 1254 v = Mat.matVecMult(this.cssTransMat, v); 1255 v[1] /= v[0]; 1256 v[2] /= v[0]; 1257 return [v[1], v[2]]; 1258 1259 // Method without CSS transformation 1260 /* 1261 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 1262 */ 1263 }, 1264 1265 /** 1266 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 1267 * @param {Number} x Current mouse/touch coordinates 1268 * @param {Number} y Current mouse/touch coordinates 1269 */ 1270 initMoveOrigin: function (x, y) { 1271 this.drag_dx = x - this.origin.scrCoords[1]; 1272 this.drag_dy = y - this.origin.scrCoords[2]; 1273 1274 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 1275 this.updateQuality = this.BOARD_QUALITY_LOW; 1276 }, 1277 1278 /** 1279 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 1280 * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul> 1281 * @param {Number} x Current mouse/touch coordinates 1282 * @param {Number} y current mouse/touch coordinates 1283 * @param {Object} evt An event object 1284 * @param {String} type What type of event? 'touch', 'mouse' or 'pen'. 1285 * @returns {Array} A list of geometric elements. 1286 */ 1287 initMoveObject: function (x, y, evt, type) { 1288 var pEl, 1289 el, 1290 collect = [], 1291 offset = [], 1292 haspoint, 1293 len = this.objectsList.length, 1294 dragEl = { visProp: { layer: -10000 } }; 1295 1296 // Store status of key presses for 3D movement 1297 this._shiftKey = evt.shiftKey; 1298 this._ctrlKey = evt.ctrlKey; 1299 1300 //for (el in this.objects) { 1301 for (el = 0; el < len; el++) { 1302 pEl = this.objectsList[el]; 1303 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 1304 1305 if (pEl.visPropCalc.visible && haspoint) { 1306 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 1307 this.downObjects.push(pEl); 1308 } 1309 1310 if (haspoint && 1311 pEl.isDraggable && 1312 pEl.visPropCalc.visible && 1313 ((this.geonextCompatibilityMode && 1314 (Type.isPoint(pEl) || pEl.elementClass === Const.OBJECT_CLASS_TEXT)) || 1315 !this.geonextCompatibilityMode) && 1316 !Type.evaluate(pEl.visProp.fixed) 1317 /*(!pEl.visProp.frozen) &&*/ 1318 ) { 1319 // Elements in the highest layer get priority. 1320 if ( 1321 pEl.visProp.layer > dragEl.visProp.layer || 1322 (pEl.visProp.layer === dragEl.visProp.layer && 1323 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()) 1324 ) { 1325 // If an element and its label have the focus 1326 // simultaneously, the element is taken. 1327 // This only works if we assume that every browser runs 1328 // through this.objects in the right order, i.e. an element A 1329 // added before element B turns up here before B does. 1330 if ( 1331 !this.attr.ignorelabels || 1332 !Type.exists(dragEl.label) || 1333 pEl !== dragEl.label 1334 ) { 1335 dragEl = pEl; 1336 collect.push(dragEl); 1337 // Save offset for large coords elements. 1338 if (Type.exists(dragEl.coords)) { 1339 offset.push( 1340 Statistics.subtract(dragEl.coords.scrCoords.slice(1), [ 1341 x, 1342 y 1343 ]) 1344 ); 1345 } else { 1346 offset.push([0, 0]); 1347 } 1348 1349 // we can't drop out of this loop because of the event handling system 1350 //if (this.attr.takefirst) { 1351 // return collect; 1352 //} 1353 } 1354 } 1355 } 1356 } 1357 1358 if (this.attr.drag.enabled && collect.length > 0) { 1359 this.mode = this.BOARD_MODE_DRAG; 1360 } 1361 1362 // A one-element array is returned. 1363 if (this.attr.takefirst) { 1364 collect.length = 1; 1365 this._drag_offset = offset[0]; 1366 } else { 1367 collect = collect.slice(-1); 1368 this._drag_offset = offset[offset.length - 1]; 1369 } 1370 1371 if (!this._drag_offset) { 1372 this._drag_offset = [0, 0]; 1373 } 1374 1375 // Move drag element to the top of the layer 1376 if (this.renderer.type === 'svg' && 1377 Type.exists(collect[0]) && 1378 Type.evaluate(collect[0].visProp.dragtotopoflayer) && 1379 collect.length === 1 && 1380 Type.exists(collect[0].rendNode) 1381 ) { 1382 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode); 1383 } 1384 1385 // // Init rotation angle and scale factor for two finger movements 1386 // this.previousRotation = 0.0; 1387 // this.previousScale = 1.0; 1388 1389 if (collect.length >= 1) { 1390 collect[0].highlight(true); 1391 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]); 1392 } 1393 1394 return collect; 1395 }, 1396 1397 /** 1398 * Moves an object. 1399 * @param {Number} x Coordinate 1400 * @param {Number} y Coordinate 1401 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 1402 * @param {Object} evt The event object. 1403 * @param {String} type Mouse or touch event? 1404 */ 1405 moveObject: function (x, y, o, evt, type) { 1406 var newPos = new Coords( 1407 Const.COORDS_BY_SCREEN, 1408 this.getScrCoordsOfMouse(x, y), 1409 this 1410 ), 1411 drag, 1412 dragScrCoords, 1413 newDragScrCoords; 1414 1415 if (!(o && o.obj)) { 1416 return; 1417 } 1418 drag = o.obj; 1419 1420 // Avoid updates for very small movements of coordsElements, see below 1421 if (drag.coords) { 1422 dragScrCoords = drag.coords.scrCoords.slice(); 1423 } 1424 1425 this.addLogEntry('drag', drag, newPos.usrCoords.slice(1)); 1426 1427 // Store the position. 1428 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 1429 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 1430 1431 // Store status of key presses for 3D movement 1432 this._shiftKey = evt.shiftKey; 1433 this._ctrlKey = evt.ctrlKey; 1434 1435 // 1436 // We have to distinguish between CoordsElements and other elements like lines. 1437 // The latter need the difference between two move events. 1438 if (Type.exists(drag.coords)) { 1439 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position); 1440 } else { 1441 this.displayInfobox(false); 1442 // Hide infobox in case the user has touched an intersection point 1443 // and drags the underlying line now. 1444 1445 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 1446 drag.setPositionDirectly( 1447 Const.COORDS_BY_SCREEN, 1448 [newPos.scrCoords[1], newPos.scrCoords[2]], 1449 [o.targets[0].Xprev, o.targets[0].Yprev] 1450 ); 1451 } 1452 // Remember the actual position for the next move event. Then we are able to 1453 // compute the difference vector. 1454 o.targets[0].Xprev = newPos.scrCoords[1]; 1455 o.targets[0].Yprev = newPos.scrCoords[2]; 1456 } 1457 // This may be necessary for some gliders and labels 1458 if (Type.exists(drag.coords)) { 1459 drag.prepareUpdate().update(false).updateRenderer(); 1460 this.updateInfobox(drag); 1461 drag.prepareUpdate().update(true).updateRenderer(); 1462 } 1463 1464 if (drag.coords) { 1465 newDragScrCoords = drag.coords.scrCoords; 1466 } 1467 // No updates for very small movements of coordsElements 1468 if ( 1469 !drag.coords || 1470 dragScrCoords[1] !== newDragScrCoords[1] || 1471 dragScrCoords[2] !== newDragScrCoords[2] 1472 ) { 1473 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1474 1475 this.update(); 1476 } 1477 drag.highlight(true); 1478 this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]); 1479 1480 drag.lastDragTime = new Date(); 1481 }, 1482 1483 /** 1484 * Moves elements in multitouch mode. 1485 * @param {Array} p1 x,y coordinates of first touch 1486 * @param {Array} p2 x,y coordinates of second touch 1487 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1488 * @param {Object} evt The event object that lead to this movement. 1489 */ 1490 twoFingerMove: function (o, id, evt) { 1491 var drag; 1492 1493 if (Type.exists(o) && Type.exists(o.obj)) { 1494 drag = o.obj; 1495 } else { 1496 return; 1497 } 1498 1499 if ( 1500 drag.elementClass === Const.OBJECT_CLASS_LINE || 1501 drag.type === Const.OBJECT_TYPE_POLYGON 1502 ) { 1503 this.twoFingerTouchObject(o.targets, drag, id); 1504 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1505 this.twoFingerTouchCircle(o.targets, drag, id); 1506 } 1507 1508 if (evt) { 1509 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1510 } 1511 }, 1512 1513 /** 1514 * Compute the transformation matrix to move an element according to the 1515 * previous and actual positions of finger 1 and finger 2. 1516 * See also https://math.stackexchange.com/questions/4010538/solve-for-2d-translation-rotation-and-scale-given-two-touch-point-movements 1517 * 1518 * @param {Object} finger1 Actual and previous position of finger 1 1519 * @param {Object} finger1 Actual and previous position of finger 1 1520 * @param {Boolean} scalable Flag if element may be scaled 1521 * @param {Boolean} rotatable Flag if element may be rotated 1522 * @returns {Array} 1523 */ 1524 getTwoFingerTransform(finger1, finger2, scalable, rotatable) { 1525 var crd, 1526 x1, y1, x2, y2, 1527 dx, dy, 1528 xx1, yy1, xx2, yy2, 1529 dxx, dyy, 1530 C, S, LL, tx, ty, lbda; 1531 1532 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.Xprev, finger1.Yprev], this).usrCoords; 1533 x1 = crd[1]; 1534 y1 = crd[2]; 1535 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.Xprev, finger2.Yprev], this).usrCoords; 1536 x2 = crd[1]; 1537 y2 = crd[2]; 1538 1539 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.X, finger1.Y], this).usrCoords; 1540 xx1 = crd[1]; 1541 yy1 = crd[2]; 1542 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.X, finger2.Y], this).usrCoords; 1543 xx2 = crd[1]; 1544 yy2 = crd[2]; 1545 1546 dx = x2 - x1; 1547 dy = y2 - y1; 1548 dxx = xx2 - xx1; 1549 dyy = yy2 - yy1; 1550 1551 LL = dx * dx + dy * dy; 1552 C = (dxx * dx + dyy * dy) / LL; 1553 S = (dyy * dx - dxx * dy) / LL; 1554 if (!scalable) { 1555 lbda = Mat.hypot(C, S); 1556 C /= lbda; 1557 S /= lbda; 1558 } 1559 if (!rotatable) { 1560 S = 0; 1561 } 1562 tx = 0.5 * (xx1 + xx2 - C * (x1 + x2) + S * (y1 + y2)); 1563 ty = 0.5 * (yy1 + yy2 - S * (x1 + x2) - C * (y1 + y2)); 1564 1565 return [1, 0, 0, 1566 tx, C, -S, 1567 ty, S, C]; 1568 }, 1569 1570 /** 1571 * Moves, rotates and scales a line or polygon with two fingers. 1572 * <p> 1573 * If one vertex of the polygon snaps to the grid or to points or is not draggable, 1574 * two-finger-movement is cancelled. 1575 * 1576 * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}. 1577 * @param {object} drag The object that is dragged: 1578 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1579 */ 1580 twoFingerTouchObject: function (tar, drag, id) { 1581 var t, T, 1582 ar, i, len, vp, 1583 snap = false; 1584 1585 if ( 1586 Type.exists(tar[0]) && 1587 Type.exists(tar[1]) && 1588 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1589 ) { 1590 1591 T = this.getTwoFingerTransform( 1592 tar[0], tar[1], 1593 Type.evaluate(drag.visProp.scalable), 1594 Type.evaluate(drag.visProp.rotatable)); 1595 t = this.create('transform', T, { type: 'generic' }); 1596 t.update(); 1597 1598 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1599 ar = []; 1600 if (drag.point1.draggable()) { 1601 ar.push(drag.point1); 1602 } 1603 if (drag.point2.draggable()) { 1604 ar.push(drag.point2); 1605 } 1606 t.applyOnce(ar); 1607 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1608 len = drag.vertices.length - 1; 1609 vp = drag.visProp; 1610 snap = Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints); 1611 for (i = 0; i < len && !snap; ++i) { 1612 vp = drag.vertices[i].visProp; 1613 snap = snap || Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints); 1614 snap = snap || (!drag.vertices[i].draggable()); 1615 } 1616 if (!snap) { 1617 ar = []; 1618 for (i = 0; i < len; ++i) { 1619 if (drag.vertices[i].draggable()) { 1620 ar.push(drag.vertices[i]); 1621 } 1622 } 1623 t.applyOnce(ar); 1624 } 1625 } 1626 1627 this.update(); 1628 drag.highlight(true); 1629 } 1630 }, 1631 1632 /* 1633 * Moves, rotates and scales a circle with two fingers. 1634 * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}. 1635 * @param {object} drag The object that is dragged: 1636 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1637 */ 1638 twoFingerTouchCircle: function (tar, drag, id) { 1639 var fixEl, moveEl, np, op, fix, d, alpha, t1, t2, t3, t4; 1640 1641 if (drag.method === 'pointCircle' || drag.method === 'pointLine') { 1642 return; 1643 } 1644 1645 if ( 1646 Type.exists(tar[0]) && 1647 Type.exists(tar[1]) && 1648 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1649 ) { 1650 if (id === tar[0].num) { 1651 fixEl = tar[1]; 1652 moveEl = tar[0]; 1653 } else { 1654 fixEl = tar[0]; 1655 moveEl = tar[1]; 1656 } 1657 1658 fix = new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this) 1659 .usrCoords; 1660 // Previous finger position 1661 op = new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this) 1662 .usrCoords; 1663 // New finger position 1664 np = new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this).usrCoords; 1665 1666 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1)); 1667 1668 // Rotate and scale by the movement of the second finger 1669 t1 = this.create('transform', [-fix[1], -fix[2]], { 1670 type: 'translate' 1671 }); 1672 t2 = this.create('transform', [alpha], { type: 'rotate' }); 1673 t1.melt(t2); 1674 if (Type.evaluate(drag.visProp.scalable)) { 1675 d = Geometry.distance(fix, np) / Geometry.distance(fix, op); 1676 t3 = this.create('transform', [d, d], { type: 'scale' }); 1677 t1.melt(t3); 1678 } 1679 t4 = this.create('transform', [fix[1], fix[2]], { 1680 type: 'translate' 1681 }); 1682 t1.melt(t4); 1683 1684 if (drag.center.draggable()) { 1685 t1.applyOnce([drag.center]); 1686 } 1687 1688 if (drag.method === 'twoPoints') { 1689 if (drag.point2.draggable()) { 1690 t1.applyOnce([drag.point2]); 1691 } 1692 } else if (drag.method === 'pointRadius') { 1693 if (Type.isNumber(drag.updateRadius.origin)) { 1694 drag.setRadius(drag.radius * d); 1695 } 1696 } 1697 1698 this.update(drag.center); 1699 drag.highlight(true); 1700 } 1701 }, 1702 1703 highlightElements: function (x, y, evt, target) { 1704 var el, 1705 pEl, 1706 pId, 1707 overObjects = {}, 1708 len = this.objectsList.length; 1709 1710 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1711 for (el = 0; el < len; el++) { 1712 pEl = this.objectsList[el]; 1713 pId = pEl.id; 1714 if ( 1715 Type.exists(pEl.hasPoint) && 1716 pEl.visPropCalc.visible && 1717 pEl.hasPoint(x, y) 1718 ) { 1719 // this is required in any case because otherwise the box won't be shown until the point is dragged 1720 this.updateInfobox(pEl); 1721 1722 if (!Type.exists(this.highlightedObjects[pId])) { 1723 // highlight only if not highlighted 1724 overObjects[pId] = pEl; 1725 pEl.highlight(); 1726 // triggers board event. 1727 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1728 } 1729 1730 if (pEl.mouseover) { 1731 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1732 } else { 1733 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1734 pEl.mouseover = true; 1735 } 1736 } 1737 } 1738 1739 for (el = 0; el < len; el++) { 1740 pEl = this.objectsList[el]; 1741 pId = pEl.id; 1742 if (pEl.mouseover) { 1743 if (!overObjects[pId]) { 1744 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1745 pEl.mouseover = false; 1746 } 1747 } 1748 } 1749 }, 1750 1751 /** 1752 * Helper function which returns a reasonable starting point for the object being dragged. 1753 * Formerly known as initXYstart(). 1754 * @private 1755 * @param {JXG.GeometryElement} obj The object to be dragged 1756 * @param {Array} targets Array of targets. It is changed by this function. 1757 */ 1758 saveStartPos: function (obj, targets) { 1759 var xy = [], 1760 i, 1761 len; 1762 1763 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1764 xy.push([1, NaN, NaN]); 1765 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1766 xy.push(obj.point1.coords.usrCoords); 1767 xy.push(obj.point2.coords.usrCoords); 1768 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1769 xy.push(obj.center.coords.usrCoords); 1770 if (obj.method === 'twoPoints') { 1771 xy.push(obj.point2.coords.usrCoords); 1772 } 1773 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1774 len = obj.vertices.length - 1; 1775 for (i = 0; i < len; i++) { 1776 xy.push(obj.vertices[i].coords.usrCoords); 1777 } 1778 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1779 xy.push(obj.point1.coords.usrCoords); 1780 xy.push(obj.point2.coords.usrCoords); 1781 xy.push(obj.point3.coords.usrCoords); 1782 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1783 xy.push(obj.coords.usrCoords); 1784 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1785 // if (Type.exists(obj.parents)) { 1786 // len = obj.parents.length; 1787 // if (len > 0) { 1788 // for (i = 0; i < len; i++) { 1789 // xy.push(this.select(obj.parents[i]).coords.usrCoords); 1790 // } 1791 // } else 1792 // } 1793 if (obj.points.length > 0) { 1794 xy.push(obj.points[0].usrCoords); 1795 } 1796 } else { 1797 try { 1798 xy.push(obj.coords.usrCoords); 1799 } catch (e) { 1800 JXG.debug( 1801 'JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e 1802 ); 1803 } 1804 } 1805 1806 len = xy.length; 1807 for (i = 0; i < len; i++) { 1808 targets.Zstart.push(xy[i][0]); 1809 targets.Xstart.push(xy[i][1]); 1810 targets.Ystart.push(xy[i][2]); 1811 } 1812 }, 1813 1814 mouseOriginMoveStart: function (evt) { 1815 var r, pos; 1816 1817 r = this._isRequiredKeyPressed(evt, 'pan'); 1818 if (r) { 1819 pos = this.getMousePosition(evt); 1820 this.initMoveOrigin(pos[0], pos[1]); 1821 } 1822 1823 return r; 1824 }, 1825 1826 mouseOriginMove: function (evt) { 1827 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1828 pos; 1829 1830 if (r) { 1831 pos = this.getMousePosition(evt); 1832 this.moveOrigin(pos[0], pos[1], true); 1833 } 1834 1835 return r; 1836 }, 1837 1838 /** 1839 * Start moving the origin with one finger. 1840 * @private 1841 * @param {Object} evt Event from touchStartListener 1842 * @return {Boolean} returns if the origin is moved. 1843 */ 1844 touchStartMoveOriginOneFinger: function (evt) { 1845 var touches = evt['touches'], 1846 conditions, 1847 pos; 1848 1849 conditions = 1850 this.attr.pan.enabled && !this.attr.pan.needtwofingers && touches.length === 1; 1851 1852 if (conditions) { 1853 pos = this.getMousePosition(evt, 0); 1854 this.initMoveOrigin(pos[0], pos[1]); 1855 } 1856 1857 return conditions; 1858 }, 1859 1860 /** 1861 * Move the origin with one finger 1862 * @private 1863 * @param {Object} evt Event from touchMoveListener 1864 * @return {Boolean} returns if the origin is moved. 1865 */ 1866 touchOriginMove: function (evt) { 1867 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1868 pos; 1869 1870 if (r) { 1871 pos = this.getMousePosition(evt, 0); 1872 this.moveOrigin(pos[0], pos[1], true); 1873 } 1874 1875 return r; 1876 }, 1877 1878 /** 1879 * Stop moving the origin with one finger 1880 * @return {null} null 1881 * @private 1882 */ 1883 originMoveEnd: function () { 1884 this.updateQuality = this.BOARD_QUALITY_HIGH; 1885 this.mode = this.BOARD_MODE_NONE; 1886 }, 1887 1888 /********************************************************** 1889 * 1890 * Event Handler 1891 * 1892 **********************************************************/ 1893 1894 /** 1895 * Suppresses the default event handling. 1896 * Used for context menu. 1897 * 1898 * @param {Event} e 1899 * @returns {Boolean} false 1900 */ 1901 suppressDefault: function (e) { 1902 if (Type.exists(e)) { 1903 e.preventDefault(); 1904 } 1905 return false; 1906 }, 1907 1908 /** 1909 * Add all possible event handlers to the board object 1910 * that move objects, i.e. mouse, pointer and touch events. 1911 */ 1912 addEventHandlers: function () { 1913 if (Env.supportsPointerEvents()) { 1914 this.addPointerEventHandlers(); 1915 } else { 1916 this.addMouseEventHandlers(); 1917 this.addTouchEventHandlers(); 1918 } 1919 1920 if (this.containerObj !== null) { 1921 // this.containerObj.oncontextmenu = this.suppressDefault; 1922 Env.addEvent(this.containerObj, 'contextmenu', this.suppressDefault, this); 1923 } 1924 1925 // This one produces errors on IE 1926 // // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 1927 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 1928 // or Opera versions the user must explicitly allow the deactivation of the context menu. 1929 }, 1930 1931 /** 1932 * Remove all event handlers from the board object 1933 */ 1934 removeEventHandlers: function () { 1935 if ((this.hasPointerHandlers || this.hasMouseHandlers || this.hasTouchHandlers) && 1936 this.containerObj !== null 1937 ) { 1938 Env.removeEvent(this.containerObj, 'contextmenu', this.suppressDefault, this); 1939 } 1940 1941 this.removeMouseEventHandlers(); 1942 this.removeTouchEventHandlers(); 1943 this.removePointerEventHandlers(); 1944 1945 this.removeFullscreenEventHandlers(); 1946 this.removeKeyboardEventHandlers(); 1947 this.removeResizeEventHandlers(); 1948 1949 // if (Env.isBrowser) { 1950 // if (Type.exists(this.resizeObserver)) { 1951 // this.stopResizeObserver(); 1952 // } else { 1953 // Env.removeEvent(window, 'resize', this.resizeListener, this); 1954 // this.stopIntersectionObserver(); 1955 // } 1956 // Env.removeEvent(window, 'scroll', this.scrollListener, this); 1957 // } 1958 }, 1959 1960 /** 1961 * Add resize related event handlers 1962 * 1963 */ 1964 addResizeEventHandlers: function () { 1965 // var that = this; 1966 1967 this.resizeHandlers = []; 1968 if (Env.isBrowser) { 1969 try { 1970 // Supported by all new browsers 1971 // resizeObserver: triggered if size of the JSXGraph div changes. 1972 this.startResizeObserver(); 1973 this.resizeHandlers.push('resizeobserver'); 1974 } catch (err) { 1975 // Certain Safari and edge version do not support 1976 // resizeObserver, but intersectionObserver. 1977 // resize event: triggered if size of window changes 1978 Env.addEvent(window, 'resize', this.resizeListener, this); 1979 // intersectionObserver: triggered if JSXGraph becomes visible. 1980 this.startIntersectionObserver(); 1981 this.resizeHandlers.push('resize'); 1982 } 1983 // Scroll event: needs to be captured since on mobile devices 1984 // sometimes a header bar is displayed / hidden, which triggers a 1985 // resize event. 1986 Env.addEvent(window, 'scroll', this.scrollListener, this); 1987 this.resizeHandlers.push('scroll'); 1988 1989 // On browser print: 1990 // we need to call the listener when having @media: print. 1991 try { 1992 // window.matchMedia("print").addEventListener('change', this.printListenerMatch.apply(this, arguments)); 1993 window.matchMedia("print").addEventListener('change', this.printListenerMatch.bind(this)); 1994 window.matchMedia("screen").addEventListener('change', this.printListenerMatch.bind(this)); 1995 this.resizeHandlers.push('print'); 1996 } catch (err) { 1997 JXG.debug("Error adding printListener", err); 1998 } 1999 // if (Type.isFunction(MediaQueryList.prototype.addEventListener)) { 2000 // window.matchMedia("print").addEventListener('change', function (mql) { 2001 // if (mql.matches) { 2002 // that.printListener(); 2003 // } 2004 // }); 2005 // } else if (Type.isFunction(MediaQueryList.prototype.addListener)) { // addListener might be deprecated 2006 // window.matchMedia("print").addListener(function (mql, ev) { 2007 // if (mql.matches) { 2008 // that.printListener(ev); 2009 // } 2010 // }); 2011 // } 2012 2013 // When closing the print dialog we again have to resize. 2014 // Env.addEvent(window, 'afterprint', this.printListener, this); 2015 // this.resizeHandlers.push('afterprint'); 2016 } 2017 }, 2018 2019 /** 2020 * Remove resize related event handlers 2021 * 2022 */ 2023 removeResizeEventHandlers: function () { 2024 var i, e; 2025 if (this.resizeHandlers.length > 0 && Env.isBrowser) { 2026 for (i = 0; i < this.resizeHandlers.length; i++) { 2027 e = this.resizeHandlers[i]; 2028 switch (e) { 2029 case 'resizeobserver': 2030 if (Type.exists(this.resizeObserver)) { 2031 this.stopResizeObserver(); 2032 } 2033 break; 2034 case 'resize': 2035 Env.removeEvent(window, 'resize', this.resizeListener, this); 2036 if (Type.exists(this.intersectionObserver)) { 2037 this.stopIntersectionObserver(); 2038 } 2039 break; 2040 case 'scroll': 2041 Env.removeEvent(window, 'scroll', this.scrollListener, this); 2042 break; 2043 case 'print': 2044 window.matchMedia("print").removeEventListener('change', this.printListenerMatch.bind(this), false); 2045 window.matchMedia("screen").removeEventListener('change', this.printListenerMatch.bind(this), false); 2046 break; 2047 // case 'afterprint': 2048 // Env.removeEvent(window, 'afterprint', this.printListener, this); 2049 // break; 2050 } 2051 } 2052 this.resizeHandlers = []; 2053 } 2054 }, 2055 2056 2057 /** 2058 * Registers pointer event handlers. 2059 */ 2060 addPointerEventHandlers: function () { 2061 if (!this.hasPointerHandlers && Env.isBrowser) { 2062 var moveTarget = this.attr.movetarget || this.containerObj; 2063 2064 if (window.navigator.msPointerEnabled) { 2065 // IE10- 2066 Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2067 Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2068 } else { 2069 Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2070 Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2071 Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2072 Env.addEvent(moveTarget, 'click', this.pointerClickListener, this); 2073 Env.addEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2074 } 2075 2076 if (this.containerObj !== null) { 2077 // This is needed for capturing touch events. 2078 // It is in jsxgraph.css, for ms-touch-action... 2079 this.containerObj.style.touchAction = 'none'; 2080 } 2081 2082 this.hasPointerHandlers = true; 2083 } 2084 }, 2085 2086 /** 2087 * Registers mouse move, down and wheel event handlers. 2088 */ 2089 addMouseEventHandlers: function () { 2090 if (!this.hasMouseHandlers && Env.isBrowser) { 2091 var moveTarget = this.attr.movetarget || this.containerObj; 2092 2093 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2094 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2095 Env.addEvent(moveTarget, 'click', this.mouseClickListener, this); 2096 Env.addEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2097 2098 this.hasMouseHandlers = true; 2099 } 2100 }, 2101 2102 /** 2103 * Register touch start and move and gesture start and change event handlers. 2104 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 2105 * will not be registered. 2106 * 2107 * Since iOS 13, touch events were abandoned in favour of pointer events 2108 */ 2109 addTouchEventHandlers: function (appleGestures) { 2110 if (!this.hasTouchHandlers && Env.isBrowser) { 2111 var moveTarget = this.attr.movetarget || this.containerObj; 2112 2113 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2114 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2115 2116 /* 2117 if (!Type.exists(appleGestures) || appleGestures) { 2118 // Gesture listener are called in touchStart and touchMove. 2119 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 2120 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 2121 } 2122 */ 2123 2124 this.hasTouchHandlers = true; 2125 } 2126 }, 2127 2128 /** 2129 * Registers pointer event handlers. 2130 */ 2131 addWheelEventHandlers: function () { 2132 if (!this.hasWheelHandlers && Env.isBrowser) { 2133 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2134 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2135 this.hasWheelHandlers = true; 2136 } 2137 }, 2138 2139 /** 2140 * Add fullscreen events which update the CSS transformation matrix to correct 2141 * the mouse/touch/pointer positions in case of CSS transformations. 2142 */ 2143 addFullscreenEventHandlers: function () { 2144 var i, 2145 // standard/Edge, firefox, chrome/safari, IE11 2146 events = [ 2147 'fullscreenchange', 2148 'mozfullscreenchange', 2149 'webkitfullscreenchange', 2150 'msfullscreenchange' 2151 ], 2152 le = events.length; 2153 2154 if (!this.hasFullscreenEventHandlers && Env.isBrowser) { 2155 for (i = 0; i < le; i++) { 2156 Env.addEvent(this.document, events[i], this.fullscreenListener, this); 2157 } 2158 this.hasFullscreenEventHandlers = true; 2159 } 2160 }, 2161 2162 /** 2163 * Register keyboard event handlers. 2164 */ 2165 addKeyboardEventHandlers: function () { 2166 if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) { 2167 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2168 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2169 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2170 this.hasKeyboardHandlers = true; 2171 } 2172 }, 2173 2174 /** 2175 * Remove all registered touch event handlers. 2176 */ 2177 removeKeyboardEventHandlers: function () { 2178 if (this.hasKeyboardHandlers && Env.isBrowser) { 2179 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2180 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2181 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2182 this.hasKeyboardHandlers = false; 2183 } 2184 }, 2185 2186 /** 2187 * Remove all registered event handlers regarding fullscreen mode. 2188 */ 2189 removeFullscreenEventHandlers: function () { 2190 var i, 2191 // standard/Edge, firefox, chrome/safari, IE11 2192 events = [ 2193 'fullscreenchange', 2194 'mozfullscreenchange', 2195 'webkitfullscreenchange', 2196 'msfullscreenchange' 2197 ], 2198 le = events.length; 2199 2200 if (this.hasFullscreenEventHandlers && Env.isBrowser) { 2201 for (i = 0; i < le; i++) { 2202 Env.removeEvent(this.document, events[i], this.fullscreenListener, this); 2203 } 2204 this.hasFullscreenEventHandlers = false; 2205 } 2206 }, 2207 2208 /** 2209 * Remove MSPointer* Event handlers. 2210 */ 2211 removePointerEventHandlers: function () { 2212 if (this.hasPointerHandlers && Env.isBrowser) { 2213 var moveTarget = this.attr.movetarget || this.containerObj; 2214 2215 if (window.navigator.msPointerEnabled) { 2216 // IE10- 2217 Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2218 Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2219 } else { 2220 Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2221 Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2222 Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2223 Env.removeEvent(moveTarget, 'click', this.pointerClickListener, this); 2224 Env.removeEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2225 } 2226 2227 if (this.hasWheelHandlers) { 2228 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2229 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2230 } 2231 2232 if (this.hasPointerUp) { 2233 if (window.navigator.msPointerEnabled) { 2234 // IE10- 2235 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2236 } else { 2237 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2238 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2239 } 2240 this.hasPointerUp = false; 2241 } 2242 2243 this.hasPointerHandlers = false; 2244 } 2245 }, 2246 2247 /** 2248 * De-register mouse event handlers. 2249 */ 2250 removeMouseEventHandlers: function () { 2251 if (this.hasMouseHandlers && Env.isBrowser) { 2252 var moveTarget = this.attr.movetarget || this.containerObj; 2253 2254 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2255 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2256 Env.removeEvent(moveTarget, 'click', this.mouseClickListener, this); 2257 Env.removeEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2258 2259 if (this.hasMouseUp) { 2260 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 2261 this.hasMouseUp = false; 2262 } 2263 2264 if (this.hasWheelHandlers) { 2265 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2266 Env.removeEvent( 2267 this.containerObj, 2268 'DOMMouseScroll', 2269 this.mouseWheelListener, 2270 this 2271 ); 2272 } 2273 2274 this.hasMouseHandlers = false; 2275 } 2276 }, 2277 2278 /** 2279 * Remove all registered touch event handlers. 2280 */ 2281 removeTouchEventHandlers: function () { 2282 if (this.hasTouchHandlers && Env.isBrowser) { 2283 var moveTarget = this.attr.movetarget || this.containerObj; 2284 2285 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2286 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2287 2288 if (this.hasTouchEnd) { 2289 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 2290 this.hasTouchEnd = false; 2291 } 2292 2293 this.hasTouchHandlers = false; 2294 } 2295 }, 2296 2297 /** 2298 * Handler for click on left arrow in the navigation bar 2299 * @returns {JXG.Board} Reference to the board 2300 */ 2301 clickLeftArrow: function () { 2302 this.moveOrigin( 2303 this.origin.scrCoords[1] + this.canvasWidth * 0.1, 2304 this.origin.scrCoords[2] 2305 ); 2306 return this; 2307 }, 2308 2309 /** 2310 * Handler for click on right arrow in the navigation bar 2311 * @returns {JXG.Board} Reference to the board 2312 */ 2313 clickRightArrow: 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 up arrow in the navigation bar 2323 * @returns {JXG.Board} Reference to the board 2324 */ 2325 clickUpArrow: function () { 2326 this.moveOrigin( 2327 this.origin.scrCoords[1], 2328 this.origin.scrCoords[2] - this.canvasHeight * 0.1 2329 ); 2330 return this; 2331 }, 2332 2333 /** 2334 * Handler for click on down arrow in the navigation bar 2335 * @returns {JXG.Board} Reference to the board 2336 */ 2337 clickDownArrow: 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 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 2347 * Works on iOS/Safari and Android. 2348 * @param {Event} evt Browser event object 2349 * @returns {Boolean} 2350 */ 2351 gestureChangeListener: function (evt) { 2352 var c, 2353 dir1 = [], 2354 dir2 = [], 2355 angle, 2356 mi = 10, 2357 isPinch = false, 2358 // Save zoomFactors 2359 zx = this.attr.zoom.factorx, 2360 zy = this.attr.zoom.factory, 2361 factor, dist, theta, bound, 2362 zoomCenter, 2363 doZoom = false, 2364 dx, dy, cx, cy; 2365 2366 if (this.mode !== this.BOARD_MODE_ZOOM) { 2367 return true; 2368 } 2369 evt.preventDefault(); 2370 2371 dist = Geometry.distance( 2372 [evt.touches[0].clientX, evt.touches[0].clientY], 2373 [evt.touches[1].clientX, evt.touches[1].clientY], 2374 2 2375 ); 2376 2377 // Android pinch to zoom 2378 // evt.scale was available in iOS touch events (pre iOS 13) 2379 // evt.scale is undefined in Android 2380 if (evt.scale === undefined) { 2381 evt.scale = dist / this.prevDist; 2382 } 2383 2384 if (!Type.exists(this.prevCoords)) { 2385 return false; 2386 } 2387 // Compute the angle of the two finger directions 2388 dir1 = [ 2389 evt.touches[0].clientX - this.prevCoords[0][0], 2390 evt.touches[0].clientY - this.prevCoords[0][1] 2391 ]; 2392 dir2 = [ 2393 evt.touches[1].clientX - this.prevCoords[1][0], 2394 evt.touches[1].clientY - this.prevCoords[1][1] 2395 ]; 2396 2397 if ( 2398 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi && 2399 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi 2400 ) { 2401 return false; 2402 } 2403 2404 angle = Geometry.rad(dir1, [0, 0], dir2); 2405 if ( 2406 this.isPreviousGesture !== 'pan' && 2407 Math.abs(angle) > Math.PI * 0.2 && 2408 Math.abs(angle) < Math.PI * 1.8 2409 ) { 2410 isPinch = true; 2411 } 2412 2413 if (this.isPreviousGesture !== 'pan' && !isPinch) { 2414 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) { 2415 isPinch = true; 2416 } 2417 } 2418 2419 factor = evt.scale / this.prevScale; 2420 this.prevScale = evt.scale; 2421 this.prevCoords = [ 2422 [evt.touches[0].clientX, evt.touches[0].clientY], 2423 [evt.touches[1].clientX, evt.touches[1].clientY] 2424 ]; 2425 2426 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 2427 2428 if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) { 2429 // Pan detected 2430 this.isPreviousGesture = 'pan'; 2431 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 2432 2433 } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) { 2434 doZoom = false; 2435 zoomCenter = this.attr.zoom.center; 2436 // Pinch detected 2437 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 2438 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 2439 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 2440 theta = Math.abs(Math.atan2(dy, dx)); 2441 bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0; 2442 } 2443 2444 if (!this.keepaspectratio && 2445 this.attr.zoom.pinchhorizontal && 2446 theta < bound) { 2447 this.attr.zoom.factorx = factor; 2448 this.attr.zoom.factory = 1.0; 2449 cx = 0; 2450 cy = 0; 2451 doZoom = true; 2452 } else if (!this.keepaspectratio && 2453 this.attr.zoom.pinchvertical && 2454 Math.abs(theta - Math.PI * 0.5) < bound 2455 ) { 2456 this.attr.zoom.factorx = 1.0; 2457 this.attr.zoom.factory = factor; 2458 cx = 0; 2459 cy = 0; 2460 doZoom = true; 2461 } else if (this.attr.zoom.pinch) { 2462 this.attr.zoom.factorx = factor; 2463 this.attr.zoom.factory = factor; 2464 cx = c.usrCoords[1]; 2465 cy = c.usrCoords[2]; 2466 doZoom = true; 2467 } 2468 2469 if (doZoom) { 2470 if (zoomCenter === 'board') { 2471 this.zoomIn(); 2472 } else { // including zoomCenter === 'auto' 2473 this.zoomIn(cx, cy); 2474 } 2475 2476 // Restore zoomFactors 2477 this.attr.zoom.factorx = zx; 2478 this.attr.zoom.factory = zy; 2479 } 2480 } 2481 2482 return false; 2483 }, 2484 2485 /** 2486 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 2487 * on Android we emulate it. 2488 * @param {Event} evt 2489 * @returns {Boolean} 2490 */ 2491 gestureStartListener: function (evt) { 2492 var pos; 2493 2494 evt.preventDefault(); 2495 this.prevScale = 1.0; 2496 // Android pinch to zoom 2497 this.prevDist = Geometry.distance( 2498 [evt.touches[0].clientX, evt.touches[0].clientY], 2499 [evt.touches[1].clientX, evt.touches[1].clientY], 2500 2 2501 ); 2502 this.prevCoords = [ 2503 [evt.touches[0].clientX, evt.touches[0].clientY], 2504 [evt.touches[1].clientX, evt.touches[1].clientY] 2505 ]; 2506 this.isPreviousGesture = 'none'; 2507 2508 // If pinch-to-zoom is interpreted as panning 2509 // we have to prepare move origin 2510 pos = this.getMousePosition(evt, 0); 2511 this.initMoveOrigin(pos[0], pos[1]); 2512 2513 this.mode = this.BOARD_MODE_ZOOM; 2514 return false; 2515 }, 2516 2517 /** 2518 * Test if the required key combination is pressed for wheel zoom, move origin and 2519 * selection 2520 * @private 2521 * @param {Object} evt Mouse or pen event 2522 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 2523 * Corresponds to the attribute subobject. 2524 * @return {Boolean} true or false. 2525 */ 2526 _isRequiredKeyPressed: function (evt, action) { 2527 var obj = this.attr[action]; 2528 if (!obj.enabled) { 2529 return false; 2530 } 2531 2532 if ( 2533 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 2534 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 2535 ) { 2536 return true; 2537 } 2538 2539 return false; 2540 }, 2541 2542 /* 2543 * Pointer events 2544 */ 2545 2546 /** 2547 * 2548 * Check if pointer event is already registered in {@link JXG.Board#_board_touches}. 2549 * 2550 * @param {Object} evt Event object 2551 * @return {Boolean} true if down event has already been sent. 2552 * @private 2553 */ 2554 _isPointerRegistered: function (evt) { 2555 var i, 2556 len = this._board_touches.length; 2557 2558 for (i = 0; i < len; i++) { 2559 if (this._board_touches[i].pointerId === evt.pointerId) { 2560 return true; 2561 } 2562 } 2563 return false; 2564 }, 2565 2566 /** 2567 * 2568 * Store the position of a pointer event. 2569 * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}. 2570 * Allows to follow the path of that finger on the screen. 2571 * Only two simultaneous touches are supported. 2572 * 2573 * @param {Object} evt Event object 2574 * @returns {JXG.Board} Reference to the board 2575 * @private 2576 */ 2577 _pointerStorePosition: function (evt) { 2578 var i, found; 2579 2580 for (i = 0, found = false; i < this._board_touches.length; i++) { 2581 if (this._board_touches[i].pointerId === evt.pointerId) { 2582 this._board_touches[i].clientX = evt.clientX; 2583 this._board_touches[i].clientY = evt.clientY; 2584 found = true; 2585 break; 2586 } 2587 } 2588 2589 // Restrict the number of simultaneous touches to 2 2590 if (!found && this._board_touches.length < 2) { 2591 this._board_touches.push({ 2592 pointerId: evt.pointerId, 2593 clientX: evt.clientX, 2594 clientY: evt.clientY 2595 }); 2596 } 2597 2598 return this; 2599 }, 2600 2601 /** 2602 * Deregisters a pointer event in {@link JXG.Board#_board_touches}. 2603 * It happens if a finger has been lifted from the screen. 2604 * 2605 * @param {Object} evt Event object 2606 * @returns {JXG.Board} Reference to the board 2607 * @private 2608 */ 2609 _pointerRemoveTouches: function (evt) { 2610 var i; 2611 for (i = 0; i < this._board_touches.length; i++) { 2612 if (this._board_touches[i].pointerId === evt.pointerId) { 2613 this._board_touches.splice(i, 1); 2614 break; 2615 } 2616 } 2617 2618 return this; 2619 }, 2620 2621 /** 2622 * Remove all registered fingers from {@link JXG.Board#_board_touches}. 2623 * This might be necessary if too many fingers have been registered. 2624 * @returns {JXG.Board} Reference to the board 2625 * @private 2626 */ 2627 _pointerClearTouches: function (pId) { 2628 // var i; 2629 // if (pId) { 2630 // for (i = 0; i < this._board_touches.length; i++) { 2631 // if (pId === this._board_touches[i].pointerId) { 2632 // this._board_touches.splice(i, i); 2633 // break; 2634 // } 2635 // } 2636 // } else { 2637 // } 2638 if (this._board_touches.length > 0) { 2639 this.dehighlightAll(); 2640 } 2641 this.updateQuality = this.BOARD_QUALITY_HIGH; 2642 this.mode = this.BOARD_MODE_NONE; 2643 this._board_touches = []; 2644 this.touches = []; 2645 }, 2646 2647 /** 2648 * Determine which input device is used for this action. 2649 * Possible devices are 'touch', 'pen' and 'mouse'. 2650 * This affects the precision and certain events. 2651 * In case of no browser, 'mouse' is used. 2652 * 2653 * @see JXG.Board#pointerDownListener 2654 * @see JXG.Board#pointerMoveListener 2655 * @see JXG.Board#initMoveObject 2656 * @see JXG.Board#moveObject 2657 * 2658 * @param {Event} evt The browsers event object. 2659 * @returns {String} 'mouse', 'pen', or 'touch' 2660 * @private 2661 */ 2662 _getPointerInputDevice: function (evt) { 2663 if (Env.isBrowser) { 2664 if ( 2665 evt.pointerType === 'touch' || // New 2666 (window.navigator.msMaxTouchPoints && // Old 2667 window.navigator.msMaxTouchPoints > 1) 2668 ) { 2669 return 'touch'; 2670 } 2671 if (evt.pointerType === 'mouse') { 2672 return 'mouse'; 2673 } 2674 if (evt.pointerType === 'pen') { 2675 return 'pen'; 2676 } 2677 } 2678 return 'mouse'; 2679 }, 2680 2681 /** 2682 * This method is called by the browser when a pointing device is pressed on the screen. 2683 * @param {Event} evt The browsers event object. 2684 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 2685 * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault() 2686 * @returns {Boolean} false if the first finger event is sent twice, or not a browser, or in selection mode. Otherwise returns true. 2687 */ 2688 pointerDownListener: function (evt, object, allowDefaultEventHandling) { 2689 var i, j, k, pos, 2690 elements, sel, target_obj, 2691 type = 'mouse', // Used in case of no browser 2692 found, target, ta; 2693 2694 // Fix for Firefox browser: When using a second finger, the 2695 // touch event for the first finger is sent again. 2696 if (!object && this._isPointerRegistered(evt)) { 2697 return false; 2698 } 2699 2700 if (Type.evaluate(this.attr.movetarget) === null && 2701 Type.exists(evt.target) && Type.exists(evt.target.releasePointerCapture)) { 2702 evt.target.releasePointerCapture(evt.pointerId); 2703 } 2704 2705 if (!object && evt.isPrimary) { 2706 // First finger down. To be on the safe side this._board_touches is cleared. 2707 // this._pointerClearTouches(); 2708 } 2709 2710 if (!this.hasPointerUp) { 2711 if (window.navigator.msPointerEnabled) { 2712 // IE10- 2713 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2714 } else { 2715 // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android 2716 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 2717 Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2718 } 2719 this.hasPointerUp = true; 2720 } 2721 2722 if (this.hasMouseHandlers) { 2723 this.removeMouseEventHandlers(); 2724 } 2725 2726 if (this.hasTouchHandlers) { 2727 this.removeTouchEventHandlers(); 2728 } 2729 2730 // Prevent accidental selection of text 2731 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2732 this.document.selection.empty(); 2733 } else if (window.getSelection) { 2734 sel = window.getSelection(); 2735 if (sel.removeAllRanges) { 2736 try { 2737 sel.removeAllRanges(); 2738 } catch (e) { } 2739 } 2740 } 2741 2742 // Mouse, touch or pen device 2743 this._inputDevice = this._getPointerInputDevice(evt); 2744 type = this._inputDevice; 2745 this.options.precision.hasPoint = this.options.precision[type]; 2746 2747 // Handling of multi touch with pointer events should be easier than with touch events. 2748 // Every pointer device has its own pointerId, e.g. the mouse 2749 // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will 2750 // keep this id until a pointerUp event is fired. What we have to do here is: 2751 // 1. collect all elements under the current pointer 2752 // 2. run through the touches control structure 2753 // a. look for the object collected in step 1. 2754 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 2755 pos = this.getMousePosition(evt); 2756 2757 // Handle selection rectangle 2758 this._testForSelection(evt); 2759 if (this.selectingMode) { 2760 this._startSelecting(pos); 2761 this.triggerEventHandlers( 2762 ['touchstartselecting', 'pointerstartselecting', 'startselecting'], 2763 [evt] 2764 ); 2765 return; // don't continue as a normal click 2766 } 2767 2768 if (this.attr.drag.enabled && object) { 2769 elements = [object]; 2770 this.mode = this.BOARD_MODE_DRAG; 2771 } else { 2772 elements = this.initMoveObject(pos[0], pos[1], evt, type); 2773 } 2774 2775 target_obj = { 2776 num: evt.pointerId, 2777 X: pos[0], 2778 Y: pos[1], 2779 Xprev: NaN, 2780 Yprev: NaN, 2781 Xstart: [], 2782 Ystart: [], 2783 Zstart: [] 2784 }; 2785 2786 // If no draggable object can be found, get out here immediately 2787 if (elements.length > 0) { 2788 // check touches structure 2789 target = elements[elements.length - 1]; 2790 found = false; 2791 2792 // Reminder: this.touches is the list of elements which 2793 // currently 'possess' a pointer (mouse, pen, finger) 2794 for (i = 0; i < this.touches.length; i++) { 2795 // An element receives a further touch, i.e. 2796 // the target is already in our touches array, add the pointer to the existing touch 2797 if (this.touches[i].obj === target) { 2798 j = i; 2799 k = this.touches[i].targets.push(target_obj) - 1; 2800 found = true; 2801 break; 2802 } 2803 } 2804 if (!found) { 2805 // A new element has been touched. 2806 k = 0; 2807 j = 2808 this.touches.push({ 2809 obj: target, 2810 targets: [target_obj] 2811 }) - 1; 2812 } 2813 2814 this.dehighlightAll(); 2815 target.highlight(true); 2816 2817 this.saveStartPos(target, this.touches[j].targets[k]); 2818 2819 // Prevent accidental text selection 2820 // this could get us new trouble: input fields, links and drop down boxes placed as text 2821 // on the board don't work anymore. 2822 if (evt && evt.preventDefault && !allowDefaultEventHandling) { 2823 // All browser supporting pointer events know preventDefault() 2824 evt.preventDefault(); 2825 } 2826 } 2827 2828 if (this.touches.length > 0 && !allowDefaultEventHandling) { 2829 evt.preventDefault(); 2830 evt.stopPropagation(); 2831 } 2832 2833 if (!Env.isBrowser) { 2834 return false; 2835 } 2836 if (this._getPointerInputDevice(evt) !== 'touch') { 2837 if (this.mode === this.BOARD_MODE_NONE) { 2838 this.mouseOriginMoveStart(evt); 2839 } 2840 } else { 2841 this._pointerStorePosition(evt); 2842 evt.touches = this._board_touches; 2843 2844 // Touch events on empty areas of the board are handled here, see also touchStartListener 2845 // 1. case: one finger. If allowed, this triggers pan with one finger 2846 if ( 2847 evt.touches.length === 1 && 2848 this.mode === this.BOARD_MODE_NONE && 2849 this.touchStartMoveOriginOneFinger(evt) 2850 ) { 2851 // Empty by purpose 2852 } else if ( 2853 evt.touches.length === 2 && 2854 (this.mode === this.BOARD_MODE_NONE || 2855 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2856 ) { 2857 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2858 // This happens when the second finger hits the device. First, the 2859 // 'one finger pan mode' has to be cancelled. 2860 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2861 this.originMoveEnd(); 2862 } 2863 2864 this.gestureStartListener(evt); 2865 } 2866 } 2867 2868 // Allow browser scrolling 2869 // For this: pan by one finger has to be disabled 2870 ta = 'none'; // JSXGraph catches all user touch events 2871 if (this.mode === this.BOARD_MODE_NONE && 2872 Type.evaluate(this.attr.browserpan) && 2873 !(Type.evaluate(this.attr.pan.enabled) && !Type.evaluate(this.attr.pan.needtwofingers)) 2874 ) { 2875 ta = 'pan-x pan-y'; // JSXGraph allows browser scrolling 2876 } 2877 this.containerObj.style.touchAction = ta; 2878 2879 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 2880 2881 return true; 2882 }, 2883 2884 /** 2885 * Internal handling of click events for pointers and mouse. 2886 * 2887 * @param {Event} evt The browsers event object. 2888 * @param {Array} evtArray list of event names 2889 * @private 2890 */ 2891 _handleClicks: function(evt, evtArray) { 2892 var that = this, 2893 el, delay, suppress; 2894 2895 if (this.selectingMode) { 2896 evt.stopPropagation(); 2897 return; 2898 } 2899 2900 delay = Type.evaluate(this.attr.clickdelay); 2901 suppress = Type.evaluate(this.attr.dblclicksuppressclick); 2902 2903 if (suppress) { 2904 // dblclick suppresses previous click events 2905 this._preventSingleClick = false; 2906 2907 // Wait if there is a dblclick event. 2908 // If not fire a click event 2909 this._singleClickTimer = setTimeout(function() { 2910 if (!that._preventSingleClick) { 2911 // Fire click event and remove element from click list 2912 that.triggerEventHandlers(evtArray, [evt]); 2913 for (el in that.clickObjects) { 2914 if (that.clickObjects.hasOwnProperty(el)) { 2915 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2916 delete that.clickObjects[el]; 2917 } 2918 } 2919 } 2920 }, delay); 2921 } else { 2922 // dblclick is preceded by two click events 2923 2924 // Fire click events 2925 that.triggerEventHandlers(evtArray, [evt]); 2926 for (el in that.clickObjects) { 2927 if (that.clickObjects.hasOwnProperty(el)) { 2928 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2929 } 2930 } 2931 2932 // Clear list of clicked elements with a delay 2933 setTimeout(function() { 2934 for (el in that.clickObjects) { 2935 if (that.clickObjects.hasOwnProperty(el)) { 2936 delete that.clickObjects[el]; 2937 } 2938 } 2939 }, delay); 2940 } 2941 evt.stopPropagation(); 2942 }, 2943 2944 /** 2945 * Internal handling of dblclick events for pointers and mouse. 2946 * 2947 * @param {Event} evt The browsers event object. 2948 * @param {Array} evtArray list of event names 2949 * @private 2950 */ 2951 _handleDblClicks: function(evt, evtArray) { 2952 var el; 2953 2954 if (this.selectingMode) { 2955 evt.stopPropagation(); 2956 return; 2957 } 2958 2959 // Notify that a dblclick has happened 2960 this._preventSingleClick = true; 2961 clearTimeout(this._singleClickTimer); 2962 2963 // Fire dblclick event 2964 this.triggerEventHandlers(evtArray, [evt]); 2965 for (el in this.clickObjects) { 2966 if (this.clickObjects.hasOwnProperty(el)) { 2967 this.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2968 delete this.clickObjects[el]; 2969 } 2970 } 2971 2972 evt.stopPropagation(); 2973 }, 2974 2975 /** 2976 * This method is called by the browser when a pointer device clicks on the screen. 2977 * @param {Event} evt The browsers event object. 2978 */ 2979 pointerClickListener: function (evt) { 2980 this._handleClicks(evt, ['click', 'pointerclick']); 2981 }, 2982 2983 /** 2984 * This method is called by the browser when a pointer device double clicks on the screen. 2985 * @param {Event} evt The browsers event object. 2986 */ 2987 pointerDblClickListener: function (evt) { 2988 this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']); 2989 }, 2990 2991 /** 2992 * This method is called by the browser when the mouse device clicks on the screen. 2993 * @param {Event} evt The browsers event object. 2994 */ 2995 mouseClickListener: function (evt) { 2996 this._handleClicks(evt, ['click', 'mouseclick']); 2997 }, 2998 2999 /** 3000 * This method is called by the browser when the mouse device double clicks on the screen. 3001 * @param {Event} evt The browsers event object. 3002 */ 3003 mouseDblClickListener: function (evt) { 3004 this._handleDblClicks(evt, ['dblclick', 'mousedblclick']); 3005 }, 3006 3007 // /** 3008 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 3009 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 3010 // * to the border of the board, this pointerout event will be ignored. 3011 // * @param {Event} evt 3012 // * @return {Boolean} 3013 // */ 3014 // pointerOutListener: function (evt) { 3015 // if (evt.target === this.containerObj || 3016 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 3017 // this.pointerUpListener(evt); 3018 // } 3019 // return this.mode === this.BOARD_MODE_NONE; 3020 // }, 3021 3022 /** 3023 * Called periodically by the browser while the user moves a pointing device across the screen. 3024 * @param {Event} evt 3025 * @returns {Boolean} 3026 */ 3027 pointerMoveListener: function (evt) { 3028 var i, j, pos, eps, 3029 touchTargets, 3030 type = 'mouse'; // in case of no browser 3031 3032 if ( 3033 this._getPointerInputDevice(evt) === 'touch' && 3034 !this._isPointerRegistered(evt) 3035 ) { 3036 // Test, if there was a previous down event of this _getPointerId 3037 // (in case it is a touch event). 3038 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 3039 return this.BOARD_MODE_NONE; 3040 } 3041 3042 if (!this.checkFrameRate(evt)) { 3043 return false; 3044 } 3045 3046 if (this.mode !== this.BOARD_MODE_DRAG) { 3047 this.dehighlightAll(); 3048 this.displayInfobox(false); 3049 } 3050 3051 if (this.mode !== this.BOARD_MODE_NONE) { 3052 evt.preventDefault(); 3053 evt.stopPropagation(); 3054 } 3055 3056 this.updateQuality = this.BOARD_QUALITY_LOW; 3057 // Mouse, touch or pen device 3058 this._inputDevice = this._getPointerInputDevice(evt); 3059 type = this._inputDevice; 3060 this.options.precision.hasPoint = this.options.precision[type]; 3061 eps = this.options.precision.hasPoint * 0.3333; 3062 3063 pos = this.getMousePosition(evt); 3064 // Ignore pointer move event if too close at the border 3065 // and setPointerCapture is off 3066 if (Type.evaluate(this.attr.movetarget) === null && 3067 pos[0] <= eps || pos[1] <= eps || 3068 pos[0] >= this.canvasWidth - eps || 3069 pos[1] >= this.canvasHeight - eps 3070 ) { 3071 return this.mode === this.BOARD_MODE_NONE; 3072 } 3073 3074 // selection 3075 if (this.selectingMode) { 3076 this._moveSelecting(pos); 3077 this.triggerEventHandlers( 3078 ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], 3079 [evt, this.mode] 3080 ); 3081 } else if (!this.mouseOriginMove(evt)) { 3082 if (this.mode === this.BOARD_MODE_DRAG) { 3083 // Run through all jsxgraph elements which are touched by at least one finger. 3084 for (i = 0; i < this.touches.length; i++) { 3085 touchTargets = this.touches[i].targets; 3086 // Run through all touch events which have been started on this jsxgraph element. 3087 for (j = 0; j < touchTargets.length; j++) { 3088 if (touchTargets[j].num === evt.pointerId) { 3089 touchTargets[j].X = pos[0]; 3090 touchTargets[j].Y = pos[1]; 3091 3092 if (touchTargets.length === 1) { 3093 // Touch by one finger: this is possible for all elements that can be dragged 3094 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 3095 } else if (touchTargets.length === 2) { 3096 // Touch by two fingers: e.g. moving lines 3097 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 3098 3099 touchTargets[j].Xprev = pos[0]; 3100 touchTargets[j].Yprev = pos[1]; 3101 } 3102 3103 // There is only one pointer in the evt object, so there's no point in looking further 3104 break; 3105 } 3106 } 3107 } 3108 } else { 3109 if (this._getPointerInputDevice(evt) === 'touch') { 3110 this._pointerStorePosition(evt); 3111 3112 if (this._board_touches.length === 2) { 3113 evt.touches = this._board_touches; 3114 this.gestureChangeListener(evt); 3115 } 3116 } 3117 3118 // Move event without dragging an element 3119 this.highlightElements(pos[0], pos[1], evt, -1); 3120 } 3121 } 3122 3123 // Hiding the infobox is commented out, since it prevents showing the infobox 3124 // on IE 11+ on 'over' 3125 //if (this.mode !== this.BOARD_MODE_DRAG) { 3126 //this.displayInfobox(false); 3127 //} 3128 this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]); 3129 this.updateQuality = this.BOARD_QUALITY_HIGH; 3130 3131 return this.mode === this.BOARD_MODE_NONE; 3132 }, 3133 3134 /** 3135 * Triggered as soon as the user stops touching the device with at least one finger. 3136 * 3137 * @param {Event} evt 3138 * @returns {Boolean} 3139 */ 3140 pointerUpListener: function (evt) { 3141 var i, j, found, eh, 3142 touchTargets, 3143 updateNeeded = false; 3144 3145 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 3146 this.displayInfobox(false); 3147 3148 if (evt) { 3149 for (i = 0; i < this.touches.length; i++) { 3150 touchTargets = this.touches[i].targets; 3151 for (j = 0; j < touchTargets.length; j++) { 3152 if (touchTargets[j].num === evt.pointerId) { 3153 touchTargets.splice(j, 1); 3154 if (touchTargets.length === 0) { 3155 this.touches.splice(i, 1); 3156 } 3157 break; 3158 } 3159 } 3160 } 3161 } 3162 3163 this.originMoveEnd(); 3164 this.update(); 3165 3166 // selection 3167 if (this.selectingMode) { 3168 this._stopSelecting(evt); 3169 this.triggerEventHandlers( 3170 ['touchstopselecting', 'pointerstopselecting', 'stopselecting'], 3171 [evt] 3172 ); 3173 this.stopSelectionMode(); 3174 } else { 3175 for (i = this.downObjects.length - 1; i > -1; i--) { 3176 found = false; 3177 for (j = 0; j < this.touches.length; j++) { 3178 if (this.touches[j].obj.id === this.downObjects[i].id) { 3179 found = true; 3180 } 3181 } 3182 if (!found) { 3183 this.downObjects[i].triggerEventHandlers( 3184 ['touchend', 'up', 'pointerup', 'MSPointerUp'], 3185 [evt] 3186 ); 3187 if (!Type.exists(this.downObjects[i].coords)) { 3188 // snapTo methods have to be called e.g. for line elements here. 3189 // For coordsElements there might be a conflict with 3190 // attractors, see commit from 2022.04.08, 11:12:18. 3191 this.downObjects[i].snapToGrid(); 3192 this.downObjects[i].snapToPoints(); 3193 updateNeeded = true; 3194 } 3195 3196 // Check if we have to keep the element for a click or dblclick event 3197 // Otherwise remove it from downObjects 3198 eh = this.downObjects[i].eventHandlers; 3199 if ((Type.exists(eh.click) && eh.click.length > 0) || 3200 (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) || 3201 (Type.exists(eh.dblclick) && eh.dblclick.length > 0) || 3202 (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0) 3203 ) { 3204 this.clickObjects[this.downObjects[i].id] = this.downObjects[i]; 3205 } 3206 this.downObjects.splice(i, 1); 3207 } 3208 } 3209 } 3210 3211 if (this.hasPointerUp) { 3212 if (window.navigator.msPointerEnabled) { 3213 // IE10- 3214 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 3215 } else { 3216 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 3217 Env.removeEvent( 3218 this.document, 3219 'pointercancel', 3220 this.pointerUpListener, 3221 this 3222 ); 3223 } 3224 this.hasPointerUp = false; 3225 } 3226 3227 // After one finger leaves the screen the gesture is stopped. 3228 this._pointerClearTouches(evt.pointerId); 3229 if (this._getPointerInputDevice(evt) !== 'touch') { 3230 this.dehighlightAll(); 3231 } 3232 3233 if (updateNeeded) { 3234 this.update(); 3235 } 3236 3237 return true; 3238 }, 3239 3240 /** 3241 * Triggered by the pointerleave event. This is needed in addition to 3242 * {@link JXG.Board#pointerUpListener} in the situation that a pen is used 3243 * and after an up event the pen leaves the hover range vertically. Here, it happens that 3244 * after the pointerup event further pointermove events are fired and elements get highlighted. 3245 * This highlighting has to be cancelled. 3246 * 3247 * @param {Event} evt 3248 * @returns {Boolean} 3249 */ 3250 pointerLeaveListener: function (evt) { 3251 this.displayInfobox(false); 3252 this.dehighlightAll(); 3253 3254 return true; 3255 }, 3256 3257 /** 3258 * Touch-Events 3259 */ 3260 3261 /** 3262 * This method is called by the browser when a finger touches the surface of the touch-device. 3263 * @param {Event} evt The browsers event object. 3264 * @returns {Boolean} ... 3265 */ 3266 touchStartListener: function (evt) { 3267 var i, 3268 pos, 3269 elements, 3270 j, 3271 k, 3272 eps = this.options.precision.touch, 3273 obj, 3274 found, 3275 targets, 3276 evtTouches = evt['touches'], 3277 target, 3278 touchTargets; 3279 3280 if (!this.hasTouchEnd) { 3281 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 3282 this.hasTouchEnd = true; 3283 } 3284 3285 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 3286 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 3287 3288 // prevent accidental selection of text 3289 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3290 this.document.selection.empty(); 3291 } else if (window.getSelection) { 3292 window.getSelection().removeAllRanges(); 3293 } 3294 3295 // multitouch 3296 this._inputDevice = 'touch'; 3297 this.options.precision.hasPoint = this.options.precision.touch; 3298 3299 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 3300 // 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 3301 // 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 3302 // 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 3303 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 3304 // * points have higher priority over other elements. 3305 // * 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 3306 // this element and add them. 3307 // ADDENDUM 11/10/11: 3308 // (1) run through the touches control object, 3309 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 3310 // for every target in our touches objects 3311 // (3) if one of the targettouches was bound to a touches targets array, mark it 3312 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 3313 // (a) if no element could be found: mark the target touches and continue 3314 // --- in the following cases, 'init' means: 3315 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 3316 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 3317 // (b) if the element is a point, init 3318 // (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 3319 // (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 3320 // add both to the touches array and mark them. 3321 for (i = 0; i < evtTouches.length; i++) { 3322 evtTouches[i].jxg_isused = false; 3323 } 3324 3325 for (i = 0; i < this.touches.length; i++) { 3326 touchTargets = this.touches[i].targets; 3327 for (j = 0; j < touchTargets.length; j++) { 3328 touchTargets[j].num = -1; 3329 eps = this.options.precision.touch; 3330 3331 do { 3332 for (k = 0; k < evtTouches.length; k++) { 3333 // find the new targettouches 3334 if ( 3335 Math.abs( 3336 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3337 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3338 ) < 3339 eps * eps 3340 ) { 3341 touchTargets[j].num = k; 3342 touchTargets[j].X = evtTouches[k].screenX; 3343 touchTargets[j].Y = evtTouches[k].screenY; 3344 evtTouches[k].jxg_isused = true; 3345 break; 3346 } 3347 } 3348 3349 eps *= 2; 3350 } while ( 3351 touchTargets[j].num === -1 && 3352 eps < this.options.precision.touchMax 3353 ); 3354 3355 if (touchTargets[j].num === -1) { 3356 JXG.debug( 3357 "i couldn't find a targettouches for target no " + 3358 j + 3359 ' on ' + 3360 this.touches[i].obj.name + 3361 ' (' + 3362 this.touches[i].obj.id + 3363 '). Removed the target.' 3364 ); 3365 JXG.debug( 3366 'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax 3367 ); 3368 touchTargets.splice(i, 1); 3369 } 3370 } 3371 } 3372 3373 // we just re-mapped the targettouches to our existing touches list. 3374 // now we have to initialize some touches from additional targettouches 3375 for (i = 0; i < evtTouches.length; i++) { 3376 if (!evtTouches[i].jxg_isused) { 3377 pos = this.getMousePosition(evt, i); 3378 // selection 3379 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 3380 if (this.selectingMode) { 3381 this._startSelecting(pos); 3382 this.triggerEventHandlers( 3383 ['touchstartselecting', 'startselecting'], 3384 [evt] 3385 ); 3386 evt.preventDefault(); 3387 evt.stopPropagation(); 3388 this.options.precision.hasPoint = this.options.precision.mouse; 3389 return this.touches.length > 0; // don't continue as a normal click 3390 } 3391 3392 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 3393 if (elements.length !== 0) { 3394 obj = elements[elements.length - 1]; 3395 target = { 3396 num: i, 3397 X: evtTouches[i].screenX, 3398 Y: evtTouches[i].screenY, 3399 Xprev: NaN, 3400 Yprev: NaN, 3401 Xstart: [], 3402 Ystart: [], 3403 Zstart: [] 3404 }; 3405 3406 if ( 3407 Type.isPoint(obj) || 3408 obj.elementClass === Const.OBJECT_CLASS_TEXT || 3409 obj.type === Const.OBJECT_TYPE_TICKS || 3410 obj.type === Const.OBJECT_TYPE_IMAGE 3411 ) { 3412 // It's a point, so it's single touch, so we just push it to our touches 3413 targets = [target]; 3414 3415 // For the UNDO/REDO of object moves 3416 this.saveStartPos(obj, targets[0]); 3417 3418 this.touches.push({ obj: obj, targets: targets }); 3419 obj.highlight(true); 3420 } else if ( 3421 obj.elementClass === Const.OBJECT_CLASS_LINE || 3422 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 3423 obj.elementClass === Const.OBJECT_CLASS_CURVE || 3424 obj.type === Const.OBJECT_TYPE_POLYGON 3425 ) { 3426 found = false; 3427 3428 // first check if this geometric object is already captured in this.touches 3429 for (j = 0; j < this.touches.length; j++) { 3430 if (obj.id === this.touches[j].obj.id) { 3431 found = true; 3432 // only add it, if we don't have two targets in there already 3433 if (this.touches[j].targets.length === 1) { 3434 // For the UNDO/REDO of object moves 3435 this.saveStartPos(obj, target); 3436 this.touches[j].targets.push(target); 3437 } 3438 3439 evtTouches[i].jxg_isused = true; 3440 } 3441 } 3442 3443 // we couldn't find it in touches, so we just init a new touches 3444 // IF there is a second touch targetting this line, we will find it later on, and then add it to 3445 // the touches control object. 3446 if (!found) { 3447 targets = [target]; 3448 3449 // For the UNDO/REDO of object moves 3450 this.saveStartPos(obj, targets[0]); 3451 this.touches.push({ obj: obj, targets: targets }); 3452 obj.highlight(true); 3453 } 3454 } 3455 } 3456 3457 evtTouches[i].jxg_isused = true; 3458 } 3459 } 3460 3461 if (this.touches.length > 0) { 3462 evt.preventDefault(); 3463 evt.stopPropagation(); 3464 } 3465 3466 // Touch events on empty areas of the board are handled here: 3467 // 1. case: one finger. If allowed, this triggers pan with one finger 3468 if ( 3469 evtTouches.length === 1 && 3470 this.mode === this.BOARD_MODE_NONE && 3471 this.touchStartMoveOriginOneFinger(evt) 3472 ) { 3473 } else if ( 3474 evtTouches.length === 2 && 3475 (this.mode === this.BOARD_MODE_NONE || 3476 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 3477 ) { 3478 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 3479 // This happens when the second finger hits the device. First, the 3480 // 'one finger pan mode' has to be cancelled. 3481 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 3482 this.originMoveEnd(); 3483 } 3484 this.gestureStartListener(evt); 3485 } 3486 3487 this.options.precision.hasPoint = this.options.precision.mouse; 3488 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 3489 3490 return false; 3491 //return this.touches.length > 0; 3492 }, 3493 3494 /** 3495 * Called periodically by the browser while the user moves his fingers across the device. 3496 * @param {Event} evt 3497 * @returns {Boolean} 3498 */ 3499 touchMoveListener: function (evt) { 3500 var i, 3501 pos1, 3502 pos2, 3503 touchTargets, 3504 evtTouches = evt['touches']; 3505 3506 if (!this.checkFrameRate(evt)) { 3507 return false; 3508 } 3509 3510 if (this.mode !== this.BOARD_MODE_NONE) { 3511 evt.preventDefault(); 3512 evt.stopPropagation(); 3513 } 3514 3515 if (this.mode !== this.BOARD_MODE_DRAG) { 3516 this.dehighlightAll(); 3517 this.displayInfobox(false); 3518 } 3519 3520 this._inputDevice = 'touch'; 3521 this.options.precision.hasPoint = this.options.precision.touch; 3522 this.updateQuality = this.BOARD_QUALITY_LOW; 3523 3524 // selection 3525 if (this.selectingMode) { 3526 for (i = 0; i < evtTouches.length; i++) { 3527 if (!evtTouches[i].jxg_isused) { 3528 pos1 = this.getMousePosition(evt, i); 3529 this._moveSelecting(pos1); 3530 this.triggerEventHandlers( 3531 ['touchmoves', 'moveselecting'], 3532 [evt, this.mode] 3533 ); 3534 break; 3535 } 3536 } 3537 } else { 3538 if (!this.touchOriginMove(evt)) { 3539 if (this.mode === this.BOARD_MODE_DRAG) { 3540 // Runs over through all elements which are touched 3541 // by at least one finger. 3542 for (i = 0; i < this.touches.length; i++) { 3543 touchTargets = this.touches[i].targets; 3544 if (touchTargets.length === 1) { 3545 // Touch by one finger: this is possible for all elements that can be dragged 3546 if (evtTouches[touchTargets[0].num]) { 3547 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3548 if ( 3549 pos1[0] < 0 || 3550 pos1[0] > this.canvasWidth || 3551 pos1[1] < 0 || 3552 pos1[1] > this.canvasHeight 3553 ) { 3554 return; 3555 } 3556 touchTargets[0].X = pos1[0]; 3557 touchTargets[0].Y = pos1[1]; 3558 this.moveObject( 3559 pos1[0], 3560 pos1[1], 3561 this.touches[i], 3562 evt, 3563 'touch' 3564 ); 3565 } 3566 } else if ( 3567 touchTargets.length === 2 && 3568 touchTargets[0].num > -1 && 3569 touchTargets[1].num > -1 3570 ) { 3571 // Touch by two fingers: moving lines, ... 3572 if ( 3573 evtTouches[touchTargets[0].num] && 3574 evtTouches[touchTargets[1].num] 3575 ) { 3576 // Get coordinates of the two touches 3577 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3578 pos2 = this.getMousePosition(evt, touchTargets[1].num); 3579 if ( 3580 pos1[0] < 0 || 3581 pos1[0] > this.canvasWidth || 3582 pos1[1] < 0 || 3583 pos1[1] > this.canvasHeight || 3584 pos2[0] < 0 || 3585 pos2[0] > this.canvasWidth || 3586 pos2[1] < 0 || 3587 pos2[1] > this.canvasHeight 3588 ) { 3589 return; 3590 } 3591 3592 touchTargets[0].X = pos1[0]; 3593 touchTargets[0].Y = pos1[1]; 3594 touchTargets[1].X = pos2[0]; 3595 touchTargets[1].Y = pos2[1]; 3596 3597 this.twoFingerMove( 3598 this.touches[i], 3599 touchTargets[0].num, 3600 evt 3601 ); 3602 3603 touchTargets[0].Xprev = pos1[0]; 3604 touchTargets[0].Yprev = pos1[1]; 3605 touchTargets[1].Xprev = pos2[0]; 3606 touchTargets[1].Yprev = pos2[1]; 3607 } 3608 } 3609 } 3610 } else { 3611 if (evtTouches.length === 2) { 3612 this.gestureChangeListener(evt); 3613 } 3614 // Move event without dragging an element 3615 pos1 = this.getMousePosition(evt, 0); 3616 this.highlightElements(pos1[0], pos1[1], evt, -1); 3617 } 3618 } 3619 } 3620 3621 if (this.mode !== this.BOARD_MODE_DRAG) { 3622 this.displayInfobox(false); 3623 } 3624 3625 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 3626 this.options.precision.hasPoint = this.options.precision.mouse; 3627 this.updateQuality = this.BOARD_QUALITY_HIGH; 3628 3629 return this.mode === this.BOARD_MODE_NONE; 3630 }, 3631 3632 /** 3633 * Triggered as soon as the user stops touching the device with at least one finger. 3634 * @param {Event} evt 3635 * @returns {Boolean} 3636 */ 3637 touchEndListener: function (evt) { 3638 var i, 3639 j, 3640 k, 3641 eps = this.options.precision.touch, 3642 tmpTouches = [], 3643 found, 3644 foundNumber, 3645 evtTouches = evt && evt['touches'], 3646 touchTargets, 3647 updateNeeded = false; 3648 3649 this.triggerEventHandlers(['touchend', 'up'], [evt]); 3650 this.displayInfobox(false); 3651 3652 // selection 3653 if (this.selectingMode) { 3654 this._stopSelecting(evt); 3655 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 3656 this.stopSelectionMode(); 3657 } else if (evtTouches && evtTouches.length > 0) { 3658 for (i = 0; i < this.touches.length; i++) { 3659 tmpTouches[i] = this.touches[i]; 3660 } 3661 this.touches.length = 0; 3662 3663 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 3664 // convert the operation to a simple one-finger-translation. 3665 // ADDENDUM 11/10/11: 3666 // see addendum to touchStartListener from 11/10/11 3667 // (1) run through the tmptouches 3668 // (2) check the touches.obj, if it is a 3669 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 3670 // (b) line with 3671 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 3672 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 3673 // (c) circle with [proceed like in line] 3674 3675 // init the targettouches marker 3676 for (i = 0; i < evtTouches.length; i++) { 3677 evtTouches[i].jxg_isused = false; 3678 } 3679 3680 for (i = 0; i < tmpTouches.length; i++) { 3681 // could all targets of the current this.touches.obj be assigned to targettouches? 3682 found = false; 3683 foundNumber = 0; 3684 touchTargets = tmpTouches[i].targets; 3685 3686 for (j = 0; j < touchTargets.length; j++) { 3687 touchTargets[j].found = false; 3688 for (k = 0; k < evtTouches.length; k++) { 3689 if ( 3690 Math.abs( 3691 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3692 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3693 ) < 3694 eps * eps 3695 ) { 3696 touchTargets[j].found = true; 3697 touchTargets[j].num = k; 3698 touchTargets[j].X = evtTouches[k].screenX; 3699 touchTargets[j].Y = evtTouches[k].screenY; 3700 foundNumber += 1; 3701 break; 3702 } 3703 } 3704 } 3705 3706 if (Type.isPoint(tmpTouches[i].obj)) { 3707 found = touchTargets[0] && touchTargets[0].found; 3708 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 3709 found = 3710 (touchTargets[0] && touchTargets[0].found) || 3711 (touchTargets[1] && touchTargets[1].found); 3712 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 3713 found = foundNumber === 1 || foundNumber === 3; 3714 } 3715 3716 // if we found this object to be still dragged by the user, add it back to this.touches 3717 if (found) { 3718 this.touches.push({ 3719 obj: tmpTouches[i].obj, 3720 targets: [] 3721 }); 3722 3723 for (j = 0; j < touchTargets.length; j++) { 3724 if (touchTargets[j].found) { 3725 this.touches[this.touches.length - 1].targets.push({ 3726 num: touchTargets[j].num, 3727 X: touchTargets[j].screenX, 3728 Y: touchTargets[j].screenY, 3729 Xprev: NaN, 3730 Yprev: NaN, 3731 Xstart: touchTargets[j].Xstart, 3732 Ystart: touchTargets[j].Ystart, 3733 Zstart: touchTargets[j].Zstart 3734 }); 3735 } 3736 } 3737 } else { 3738 tmpTouches[i].obj.noHighlight(); 3739 } 3740 } 3741 } else { 3742 this.touches.length = 0; 3743 } 3744 3745 for (i = this.downObjects.length - 1; i > -1; i--) { 3746 found = false; 3747 for (j = 0; j < this.touches.length; j++) { 3748 if (this.touches[j].obj.id === this.downObjects[i].id) { 3749 found = true; 3750 } 3751 } 3752 if (!found) { 3753 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 3754 if (!Type.exists(this.downObjects[i].coords)) { 3755 // snapTo methods have to be called e.g. for line elements here. 3756 // For coordsElements there might be a conflict with 3757 // attractors, see commit from 2022.04.08, 11:12:18. 3758 this.downObjects[i].snapToGrid(); 3759 this.downObjects[i].snapToPoints(); 3760 updateNeeded = true; 3761 } 3762 this.downObjects.splice(i, 1); 3763 } 3764 } 3765 3766 if (!evtTouches || evtTouches.length === 0) { 3767 if (this.hasTouchEnd) { 3768 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 3769 this.hasTouchEnd = false; 3770 } 3771 3772 this.dehighlightAll(); 3773 this.updateQuality = this.BOARD_QUALITY_HIGH; 3774 3775 this.originMoveEnd(); 3776 if (updateNeeded) { 3777 this.update(); 3778 } 3779 } 3780 3781 return true; 3782 }, 3783 3784 /** 3785 * This method is called by the browser when the mouse button is clicked. 3786 * @param {Event} evt The browsers event object. 3787 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 3788 */ 3789 mouseDownListener: function (evt) { 3790 var pos, elements, result; 3791 3792 // prevent accidental selection of text 3793 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3794 this.document.selection.empty(); 3795 } else if (window.getSelection) { 3796 window.getSelection().removeAllRanges(); 3797 } 3798 3799 if (!this.hasMouseUp) { 3800 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 3801 this.hasMouseUp = true; 3802 } else { 3803 // In case this.hasMouseUp==true, it may be that there was a 3804 // mousedown event before which was not followed by an mouseup event. 3805 // This seems to happen with interactive whiteboard pens sometimes. 3806 return; 3807 } 3808 3809 this._inputDevice = 'mouse'; 3810 this.options.precision.hasPoint = this.options.precision.mouse; 3811 pos = this.getMousePosition(evt); 3812 3813 // selection 3814 this._testForSelection(evt); 3815 if (this.selectingMode) { 3816 this._startSelecting(pos); 3817 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 3818 return; // don't continue as a normal click 3819 } 3820 3821 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 3822 3823 // if no draggable object can be found, get out here immediately 3824 if (elements.length === 0) { 3825 this.mode = this.BOARD_MODE_NONE; 3826 result = true; 3827 } else { 3828 this.mouse = { 3829 obj: null, 3830 targets: [ 3831 { 3832 X: pos[0], 3833 Y: pos[1], 3834 Xprev: NaN, 3835 Yprev: NaN 3836 } 3837 ] 3838 }; 3839 this.mouse.obj = elements[elements.length - 1]; 3840 3841 this.dehighlightAll(); 3842 this.mouse.obj.highlight(true); 3843 3844 this.mouse.targets[0].Xstart = []; 3845 this.mouse.targets[0].Ystart = []; 3846 this.mouse.targets[0].Zstart = []; 3847 3848 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 3849 3850 // prevent accidental text selection 3851 // this could get us new trouble: input fields, links and drop down boxes placed as text 3852 // on the board don't work anymore. 3853 if (evt && evt.preventDefault) { 3854 evt.preventDefault(); 3855 } else if (window.event) { 3856 window.event.returnValue = false; 3857 } 3858 } 3859 3860 if (this.mode === this.BOARD_MODE_NONE) { 3861 result = this.mouseOriginMoveStart(evt); 3862 } 3863 3864 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 3865 3866 return result; 3867 }, 3868 3869 /** 3870 * This method is called by the browser when the mouse is moved. 3871 * @param {Event} evt The browsers event object. 3872 */ 3873 mouseMoveListener: function (evt) { 3874 var pos; 3875 3876 if (!this.checkFrameRate(evt)) { 3877 return false; 3878 } 3879 3880 pos = this.getMousePosition(evt); 3881 3882 this.updateQuality = this.BOARD_QUALITY_LOW; 3883 3884 if (this.mode !== this.BOARD_MODE_DRAG) { 3885 this.dehighlightAll(); 3886 this.displayInfobox(false); 3887 } 3888 3889 // we have to check for four cases: 3890 // * user moves origin 3891 // * user drags an object 3892 // * user just moves the mouse, here highlight all elements at 3893 // the current mouse position 3894 // * the user is selecting 3895 3896 // selection 3897 if (this.selectingMode) { 3898 this._moveSelecting(pos); 3899 this.triggerEventHandlers( 3900 ['mousemoveselecting', 'moveselecting'], 3901 [evt, this.mode] 3902 ); 3903 } else if (!this.mouseOriginMove(evt)) { 3904 if (this.mode === this.BOARD_MODE_DRAG) { 3905 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 3906 } else { 3907 // BOARD_MODE_NONE 3908 // Move event without dragging an element 3909 this.highlightElements(pos[0], pos[1], evt, -1); 3910 } 3911 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 3912 } 3913 this.updateQuality = this.BOARD_QUALITY_HIGH; 3914 }, 3915 3916 /** 3917 * This method is called by the browser when the mouse button is released. 3918 * @param {Event} evt 3919 */ 3920 mouseUpListener: function (evt) { 3921 var i; 3922 3923 if (this.selectingMode === false) { 3924 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 3925 } 3926 3927 // redraw with high precision 3928 this.updateQuality = this.BOARD_QUALITY_HIGH; 3929 3930 if (this.mouse && this.mouse.obj) { 3931 if (!Type.exists(this.mouse.obj.coords)) { 3932 // snapTo methods have to be called e.g. for line elements here. 3933 // For coordsElements there might be a conflict with 3934 // attractors, see commit from 2022.04.08, 11:12:18. 3935 // The parameter is needed for lines with snapToGrid enabled 3936 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 3937 this.mouse.obj.snapToPoints(); 3938 } 3939 } 3940 3941 this.originMoveEnd(); 3942 this.dehighlightAll(); 3943 this.update(); 3944 3945 // selection 3946 if (this.selectingMode) { 3947 this._stopSelecting(evt); 3948 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 3949 this.stopSelectionMode(); 3950 } else { 3951 for (i = 0; i < this.downObjects.length; i++) { 3952 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 3953 } 3954 } 3955 3956 this.downObjects.length = 0; 3957 3958 if (this.hasMouseUp) { 3959 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 3960 this.hasMouseUp = false; 3961 } 3962 3963 // release dragged mouse object 3964 this.mouse = null; 3965 }, 3966 3967 /** 3968 * Handler for mouse wheel events. Used to zoom in and out of the board. 3969 * @param {Event} evt 3970 * @returns {Boolean} 3971 */ 3972 mouseWheelListener: function (evt) { 3973 var wd, zoomCenter, pos; 3974 3975 if (!this.attr.zoom.enabled || 3976 !this.attr.zoom.wheel || 3977 !this._isRequiredKeyPressed(evt, 'zoom')) { 3978 3979 return true; 3980 } 3981 3982 evt = evt || window.event; 3983 wd = evt.detail ? -evt.detail : evt.wheelDelta / 40; 3984 zoomCenter = this.attr.zoom.center; 3985 3986 if (zoomCenter === 'board') { 3987 pos = []; 3988 } else { // including zoomCenter === 'auto' 3989 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords; 3990 } 3991 3992 // pos == [] does not throw an error 3993 if (wd > 0) { 3994 this.zoomIn(pos[1], pos[2]); 3995 } else { 3996 this.zoomOut(pos[1], pos[2]); 3997 } 3998 3999 this.triggerEventHandlers(['mousewheel'], [evt]); 4000 4001 evt.preventDefault(); 4002 return false; 4003 }, 4004 4005 /** 4006 * Allow moving of JSXGraph elements with arrow keys. 4007 * The selection of the element is done with the tab key. For this, 4008 * the attribute 'tabindex' of the element has to be set to some number (default=0). 4009 * tabindex corresponds to the HTML attribute of the same name. 4010 * <p> 4011 * Panning of the construction is done with arrow keys 4012 * if the pan key (shift or ctrl - depending on the board attributes) is pressed. 4013 * <p> 4014 * Zooming is triggered with the keys +, o, -, if 4015 * the pan key (shift or ctrl - depending on the board attributes) is pressed. 4016 * <p> 4017 * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus. 4018 * 4019 * @param {Event} evt The browser's event object 4020 * 4021 * @see JXG.Board#keyboard 4022 * @see JXG.Board#keyFocusInListener 4023 * @see JXG.Board#keyFocusOutListener 4024 * 4025 */ 4026 keyDownListener: function (evt) { 4027 var id_node = evt.target.id, 4028 id, el, res, doc, 4029 sX = 0, 4030 sY = 0, 4031 // dx, dy are provided in screen units and 4032 // are converted to user coordinates 4033 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 4034 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 4035 // u = 100, 4036 doZoom = false, 4037 done = true, 4038 dir, 4039 actPos; 4040 4041 if (!this.attr.keyboard.enabled || id_node === '') { 4042 return false; 4043 } 4044 4045 // dx = Math.round(dx * u) / u; 4046 // dy = Math.round(dy * u) / u; 4047 4048 // An element of type input or textarea has foxus, get out of here. 4049 doc = this.containerObj.shadowRoot || document; 4050 if (doc.activeElement) { 4051 el = doc.activeElement; 4052 if (el.tagName === 'INPUT' || el.tagName === 'textarea') { 4053 return false; 4054 } 4055 } 4056 4057 // Get the JSXGraph id from the id of the SVG node. 4058 id = id_node.replace(this.containerObj.id + '_', ''); 4059 el = this.select(id); 4060 4061 if (Type.exists(el.coords)) { 4062 actPos = el.coords.usrCoords.slice(1); 4063 } 4064 4065 if ( 4066 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 4067 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) 4068 ) { 4069 // Pan key has been pressed 4070 4071 if (Type.evaluate(this.attr.zoom.enabled) === true) { 4072 doZoom = true; 4073 } 4074 4075 // Arrow keys 4076 if (evt.keyCode === 38) { 4077 // up 4078 this.clickUpArrow(); 4079 } else if (evt.keyCode === 40) { 4080 // down 4081 this.clickDownArrow(); 4082 } else if (evt.keyCode === 37) { 4083 // left 4084 this.clickLeftArrow(); 4085 } else if (evt.keyCode === 39) { 4086 // right 4087 this.clickRightArrow(); 4088 4089 // Zoom keys 4090 } else if (doZoom && evt.keyCode === 171) { 4091 // + 4092 this.zoomIn(); 4093 } else if (doZoom && evt.keyCode === 173) { 4094 // - 4095 this.zoomOut(); 4096 } else if (doZoom && evt.keyCode === 79) { 4097 // o 4098 this.zoom100(); 4099 } else { 4100 done = false; 4101 } 4102 } else { 4103 // Adapt dx, dy to snapToGrid and attractToGrid. 4104 // snapToGrid has priority. 4105 if (Type.exists(el.visProp)) { 4106 if ( 4107 Type.exists(el.visProp.snaptogrid) && 4108 el.visProp.snaptogrid && 4109 Type.evaluate(el.visProp.snapsizex) && 4110 Type.evaluate(el.visProp.snapsizey) 4111 ) { 4112 // Adapt dx, dy such that snapToGrid is possible 4113 res = el.getSnapSizes(); 4114 sX = res[0]; 4115 sY = res[1]; 4116 // If snaptogrid is true, 4117 // we can only jump from grid point to grid point. 4118 dx = sX; 4119 dy = sY; 4120 } else if ( 4121 Type.exists(el.visProp.attracttogrid) && 4122 el.visProp.attracttogrid && 4123 Type.evaluate(el.visProp.attractordistance) && 4124 Type.evaluate(el.visProp.attractorunit) 4125 ) { 4126 // Adapt dx, dy such that attractToGrid is possible 4127 sX = 1.1 * Type.evaluate(el.visProp.attractordistance); 4128 sY = sX; 4129 4130 if (Type.evaluate(el.visProp.attractorunit) === 'screen') { 4131 sX /= this.unitX; 4132 sY /= this.unitX; 4133 } 4134 dx = Math.max(sX, dx); 4135 dy = Math.max(sY, dy); 4136 } 4137 } 4138 4139 if (evt.keyCode === 38) { 4140 // up 4141 dir = [0, dy]; 4142 } else if (evt.keyCode === 40) { 4143 // down 4144 dir = [0, -dy]; 4145 } else if (evt.keyCode === 37) { 4146 // left 4147 dir = [-dx, 0]; 4148 } else if (evt.keyCode === 39) { 4149 // right 4150 dir = [dx, 0]; 4151 } else { 4152 done = false; 4153 } 4154 4155 if (dir && el.isDraggable && 4156 el.visPropCalc.visible && 4157 ((this.geonextCompatibilityMode && 4158 (Type.isPoint(el) || 4159 el.elementClass === Const.OBJECT_CLASS_TEXT) 4160 ) || !this.geonextCompatibilityMode) && 4161 !Type.evaluate(el.visProp.fixed) 4162 ) { 4163 4164 4165 this.mode = this.BOARD_MODE_DRAG; 4166 if (Type.exists(el.coords)) { 4167 dir[0] += actPos[0]; 4168 dir[1] += actPos[1]; 4169 } 4170 // For coordsElement setPosition has to call setPositionDirectly. 4171 // Otherwise the position is set by a translation. 4172 if (Type.exists(el.coords)) { 4173 el.setPosition(JXG.COORDS_BY_USER, dir); 4174 this.updateInfobox(el); 4175 } else { 4176 this.displayInfobox(false); 4177 el.setPositionDirectly( 4178 Const.COORDS_BY_USER, 4179 dir, 4180 [0, 0] 4181 ); 4182 } 4183 4184 this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); 4185 el.triggerEventHandlers(['keydrag', 'drag'], [evt]); 4186 this.mode = this.BOARD_MODE_NONE; 4187 } 4188 } 4189 4190 this.update(); 4191 4192 if (done && Type.exists(evt.preventDefault)) { 4193 evt.preventDefault(); 4194 } 4195 return done; 4196 }, 4197 4198 /** 4199 * Event listener for SVG elements getting focus. 4200 * This is needed for highlighting when using keyboard control. 4201 * Only elements having the attribute 'tabindex' can receive focus. 4202 * 4203 * @see JXG.Board#keyFocusOutListener 4204 * @see JXG.Board#keyDownListener 4205 * @see JXG.Board#keyboard 4206 * 4207 * @param {Event} evt The browser's event object 4208 */ 4209 keyFocusInListener: function (evt) { 4210 var id_node = evt.target.id, 4211 id, 4212 el; 4213 4214 if (!this.attr.keyboard.enabled || id_node === '') { 4215 return false; 4216 } 4217 4218 id = id_node.replace(this.containerObj.id + '_', ''); 4219 el = this.select(id); 4220 if (Type.exists(el.highlight)) { 4221 el.highlight(true); 4222 this.focusObjects = [id]; 4223 el.triggerEventHandlers(['hit'], [evt]); 4224 } 4225 if (Type.exists(el.coords)) { 4226 this.updateInfobox(el); 4227 } 4228 }, 4229 4230 /** 4231 * Event listener for SVG elements losing focus. 4232 * This is needed for dehighlighting when using keyboard control. 4233 * Only elements having the attribute 'tabindex' can receive focus. 4234 * 4235 * @see JXG.Board#keyFocusInListener 4236 * @see JXG.Board#keyDownListener 4237 * @see JXG.Board#keyboard 4238 * 4239 * @param {Event} evt The browser's event object 4240 */ 4241 keyFocusOutListener: function (evt) { 4242 if (!this.attr.keyboard.enabled) { 4243 return false; 4244 } 4245 this.focusObjects = []; // This has to be before displayInfobox(false) 4246 this.dehighlightAll(); 4247 this.displayInfobox(false); 4248 }, 4249 4250 /** 4251 * Update the width and height of the JSXGraph container div element. 4252 * If width and height are not supplied, read actual values with offsetWidth/Height, 4253 * and call board.resizeContainer() with this values. 4254 * <p> 4255 * If necessary, also call setBoundingBox(). 4256 * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element 4257 * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element 4258 * @returns 4259 * 4260 * @see JXG.Board#startResizeObserver 4261 * @see JXG.Board#resizeListener 4262 * @see JXG.Board#resizeContainer 4263 * @see JXG.Board#setBoundingBox 4264 * 4265 */ 4266 updateContainerDims: function (width, height) { 4267 var w = width, 4268 h = height, 4269 // bb, 4270 css, 4271 width_adjustment, height_adjustment; 4272 4273 if (width === undefined) { 4274 // Get size of the board's container div 4275 // 4276 // offsetWidth/Height ignores CSS transforms, 4277 // getBoundingClientRect includes CSS transforms 4278 // 4279 // bb = this.containerObj.getBoundingClientRect(); 4280 // w = bb.width; 4281 // h = bb.height; 4282 w = this.containerObj.offsetWidth; 4283 h = this.containerObj.offsetHeight; 4284 } 4285 4286 if (width === undefined && window && window.getComputedStyle) { 4287 // Subtract the border size 4288 css = window.getComputedStyle(this.containerObj, null); 4289 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 4290 if (!isNaN(width_adjustment)) { 4291 w -= width_adjustment; 4292 } 4293 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 4294 if (!isNaN(height_adjustment)) { 4295 h -= height_adjustment; 4296 } 4297 } 4298 4299 // If div is invisible - do nothing 4300 if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { 4301 return; 4302 } 4303 4304 // If bounding box is not yet initialized, do it now. 4305 if (isNaN(this.getBoundingBox()[0])) { 4306 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 4307 } 4308 4309 // Do nothing if the dimension did not change since being visible 4310 // the last time. Note that if the div had display:none in the mean time, 4311 // we did not store this._prevDim. 4312 if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) { 4313 return; 4314 } 4315 // Set the size of the SVG or canvas element 4316 this.resizeContainer(w, h, true); 4317 this._prevDim = { 4318 w: w, 4319 h: h 4320 }; 4321 }, 4322 4323 /** 4324 * Start observer which reacts to size changes of the JSXGraph 4325 * container div element. Calls updateContainerDims(). 4326 * If not available, an event listener for the window-resize event is started. 4327 * On mobile devices also scrolling might trigger resizes. 4328 * However, resize events triggered by scrolling events should be ignored. 4329 * Therefore, also a scrollListener is started. 4330 * Resize can be controlled with the board attribute resize. 4331 * 4332 * @see JXG.Board#updateContainerDims 4333 * @see JXG.Board#resizeListener 4334 * @see JXG.Board#scrollListener 4335 * @see JXG.Board#resize 4336 * 4337 */ 4338 startResizeObserver: function () { 4339 var that = this; 4340 4341 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4342 return; 4343 } 4344 4345 this.resizeObserver = new ResizeObserver(function (entries) { 4346 var bb; 4347 if (!that._isResizing) { 4348 that._isResizing = true; 4349 bb = entries[0].contentRect; 4350 window.setTimeout(function () { 4351 try { 4352 that.updateContainerDims(bb.width, bb.height); 4353 } catch (err) { 4354 that.stopResizeObserver(); 4355 } finally { 4356 that._isResizing = false; 4357 } 4358 }, that.attr.resize.throttle); 4359 } 4360 }); 4361 this.resizeObserver.observe(this.containerObj); 4362 }, 4363 4364 /** 4365 * Stops the resize observer. 4366 * @see JXG.Board#startResizeObserver 4367 * 4368 */ 4369 stopResizeObserver: function () { 4370 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4371 return; 4372 } 4373 4374 if (Type.exists(this.resizeObserver)) { 4375 this.resizeObserver.unobserve(this.containerObj); 4376 } 4377 }, 4378 4379 /** 4380 * Fallback solutions if there is no resizeObserver available in the browser. 4381 * Reacts to resize events of the window (only). Otherwise similar to 4382 * startResizeObserver(). To handle changes of the visibility 4383 * of the JSXGraph container element, additionally an intersection observer is used. 4384 * which watches changes in the visibility of the JSXGraph container element. 4385 * This is necessary e.g. for register tabs or dia shows. 4386 * 4387 * @see JXG.Board#startResizeObserver 4388 * @see JXG.Board#startIntersectionObserver 4389 */ 4390 resizeListener: function () { 4391 var that = this; 4392 4393 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4394 return; 4395 } 4396 if (!this._isScrolling && !this._isResizing) { 4397 this._isResizing = true; 4398 window.setTimeout(function () { 4399 that.updateContainerDims(); 4400 that._isResizing = false; 4401 }, this.attr.resize.throttle); 4402 } 4403 }, 4404 4405 /** 4406 * Listener to watch for scroll events. Sets board._isScrolling = true 4407 * @param {Event} evt The browser's event object 4408 * 4409 * @see JXG.Board#startResizeObserver 4410 * @see JXG.Board#resizeListener 4411 * 4412 */ 4413 scrollListener: function (evt) { 4414 var that = this; 4415 4416 if (!Env.isBrowser) { 4417 return; 4418 } 4419 if (!this._isScrolling) { 4420 this._isScrolling = true; 4421 window.setTimeout(function () { 4422 that._isScrolling = false; 4423 }, 66); 4424 } 4425 }, 4426 4427 /** 4428 * Watch for changes of the visibility of the JSXGraph container element. 4429 * 4430 * @see JXG.Board#startResizeObserver 4431 * @see JXG.Board#resizeListener 4432 * 4433 */ 4434 startIntersectionObserver: function () { 4435 var that = this, 4436 options = { 4437 root: null, 4438 rootMargin: '0px', 4439 threshold: 0.8 4440 }; 4441 4442 try { 4443 this.intersectionObserver = new IntersectionObserver(function (entries) { 4444 // If bounding box is not yet initialized, do it now. 4445 if (isNaN(that.getBoundingBox()[0])) { 4446 that.updateContainerDims(); 4447 } 4448 }, options); 4449 this.intersectionObserver.observe(that.containerObj); 4450 } catch (err) { 4451 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.'); 4452 } 4453 }, 4454 4455 /** 4456 * Stop the intersection observer 4457 * 4458 * @see JXG.Board#startIntersectionObserver 4459 * 4460 */ 4461 stopIntersectionObserver: function () { 4462 if (Type.exists(this.intersectionObserver)) { 4463 this.intersectionObserver.unobserve(this.containerObj); 4464 } 4465 }, 4466 4467 /** 4468 * Update the container before and after printing. 4469 * @param {Event} [evt] 4470 */ 4471 printListener: function(evt) { 4472 this.updateContainerDims(); 4473 }, 4474 4475 /** 4476 * Wrapper for printListener to be used in mediaQuery matches. 4477 * @param {MediaQueryList} mql 4478 */ 4479 printListenerMatch: function (mql) { 4480 if (mql.matches) { 4481 this.printListener(); 4482 } 4483 }, 4484 4485 /********************************************************** 4486 * 4487 * End of Event Handlers 4488 * 4489 **********************************************************/ 4490 4491 /** 4492 * Initialize the info box object which is used to display 4493 * the coordinates of points near the mouse pointer, 4494 * @returns {JXG.Board} Reference to the board 4495 */ 4496 initInfobox: function (attributes) { 4497 var attr = Type.copyAttributes(attributes, this.options, 'infobox'); 4498 4499 attr.id = this.id + '_infobox'; 4500 4501 /** 4502 * Infobox close to points in which the points' coordinates are displayed. 4503 * This is simply a JXG.Text element. Access through board.infobox. 4504 * Uses CSS class .JXGinfobox. 4505 * 4506 * @namespace 4507 * @name JXG.Board.infobox 4508 * @type JXG.Text 4509 * 4510 * @example 4511 * const board = JXG.JSXGraph.initBoard(BOARDID, { 4512 * boundingbox: [-0.5, 0.5, 0.5, -0.5], 4513 * intl: { 4514 * enabled: false, 4515 * locale: 'de-DE' 4516 * }, 4517 * keepaspectratio: true, 4518 * axis: true, 4519 * infobox: { 4520 * distanceY: 40, 4521 * intl: { 4522 * enabled: true, 4523 * options: { 4524 * minimumFractionDigits: 1, 4525 * maximumFractionDigits: 2 4526 * } 4527 * } 4528 * } 4529 * }); 4530 * var p = board.create('point', [0.1, 0.1], {}); 4531 * 4532 * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div> 4533 * <script type="text/javascript"> 4534 * (function() { 4535 * const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', { 4536 * boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false, 4537 * intl: { 4538 * enabled: false, 4539 * locale: 'de-DE' 4540 * }, 4541 * keepaspectratio: true, 4542 * axis: true, 4543 * infobox: { 4544 * distanceY: 40, 4545 * intl: { 4546 * enabled: true, 4547 * options: { 4548 * minimumFractionDigits: 1, 4549 * maximumFractionDigits: 2 4550 * } 4551 * } 4552 * } 4553 * }); 4554 * var p = board.create('point', [0.1, 0.1], {}); 4555 * })(); 4556 * 4557 * </script><pre> 4558 * 4559 */ 4560 this.infobox = this.create('text', [0, 0, '0,0'], attr); 4561 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 4562 this.infobox.dump = false; 4563 4564 this.displayInfobox(false); 4565 return this; 4566 }, 4567 4568 /** 4569 * Updates and displays a little info box to show coordinates of current selected points. 4570 * @param {JXG.GeometryElement} el A GeometryElement 4571 * @returns {JXG.Board} Reference to the board 4572 * @see JXG.Board#displayInfobox 4573 * @see JXG.Board#showInfobox 4574 * @see Point#showInfobox 4575 * 4576 */ 4577 updateInfobox: function (el) { 4578 var x, y, xc, yc, 4579 vpinfoboxdigits, 4580 distX, distY, 4581 vpsi = Type.evaluate(el.visProp.showinfobox); 4582 4583 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) { 4584 return this; 4585 } 4586 4587 if (Type.isPoint(el)) { 4588 xc = el.coords.usrCoords[1]; 4589 yc = el.coords.usrCoords[2]; 4590 distX = Type.evaluate(this.infobox.visProp.distancex); 4591 distY = Type.evaluate(this.infobox.visProp.distancey); 4592 4593 this.infobox.setCoords( 4594 xc + distX / this.unitX, 4595 yc + distY / this.unitY 4596 ); 4597 4598 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits); 4599 if (typeof el.infoboxText !== 'string') { 4600 if (vpinfoboxdigits === 'auto') { 4601 if (this.infobox.useLocale()) { 4602 x = this.infobox.formatNumberLocale(xc); 4603 y = this.infobox.formatNumberLocale(yc); 4604 } else { 4605 x = Type.autoDigits(xc); 4606 y = Type.autoDigits(yc); 4607 } 4608 } else if (Type.isNumber(vpinfoboxdigits)) { 4609 if (this.infobox.useLocale()) { 4610 x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits); 4611 y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits); 4612 } else { 4613 x = Type.toFixed(xc, vpinfoboxdigits); 4614 y = Type.toFixed(yc, vpinfoboxdigits); 4615 } 4616 4617 } else { 4618 x = xc; 4619 y = yc; 4620 } 4621 4622 this.highlightInfobox(x, y, el); 4623 } else { 4624 this.highlightCustomInfobox(el.infoboxText, el); 4625 } 4626 4627 this.displayInfobox(true); 4628 } 4629 return this; 4630 }, 4631 4632 /** 4633 * Set infobox visible / invisible. 4634 * 4635 * It uses its property hiddenByParent to memorize its status. 4636 * In this way, many DOM access can be avoided. 4637 * 4638 * @param {Boolean} val true for visible, false for invisible 4639 * @returns {JXG.Board} Reference to the board. 4640 * @see JXG.Board#updateInfobox 4641 * 4642 */ 4643 displayInfobox: function (val) { 4644 if (!val && this.focusObjects.length > 0 && 4645 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) { 4646 // If an element has focus we do not hide its infobox 4647 return this; 4648 } 4649 if (this.infobox.hiddenByParent === val) { 4650 this.infobox.hiddenByParent = !val; 4651 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 4652 } 4653 return this; 4654 }, 4655 4656 // Alias for displayInfobox to be backwards compatible. 4657 // The method showInfobox clashes with the board attribute showInfobox 4658 showInfobox: function (val) { 4659 return this.displayInfobox(val); 4660 }, 4661 4662 /** 4663 * Changes the text of the info box to show the given coordinates. 4664 * @param {Number} x 4665 * @param {Number} y 4666 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 4667 * @returns {JXG.Board} Reference to the board. 4668 */ 4669 highlightInfobox: function (x, y, el) { 4670 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 4671 return this; 4672 }, 4673 4674 /** 4675 * Changes the text of the info box to what is provided via text. 4676 * @param {String} text 4677 * @param {JXG.GeometryElement} [el] 4678 * @returns {JXG.Board} Reference to the board. 4679 */ 4680 highlightCustomInfobox: function (text, el) { 4681 this.infobox.setText(text); 4682 return this; 4683 }, 4684 4685 /** 4686 * Remove highlighting of all elements. 4687 * @returns {JXG.Board} Reference to the board. 4688 */ 4689 dehighlightAll: function () { 4690 var el, 4691 pEl, 4692 stillHighlighted = {}, 4693 needsDeHighlight = false; 4694 4695 for (el in this.highlightedObjects) { 4696 if (this.highlightedObjects.hasOwnProperty(el)) { 4697 4698 pEl = this.highlightedObjects[el]; 4699 if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus 4700 if (this.hasMouseHandlers || this.hasPointerHandlers) { 4701 pEl.noHighlight(); 4702 } 4703 needsDeHighlight = true; 4704 } else { 4705 stillHighlighted[el] = pEl; 4706 } 4707 // In highlightedObjects should only be objects which fulfill all these conditions 4708 // And in case of complex elements, like a turtle based fractal, it should be faster to 4709 // just de-highlight the element instead of checking hasPoint... 4710 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 4711 } 4712 } 4713 4714 this.highlightedObjects = stillHighlighted; 4715 4716 // We do not need to redraw during dehighlighting in CanvasRenderer 4717 // because we are redrawing anyhow 4718 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 4719 // another object is highlighted. 4720 if (this.renderer.type === 'canvas' && needsDeHighlight) { 4721 this.prepareUpdate(); 4722 this.renderer.suspendRedraw(this); 4723 this.updateRenderer(); 4724 this.renderer.unsuspendRedraw(); 4725 } 4726 4727 return this; 4728 }, 4729 4730 /** 4731 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 4732 * once. 4733 * @private 4734 * @param {Number} x X coordinate in screen coordinates 4735 * @param {Number} y Y coordinate in screen coordinates 4736 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 4737 * @see JXG.Board#getUsrCoordsOfMouse 4738 */ 4739 getScrCoordsOfMouse: function (x, y) { 4740 return [x, y]; 4741 }, 4742 4743 /** 4744 * This method calculates the user coords of the current mouse coordinates. 4745 * @param {Event} evt Event object containing the mouse coordinates. 4746 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 4747 * @example 4748 * board.on('up', function (evt) { 4749 * var a = board.getUsrCoordsOfMouse(evt), 4750 * x = a[0], 4751 * y = a[1], 4752 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4753 * // Shorter version: 4754 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4755 * }); 4756 * 4757 * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div> 4758 * <script type='text/javascript'> 4759 * (function() { 4760 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 4761 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4762 * board.on('up', function (evt) { 4763 * var a = board.getUsrCoordsOfMouse(evt), 4764 * x = a[0], 4765 * y = a[1], 4766 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4767 * // Shorter version: 4768 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4769 * }); 4770 * 4771 * })(); 4772 * 4773 * </script><pre> 4774 * 4775 * @see JXG.Board#getScrCoordsOfMouse 4776 * @see JXG.Board#getAllUnderMouse 4777 */ 4778 getUsrCoordsOfMouse: function (evt) { 4779 var cPos = this.getCoordsTopLeftCorner(), 4780 absPos = Env.getPosition(evt, null, this.document), 4781 x = absPos[0] - cPos[0], 4782 y = absPos[1] - cPos[1], 4783 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 4784 4785 return newCoords.usrCoords.slice(1); 4786 }, 4787 4788 /** 4789 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 4790 * @param {Event} evt Event object containing the mouse coordinates. 4791 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 4792 * @see JXG.Board#getUsrCoordsOfMouse 4793 * @see JXG.Board#getAllObjectsUnderMouse 4794 */ 4795 getAllUnderMouse: function (evt) { 4796 var elList = this.getAllObjectsUnderMouse(evt); 4797 elList.push(this.getUsrCoordsOfMouse(evt)); 4798 4799 return elList; 4800 }, 4801 4802 /** 4803 * Collects all elements under current mouse position. 4804 * @param {Event} evt Event object containing the mouse coordinates. 4805 * @returns {Array} Array of elements at the current mouse position. 4806 * @see JXG.Board#getAllUnderMouse 4807 */ 4808 getAllObjectsUnderMouse: function (evt) { 4809 var cPos = this.getCoordsTopLeftCorner(), 4810 absPos = Env.getPosition(evt, null, this.document), 4811 dx = absPos[0] - cPos[0], 4812 dy = absPos[1] - cPos[1], 4813 elList = [], 4814 el, 4815 pEl, 4816 len = this.objectsList.length; 4817 4818 for (el = 0; el < len; el++) { 4819 pEl = this.objectsList[el]; 4820 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 4821 elList[elList.length] = pEl; 4822 } 4823 } 4824 4825 return elList; 4826 }, 4827 4828 /** 4829 * Update the coords object of all elements which possess this 4830 * property. This is necessary after changing the viewport. 4831 * @returns {JXG.Board} Reference to this board. 4832 **/ 4833 updateCoords: function () { 4834 var el, 4835 ob, 4836 len = this.objectsList.length; 4837 4838 for (ob = 0; ob < len; ob++) { 4839 el = this.objectsList[ob]; 4840 4841 if (Type.exists(el.coords)) { 4842 if (Type.evaluate(el.visProp.frozen)) { 4843 if (el.is3D) { 4844 el.element2D.coords.screen2usr(); 4845 } else { 4846 el.coords.screen2usr(); 4847 } 4848 } else { 4849 if (el.is3D) { 4850 el.element2D.coords.usr2screen(); 4851 } else { 4852 el.coords.usr2screen(); 4853 } 4854 } 4855 } 4856 } 4857 return this; 4858 }, 4859 4860 /** 4861 * Moves the origin and initializes an update of all elements. 4862 * @param {Number} x 4863 * @param {Number} y 4864 * @param {Boolean} [diff=false] 4865 * @returns {JXG.Board} Reference to this board. 4866 */ 4867 moveOrigin: function (x, y, diff) { 4868 var ox, oy, ul, lr; 4869 if (Type.exists(x) && Type.exists(y)) { 4870 ox = this.origin.scrCoords[1]; 4871 oy = this.origin.scrCoords[2]; 4872 4873 this.origin.scrCoords[1] = x; 4874 this.origin.scrCoords[2] = y; 4875 4876 if (diff) { 4877 this.origin.scrCoords[1] -= this.drag_dx; 4878 this.origin.scrCoords[2] -= this.drag_dy; 4879 } 4880 4881 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords; 4882 lr = new Coords( 4883 Const.COORDS_BY_SCREEN, 4884 [this.canvasWidth, this.canvasHeight], 4885 this 4886 ).usrCoords; 4887 if ( 4888 ul[1] < this.maxboundingbox[0] || 4889 ul[2] > this.maxboundingbox[1] || 4890 lr[1] > this.maxboundingbox[2] || 4891 lr[2] < this.maxboundingbox[3] 4892 ) { 4893 this.origin.scrCoords[1] = ox; 4894 this.origin.scrCoords[2] = oy; 4895 } 4896 } 4897 4898 this.updateCoords().clearTraces().fullUpdate(); 4899 this.triggerEventHandlers(['boundingbox']); 4900 4901 return this; 4902 }, 4903 4904 /** 4905 * Add conditional updates to the elements. 4906 * @param {String} str String containing conditional update in geonext syntax 4907 */ 4908 addConditions: function (str) { 4909 var term, 4910 m, 4911 left, 4912 right, 4913 name, 4914 el, 4915 property, 4916 functions = [], 4917 // plaintext = 'var el, x, y, c, rgbo;\n', 4918 i = str.indexOf('<data>'), 4919 j = str.indexOf('<' + '/data>'), 4920 xyFun = function (board, el, f, what) { 4921 return function () { 4922 var e, t; 4923 4924 e = board.select(el.id); 4925 t = e.coords.usrCoords[what]; 4926 4927 if (what === 2) { 4928 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 4929 } else { 4930 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 4931 } 4932 e.prepareUpdate().update(); 4933 }; 4934 }, 4935 visFun = function (board, el, f) { 4936 return function () { 4937 var e, v; 4938 4939 e = board.select(el.id); 4940 v = f(); 4941 4942 e.setAttribute({ visible: v }); 4943 }; 4944 }, 4945 colFun = function (board, el, f, what) { 4946 return function () { 4947 var e, v; 4948 4949 e = board.select(el.id); 4950 v = f(); 4951 4952 if (what === 'strokewidth') { 4953 e.visProp.strokewidth = v; 4954 } else { 4955 v = Color.rgba2rgbo(v); 4956 e.visProp[what + 'color'] = v[0]; 4957 e.visProp[what + 'opacity'] = v[1]; 4958 } 4959 }; 4960 }, 4961 posFun = function (board, el, f) { 4962 return function () { 4963 var e = board.select(el.id); 4964 4965 e.position = f(); 4966 }; 4967 }, 4968 styleFun = function (board, el, f) { 4969 return function () { 4970 var e = board.select(el.id); 4971 4972 e.setStyle(f()); 4973 }; 4974 }; 4975 4976 if (i < 0) { 4977 return; 4978 } 4979 4980 while (i >= 0) { 4981 term = str.slice(i + 6, j); // throw away <data> 4982 m = term.indexOf('='); 4983 left = term.slice(0, m); 4984 right = term.slice(m + 1); 4985 m = left.indexOf('.'); // Resulting variable names must not contain dots, e.g. ' Steuern akt.' 4986 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 4987 el = this.elementsByName[Type.unescapeHTML(name)]; 4988 4989 property = left 4990 .slice(m + 1) 4991 .replace(/\s+/g, '') 4992 .toLowerCase(); // remove whitespace in property 4993 right = Type.createFunction(right, this, '', true); 4994 4995 // Debug 4996 if (!Type.exists(this.elementsByName[name])) { 4997 JXG.debug('debug conditions: |' + name + '| undefined'); 4998 } else { 4999 // plaintext += 'el = this.objects[\'' + el.id + '\'];\n'; 5000 5001 switch (property) { 5002 case 'x': 5003 functions.push(xyFun(this, el, right, 2)); 5004 break; 5005 case 'y': 5006 functions.push(xyFun(this, el, right, 1)); 5007 break; 5008 case 'visible': 5009 functions.push(visFun(this, el, right)); 5010 break; 5011 case 'position': 5012 functions.push(posFun(this, el, right)); 5013 break; 5014 case 'stroke': 5015 functions.push(colFun(this, el, right, 'stroke')); 5016 break; 5017 case 'style': 5018 functions.push(styleFun(this, el, right)); 5019 break; 5020 case 'strokewidth': 5021 functions.push(colFun(this, el, right, 'strokewidth')); 5022 break; 5023 case 'fill': 5024 functions.push(colFun(this, el, right, 'fill')); 5025 break; 5026 case 'label': 5027 break; 5028 default: 5029 JXG.debug( 5030 'property "' + 5031 property + 5032 '" in conditions not yet implemented:' + 5033 right 5034 ); 5035 break; 5036 } 5037 } 5038 str = str.slice(j + 7); // cut off '</data>' 5039 i = str.indexOf('<data>'); 5040 j = str.indexOf('<' + '/data>'); 5041 } 5042 5043 this.updateConditions = function () { 5044 var i; 5045 5046 for (i = 0; i < functions.length; i++) { 5047 functions[i](); 5048 } 5049 5050 this.prepareUpdate().updateElements(); 5051 return true; 5052 }; 5053 this.updateConditions(); 5054 }, 5055 5056 /** 5057 * Computes the commands in the conditions-section of the gxt file. 5058 * It is evaluated after an update, before the unsuspendRedraw. 5059 * The function is generated in 5060 * @see JXG.Board#addConditions 5061 * @private 5062 */ 5063 updateConditions: function () { 5064 return false; 5065 }, 5066 5067 /** 5068 * Calculates adequate snap sizes. 5069 * @returns {JXG.Board} Reference to the board. 5070 */ 5071 calculateSnapSizes: function () { 5072 var p1, p2, 5073 bbox = this.getBoundingBox(), 5074 gridStep = Type.evaluate(this.options.grid.majorStep), 5075 gridX = Type.evaluate(this.options.grid.gridX), 5076 gridY = Type.evaluate(this.options.grid.gridY), 5077 x, y; 5078 5079 if (!Type.isArray(gridStep)) { 5080 gridStep = [gridStep, gridStep]; 5081 } 5082 if (gridStep.length < 2) { 5083 gridStep = [gridStep[0], gridStep[0]]; 5084 } 5085 if (Type.exists(gridX)) { 5086 gridStep[0] = gridX; 5087 } 5088 if (Type.exists(gridY)) { 5089 gridStep[1] = gridY; 5090 } 5091 5092 if (gridStep[0] === 'auto') { 5093 gridStep[0] = 1; 5094 } else { 5095 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX); 5096 } 5097 if (gridStep[1] === 'auto') { 5098 gridStep[1] = 1; 5099 } else { 5100 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY); 5101 } 5102 5103 p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this); 5104 p2 = new Coords( 5105 Const.COORDS_BY_USER, 5106 [gridStep[0], gridStep[1]], 5107 this 5108 ); 5109 x = p1.scrCoords[1] - p2.scrCoords[1]; 5110 y = p1.scrCoords[2] - p2.scrCoords[2]; 5111 5112 this.options.grid.snapSizeX = gridStep[0]; 5113 while (Math.abs(x) > 25) { 5114 this.options.grid.snapSizeX *= 2; 5115 x /= 2; 5116 } 5117 5118 this.options.grid.snapSizeY = gridStep[1]; 5119 while (Math.abs(y) > 25) { 5120 this.options.grid.snapSizeY *= 2; 5121 y /= 2; 5122 } 5123 5124 return this; 5125 }, 5126 5127 /** 5128 * Apply update on all objects with the new zoom-factors. Clears all traces. 5129 * @returns {JXG.Board} Reference to the board. 5130 */ 5131 applyZoom: function () { 5132 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 5133 5134 return this; 5135 }, 5136 5137 /** 5138 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5139 * The zoom operation is centered at x, y. 5140 * @param {Number} [x] 5141 * @param {Number} [y] 5142 * @returns {JXG.Board} Reference to the board 5143 */ 5144 zoomIn: function (x, y) { 5145 var bb = this.getBoundingBox(), 5146 zX = this.attr.zoom.factorx, 5147 zY = this.attr.zoom.factory, 5148 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 5149 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 5150 lr = 0.5, 5151 tr = 0.5, 5152 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 5153 5154 if ( 5155 (this.zoomX > this.attr.zoom.max && zX > 1.0) || 5156 (this.zoomY > this.attr.zoom.max && zY > 1.0) || 5157 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 5158 (this.zoomY < mi && zY < 1.0) 5159 ) { 5160 return this; 5161 } 5162 5163 if (Type.isNumber(x) && Type.isNumber(y)) { 5164 lr = (x - bb[0]) / (bb[2] - bb[0]); 5165 tr = (bb[1] - y) / (bb[1] - bb[3]); 5166 } 5167 5168 this.setBoundingBox( 5169 [ 5170 bb[0] + dX * lr, 5171 bb[1] - dY * tr, 5172 bb[2] - dX * (1 - lr), 5173 bb[3] + dY * (1 - tr) 5174 ], 5175 this.keepaspectratio, 5176 'update' 5177 ); 5178 return this.applyZoom(); 5179 }, 5180 5181 /** 5182 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5183 * The zoom operation is centered at x, y. 5184 * 5185 * @param {Number} [x] 5186 * @param {Number} [y] 5187 * @returns {JXG.Board} Reference to the board 5188 */ 5189 zoomOut: function (x, y) { 5190 var bb = this.getBoundingBox(), 5191 zX = this.attr.zoom.factorx, 5192 zY = this.attr.zoom.factory, 5193 dX = (bb[2] - bb[0]) * (1.0 - zX), 5194 dY = (bb[1] - bb[3]) * (1.0 - zY), 5195 lr = 0.5, 5196 tr = 0.5, 5197 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 5198 5199 if (this.zoomX < mi || this.zoomY < mi) { 5200 return this; 5201 } 5202 5203 if (Type.isNumber(x) && Type.isNumber(y)) { 5204 lr = (x - bb[0]) / (bb[2] - bb[0]); 5205 tr = (bb[1] - y) / (bb[1] - bb[3]); 5206 } 5207 5208 this.setBoundingBox( 5209 [ 5210 bb[0] + dX * lr, 5211 bb[1] - dY * tr, 5212 bb[2] - dX * (1 - lr), 5213 bb[3] + dY * (1 - tr) 5214 ], 5215 this.keepaspectratio, 5216 'update' 5217 ); 5218 5219 return this.applyZoom(); 5220 }, 5221 5222 /** 5223 * Reset the zoom level to the original zoom level from initBoard(); 5224 * Additionally, if the board as been initialized with a boundingBox (which is the default), 5225 * restore the viewport to the original viewport during initialization. Otherwise, 5226 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 5227 * just set the zoom level to 100%. 5228 * 5229 * @returns {JXG.Board} Reference to the board 5230 */ 5231 zoom100: function () { 5232 var bb, dX, dY; 5233 5234 if (Type.exists(this.attr.boundingbox)) { 5235 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 5236 } else { 5237 // Board has been set up with unitX/Y and originX/Y 5238 bb = this.getBoundingBox(); 5239 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 5240 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 5241 this.setBoundingBox( 5242 [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], 5243 this.keepaspectratio, 5244 'reset' 5245 ); 5246 } 5247 return this.applyZoom(); 5248 }, 5249 5250 /** 5251 * Zooms the board so every visible point is shown. Keeps aspect ratio. 5252 * @returns {JXG.Board} Reference to the board 5253 */ 5254 zoomAllPoints: function () { 5255 var el, 5256 border, 5257 borderX, 5258 borderY, 5259 pEl, 5260 minX = 0, 5261 maxX = 0, 5262 minY = 0, 5263 maxY = 0, 5264 len = this.objectsList.length; 5265 5266 for (el = 0; el < len; el++) { 5267 pEl = this.objectsList[el]; 5268 5269 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 5270 if (pEl.coords.usrCoords[1] < minX) { 5271 minX = pEl.coords.usrCoords[1]; 5272 } else if (pEl.coords.usrCoords[1] > maxX) { 5273 maxX = pEl.coords.usrCoords[1]; 5274 } 5275 if (pEl.coords.usrCoords[2] > maxY) { 5276 maxY = pEl.coords.usrCoords[2]; 5277 } else if (pEl.coords.usrCoords[2] < minY) { 5278 minY = pEl.coords.usrCoords[2]; 5279 } 5280 } 5281 } 5282 5283 border = 50; 5284 borderX = border / this.unitX; 5285 borderY = border / this.unitY; 5286 5287 this.setBoundingBox( 5288 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], 5289 this.keepaspectratio, 5290 'update' 5291 ); 5292 5293 return this.applyZoom(); 5294 }, 5295 5296 /** 5297 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 5298 * within the board's viewport. 5299 * @param {Array} elements A set of elements given by id, reference, or name. 5300 * @returns {JXG.Board} Reference to the board. 5301 */ 5302 zoomElements: function (elements) { 5303 var i, e, 5304 box, 5305 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 5306 cx, cy, 5307 dx, dy, 5308 d; 5309 5310 if (!Type.isArray(elements) || elements.length === 0) { 5311 return this; 5312 } 5313 5314 for (i = 0; i < elements.length; i++) { 5315 e = this.select(elements[i]); 5316 5317 box = e.bounds(); 5318 if (Type.isArray(box)) { 5319 if (box[0] < newBBox[0]) { 5320 newBBox[0] = box[0]; 5321 } 5322 if (box[1] > newBBox[1]) { 5323 newBBox[1] = box[1]; 5324 } 5325 if (box[2] > newBBox[2]) { 5326 newBBox[2] = box[2]; 5327 } 5328 if (box[3] < newBBox[3]) { 5329 newBBox[3] = box[3]; 5330 } 5331 } 5332 } 5333 5334 if (Type.isArray(newBBox)) { 5335 cx = 0.5 * (newBBox[0] + newBBox[2]); 5336 cy = 0.5 * (newBBox[1] + newBBox[3]); 5337 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5; 5338 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5; 5339 d = Math.max(dx, dy); 5340 this.setBoundingBox( 5341 [cx - d, cy + d, cx + d, cy - d], 5342 this.keepaspectratio, 5343 'update' 5344 ); 5345 } 5346 5347 return this; 5348 }, 5349 5350 /** 5351 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 5352 * @param {Number} fX 5353 * @param {Number} fY 5354 * @returns {JXG.Board} Reference to the board. 5355 */ 5356 setZoom: function (fX, fY) { 5357 var oX = this.attr.zoom.factorx, 5358 oY = this.attr.zoom.factory; 5359 5360 this.attr.zoom.factorx = fX / this.zoomX; 5361 this.attr.zoom.factory = fY / this.zoomY; 5362 5363 this.zoomIn(); 5364 5365 this.attr.zoom.factorx = oX; 5366 this.attr.zoom.factory = oY; 5367 5368 return this; 5369 }, 5370 5371 /** 5372 * Inner, recursive method of removeObject. 5373 * 5374 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5375 * The element(s) is/are given by name, id or a reference. 5376 * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements 5377 * and tests if the element to be deleted is a child element. If this is the case, it will be 5378 * removed from the list of child elements. If saveMethod=false (default), the element 5379 * is removed from the lists of child elements of all its ancestors. 5380 * The latter should be much faster. 5381 * @returns {JXG.Board} Reference to the board 5382 * @private 5383 */ 5384 _removeObj: function (object, saveMethod) { 5385 var el, i; 5386 5387 if (Type.isArray(object)) { 5388 for (i = 0; i < object.length; i++) { 5389 this._removeObj(object[i], saveMethod); 5390 } 5391 5392 return this; 5393 } 5394 5395 object = this.select(object); 5396 5397 // If the object which is about to be removed is unknown or a string, do nothing. 5398 // it is a string if a string was given and could not be resolved to an element. 5399 if (!Type.exists(object) || Type.isString(object)) { 5400 return this; 5401 } 5402 5403 try { 5404 // remove all children. 5405 for (el in object.childElements) { 5406 if (object.childElements.hasOwnProperty(el)) { 5407 object.childElements[el].board._removeObj(object.childElements[el]); 5408 } 5409 } 5410 5411 // Remove all children in elements like turtle 5412 for (el in object.objects) { 5413 if (object.objects.hasOwnProperty(el)) { 5414 object.objects[el].board._removeObj(object.objects[el]); 5415 } 5416 } 5417 5418 // Remove the element from the childElement list and the descendant list of all elements. 5419 if (saveMethod) { 5420 // Running through all objects has quadratic complexity if many objects are deleted. 5421 for (el in this.objects) { 5422 if (this.objects.hasOwnProperty(el)) { 5423 if ( 5424 Type.exists(this.objects[el].childElements) && 5425 Type.exists( 5426 this.objects[el].childElements.hasOwnProperty(object.id) 5427 ) 5428 ) { 5429 delete this.objects[el].childElements[object.id]; 5430 delete this.objects[el].descendants[object.id]; 5431 } 5432 } 5433 } 5434 } else if (Type.exists(object.ancestors)) { 5435 // Running through the ancestors should be much more efficient. 5436 for (el in object.ancestors) { 5437 if (object.ancestors.hasOwnProperty(el)) { 5438 if ( 5439 Type.exists(object.ancestors[el].childElements) && 5440 Type.exists( 5441 object.ancestors[el].childElements.hasOwnProperty(object.id) 5442 ) 5443 ) { 5444 delete object.ancestors[el].childElements[object.id]; 5445 delete object.ancestors[el].descendants[object.id]; 5446 } 5447 } 5448 } 5449 } 5450 5451 // remove the object itself from our control structures 5452 if (object._pos > -1) { 5453 this.objectsList.splice(object._pos, 1); 5454 for (i = object._pos; i < this.objectsList.length; i++) { 5455 this.objectsList[i]._pos--; 5456 } 5457 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 5458 JXG.debug( 5459 'Board.removeObject: object ' + object.id + ' not found in list.' 5460 ); 5461 } 5462 5463 delete this.objects[object.id]; 5464 delete this.elementsByName[object.name]; 5465 5466 if (object.visProp && Type.evaluate(object.visProp.trace)) { 5467 object.clearTrace(); 5468 } 5469 5470 // the object deletion itself is handled by the object. 5471 if (Type.exists(object.remove)) { 5472 object.remove(); 5473 } 5474 } catch (e) { 5475 JXG.debug(object.id + ': Could not be removed: ' + e); 5476 } 5477 5478 return this; 5479 }, 5480 5481 /** 5482 * Removes object from board and renderer. 5483 * <p> 5484 * <b>Performance hints:</b> It is recommended to use the object's id. 5485 * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt> 5486 * before looping through the elements to be removed and call 5487 * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop 5488 * in reverse order, i.e. remove the object in reverse order of their creation time. 5489 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5490 * The element(s) is/are given by name, id or a reference. 5491 * @param {Boolean} saveMethod If true, the algorithm runs through all elements 5492 * and tests if the element to be deleted is a child element. If yes, it will be 5493 * removed from the list of child elements. If false (default), the element 5494 * is removed from the lists of child elements of all its ancestors. 5495 * This should be much faster. 5496 * @returns {JXG.Board} Reference to the board 5497 */ 5498 removeObject: function (object, saveMethod) { 5499 var i; 5500 5501 this.renderer.suspendRedraw(this); 5502 if (Type.isArray(object)) { 5503 for (i = 0; i < object.length; i++) { 5504 this._removeObj(object[i], saveMethod); 5505 } 5506 } else { 5507 this._removeObj(object, saveMethod); 5508 } 5509 this.renderer.unsuspendRedraw(); 5510 5511 this.update(); 5512 return this; 5513 }, 5514 5515 /** 5516 * Removes the ancestors of an object an the object itself from board and renderer. 5517 * @param {JXG.GeometryElement} object The object to remove. 5518 * @returns {JXG.Board} Reference to the board 5519 */ 5520 removeAncestors: function (object) { 5521 var anc; 5522 5523 for (anc in object.ancestors) { 5524 if (object.ancestors.hasOwnProperty(anc)) { 5525 this.removeAncestors(object.ancestors[anc]); 5526 } 5527 } 5528 5529 this.removeObject(object); 5530 5531 return this; 5532 }, 5533 5534 /** 5535 * Initialize some objects which are contained in every GEONExT construction by default, 5536 * but are not contained in the gxt files. 5537 * @returns {JXG.Board} Reference to the board 5538 */ 5539 initGeonextBoard: function () { 5540 var p1, p2, p3; 5541 5542 p1 = this.create('point', [0, 0], { 5543 id: this.id + 'g00e0', 5544 name: 'Ursprung', 5545 withLabel: false, 5546 visible: false, 5547 fixed: true 5548 }); 5549 5550 p2 = this.create('point', [1, 0], { 5551 id: this.id + 'gX0e0', 5552 name: 'Punkt_1_0', 5553 withLabel: false, 5554 visible: false, 5555 fixed: true 5556 }); 5557 5558 p3 = this.create('point', [0, 1], { 5559 id: this.id + 'gY0e0', 5560 name: 'Punkt_0_1', 5561 withLabel: false, 5562 visible: false, 5563 fixed: true 5564 }); 5565 5566 this.create('line', [p1, p2], { 5567 id: this.id + 'gXLe0', 5568 name: 'X-Achse', 5569 withLabel: false, 5570 visible: false 5571 }); 5572 5573 this.create('line', [p1, p3], { 5574 id: this.id + 'gYLe0', 5575 name: 'Y-Achse', 5576 withLabel: false, 5577 visible: false 5578 }); 5579 5580 return this; 5581 }, 5582 5583 /** 5584 * Change the height and width of the board's container. 5585 * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using 5586 * the actual size of the bounding box and the actual value of keepaspectratio. 5587 * If setBoundingbox() should not be called automatically, 5588 * call resizeContainer with dontSetBoundingBox == true. 5589 * @param {Number} canvasWidth New width of the container. 5590 * @param {Number} canvasHeight New height of the container. 5591 * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element. 5592 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center. 5593 * @returns {JXG.Board} Reference to the board 5594 */ 5595 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 5596 var box, 5597 oldWidth, oldHeight, 5598 oX, oY; 5599 5600 oldWidth = this.canvasWidth; 5601 oldHeight = this.canvasHeight; 5602 5603 if (!dontSetBoundingBox) { 5604 box = this.getBoundingBox(); // This is the actual bounding box. 5605 } 5606 5607 // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps); 5608 // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps); 5609 this.canvasWidth = parseFloat(canvasWidth); 5610 this.canvasHeight = parseFloat(canvasHeight); 5611 5612 if (!dontset) { 5613 this.containerObj.style.width = this.canvasWidth + 'px'; 5614 this.containerObj.style.height = this.canvasHeight + 'px'; 5615 } 5616 this.renderer.resize(this.canvasWidth, this.canvasHeight); 5617 5618 if (!dontSetBoundingBox) { 5619 this.setBoundingBox(box, this.keepaspectratio, 'keep'); 5620 } else { 5621 oX = (this.canvasWidth - oldWidth) * 0.5; 5622 oY = (this.canvasHeight - oldHeight) * 0.5; 5623 5624 this.moveOrigin( 5625 this.origin.scrCoords[1] + oX, 5626 this.origin.scrCoords[2] + oY 5627 ); 5628 } 5629 5630 return this; 5631 }, 5632 5633 /** 5634 * Lists the dependencies graph in a new HTML-window. 5635 * @returns {JXG.Board} Reference to the board 5636 */ 5637 showDependencies: function () { 5638 var el, t, c, f, i; 5639 5640 t = '<p>\n'; 5641 for (el in this.objects) { 5642 if (this.objects.hasOwnProperty(el)) { 5643 i = 0; 5644 for (c in this.objects[el].childElements) { 5645 if (this.objects[el].childElements.hasOwnProperty(c)) { 5646 i += 1; 5647 } 5648 } 5649 if (i >= 0) { 5650 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 5651 } 5652 5653 for (c in this.objects[el].childElements) { 5654 if (this.objects[el].childElements.hasOwnProperty(c)) { 5655 t += 5656 this.objects[el].childElements[c].id + 5657 '(' + 5658 this.objects[el].childElements[c].name + 5659 ')' + 5660 ', '; 5661 } 5662 } 5663 t += '<p>\n'; 5664 } 5665 } 5666 t += '<' + '/p>\n'; 5667 f = window.open(); 5668 f.document.open(); 5669 f.document.write(t); 5670 f.document.close(); 5671 return this; 5672 }, 5673 5674 /** 5675 * Lists the XML code of the construction in a new HTML-window. 5676 * @returns {JXG.Board} Reference to the board 5677 */ 5678 showXML: function () { 5679 var f = window.open(''); 5680 f.document.open(); 5681 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 5682 f.document.close(); 5683 return this; 5684 }, 5685 5686 /** 5687 * Sets for all objects the needsUpdate flag to 'true'. 5688 * @returns {JXG.Board} Reference to the board 5689 */ 5690 prepareUpdate: function () { 5691 var el, 5692 pEl, 5693 len = this.objectsList.length; 5694 5695 /* 5696 if (this.attr.updatetype === 'hierarchical') { 5697 return this; 5698 } 5699 */ 5700 5701 for (el = 0; el < len; el++) { 5702 pEl = this.objectsList[el]; 5703 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5704 } 5705 5706 for (el in this.groups) { 5707 if (this.groups.hasOwnProperty(el)) { 5708 pEl = this.groups[el]; 5709 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5710 } 5711 } 5712 5713 return this; 5714 }, 5715 5716 /** 5717 * Runs through all elements and calls their update() method. 5718 * @param {JXG.GeometryElement} drag Element that caused the update. 5719 * @returns {JXG.Board} Reference to the board 5720 */ 5721 updateElements: function (drag) { 5722 var el, pEl; 5723 //var childId, i = 0; 5724 5725 drag = this.select(drag); 5726 5727 /* 5728 if (Type.exists(drag)) { 5729 for (el = 0; el < this.objectsList.length; el++) { 5730 pEl = this.objectsList[el]; 5731 if (pEl.id === drag.id) { 5732 i = el; 5733 break; 5734 } 5735 } 5736 } 5737 */ 5738 for (el = 0; el < this.objectsList.length; el++) { 5739 pEl = this.objectsList[el]; 5740 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) { 5741 pEl.updateSize(); 5742 } 5743 5744 // For updates of an element we distinguish if the dragged element is updated or 5745 // other elements are updated. 5746 // The difference lies in the treatment of gliders and points based on transformations. 5747 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility(); 5748 } 5749 5750 // update groups last 5751 for (el in this.groups) { 5752 if (this.groups.hasOwnProperty(el)) { 5753 this.groups[el].update(drag); 5754 } 5755 } 5756 5757 return this; 5758 }, 5759 5760 /** 5761 * Runs through all elements and calls their update() method. 5762 * @returns {JXG.Board} Reference to the board 5763 */ 5764 updateRenderer: function () { 5765 var el, 5766 len = this.objectsList.length; 5767 5768 if (!this.renderer) { 5769 return; 5770 } 5771 5772 /* 5773 objs = this.objectsList.slice(0); 5774 objs.sort(function (a, b) { 5775 if (a.visProp.layer < b.visProp.layer) { 5776 return -1; 5777 } else if (a.visProp.layer === b.visProp.layer) { 5778 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 5779 } else { 5780 return 1; 5781 } 5782 }); 5783 */ 5784 5785 if (this.renderer.type === 'canvas') { 5786 this.updateRendererCanvas(); 5787 } else { 5788 for (el = 0; el < len; el++) { 5789 this.objectsList[el].updateRenderer(); 5790 } 5791 } 5792 return this; 5793 }, 5794 5795 /** 5796 * Runs through all elements and calls their update() method. 5797 * This is a special version for the CanvasRenderer. 5798 * Here, we have to do our own layer handling. 5799 * @returns {JXG.Board} Reference to the board 5800 */ 5801 updateRendererCanvas: function () { 5802 var el, 5803 pEl, 5804 i, 5805 mini, 5806 la, 5807 olen = this.objectsList.length, 5808 layers = this.options.layer, 5809 len = this.options.layer.numlayers, 5810 last = Number.NEGATIVE_INFINITY; 5811 5812 for (i = 0; i < len; i++) { 5813 mini = Number.POSITIVE_INFINITY; 5814 5815 for (la in layers) { 5816 if (layers.hasOwnProperty(la)) { 5817 if (layers[la] > last && layers[la] < mini) { 5818 mini = layers[la]; 5819 } 5820 } 5821 } 5822 5823 last = mini; 5824 5825 for (el = 0; el < olen; el++) { 5826 pEl = this.objectsList[el]; 5827 5828 if (pEl.visProp.layer === mini) { 5829 pEl.prepareUpdate().updateRenderer(); 5830 } 5831 } 5832 } 5833 return this; 5834 }, 5835 5836 /** 5837 * Please use {@link JXG.Board.on} instead. 5838 * @param {Function} hook A function to be called by the board after an update occurred. 5839 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 5840 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 5841 * board object the hook is attached to. 5842 * @returns {Number} Id of the hook, required to remove the hook from the board. 5843 * @deprecated 5844 */ 5845 addHook: function (hook, m, context) { 5846 JXG.deprecated('Board.addHook()', 'Board.on()'); 5847 m = Type.def(m, 'update'); 5848 5849 context = Type.def(context, this); 5850 5851 this.hooks.push([m, hook]); 5852 this.on(m, hook, context); 5853 5854 return this.hooks.length - 1; 5855 }, 5856 5857 /** 5858 * Alias of {@link JXG.Board.on}. 5859 */ 5860 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 5861 5862 /** 5863 * Please use {@link JXG.Board.off} instead. 5864 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 5865 * @returns {JXG.Board} Reference to the board 5866 * @deprecated 5867 */ 5868 removeHook: function (id) { 5869 JXG.deprecated('Board.removeHook()', 'Board.off()'); 5870 if (this.hooks[id]) { 5871 this.off(this.hooks[id][0], this.hooks[id][1]); 5872 this.hooks[id] = null; 5873 } 5874 5875 return this; 5876 }, 5877 5878 /** 5879 * Alias of {@link JXG.Board.off}. 5880 */ 5881 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 5882 5883 /** 5884 * Runs through all hooked functions and calls them. 5885 * @returns {JXG.Board} Reference to the board 5886 * @deprecated 5887 */ 5888 updateHooks: function (m) { 5889 var arg = Array.prototype.slice.call(arguments, 0); 5890 5891 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 5892 5893 arg[0] = Type.def(arg[0], 'update'); 5894 this.triggerEventHandlers([arg[0]], arguments); 5895 5896 return this; 5897 }, 5898 5899 /** 5900 * Adds a dependent board to this board. 5901 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 5902 * @returns {JXG.Board} Reference to the board 5903 */ 5904 addChild: function (board) { 5905 if (Type.exists(board) && Type.exists(board.containerObj)) { 5906 this.dependentBoards.push(board); 5907 this.update(); 5908 } 5909 return this; 5910 }, 5911 5912 /** 5913 * Deletes a board from the list of dependent boards. 5914 * @param {JXG.Board} board Reference to the board which will be removed. 5915 * @returns {JXG.Board} Reference to the board 5916 */ 5917 removeChild: function (board) { 5918 var i; 5919 5920 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 5921 if (this.dependentBoards[i] === board) { 5922 this.dependentBoards.splice(i, 1); 5923 } 5924 } 5925 return this; 5926 }, 5927 5928 /** 5929 * Runs through most elements and calls their update() method and update the conditions. 5930 * @param {JXG.GeometryElement} [drag] Element that caused the update. 5931 * @returns {JXG.Board} Reference to the board 5932 */ 5933 update: function (drag) { 5934 var i, len, b, insert, storeActiveEl; 5935 5936 if (this.inUpdate || this.isSuspendedUpdate) { 5937 return this; 5938 } 5939 this.inUpdate = true; 5940 5941 if ( 5942 this.attr.minimizereflow === 'all' && 5943 this.containerObj && 5944 this.renderer.type !== 'vml' 5945 ) { 5946 storeActiveEl = this.document.activeElement; // Store focus element 5947 insert = this.renderer.removeToInsertLater(this.containerObj); 5948 } 5949 5950 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 5951 storeActiveEl = this.document.activeElement; 5952 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 5953 } 5954 5955 this.prepareUpdate().updateElements(drag).updateConditions(); 5956 5957 this.renderer.suspendRedraw(this); 5958 this.updateRenderer(); 5959 this.renderer.unsuspendRedraw(); 5960 this.triggerEventHandlers(['update'], []); 5961 5962 if (insert) { 5963 insert(); 5964 storeActiveEl.focus(); // Restore focus element 5965 } 5966 5967 // To resolve dependencies between boards 5968 // for (var board in JXG.boards) { 5969 len = this.dependentBoards.length; 5970 for (i = 0; i < len; i++) { 5971 b = this.dependentBoards[i]; 5972 if (Type.exists(b) && b !== this) { 5973 b.updateQuality = this.updateQuality; 5974 b.prepareUpdate().updateElements().updateConditions(); 5975 b.renderer.suspendRedraw(this); 5976 b.updateRenderer(); 5977 b.renderer.unsuspendRedraw(); 5978 b.triggerEventHandlers(['update'], []); 5979 } 5980 } 5981 5982 this.inUpdate = false; 5983 return this; 5984 }, 5985 5986 /** 5987 * Runs through all elements and calls their update() method and update the conditions. 5988 * This is necessary after zooming and changing the bounding box. 5989 * @returns {JXG.Board} Reference to the board 5990 */ 5991 fullUpdate: function () { 5992 this.needsFullUpdate = true; 5993 this.update(); 5994 this.needsFullUpdate = false; 5995 return this; 5996 }, 5997 5998 /** 5999 * Adds a grid to the board according to the settings given in board.options. 6000 * @returns {JXG.Board} Reference to the board. 6001 */ 6002 addGrid: function () { 6003 this.create('grid', []); 6004 6005 return this; 6006 }, 6007 6008 /** 6009 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 6010 * more of the grids. 6011 * @returns {JXG.Board} Reference to the board object. 6012 */ 6013 removeGrids: function () { 6014 var i; 6015 6016 for (i = 0; i < this.grids.length; i++) { 6017 this.removeObject(this.grids[i]); 6018 } 6019 6020 this.grids.length = 0; 6021 this.update(); // required for canvas renderer 6022 6023 return this; 6024 }, 6025 6026 /** 6027 * Creates a new geometric element of type elementType. 6028 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 6029 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 6030 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 6031 * methods for a list of possible parameters. 6032 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 6033 * Common attributes are name, visible, strokeColor. 6034 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 6035 * two or more elements. 6036 */ 6037 create: function (elementType, parents, attributes) { 6038 var el, i; 6039 6040 elementType = elementType.toLowerCase(); 6041 6042 if (!Type.exists(parents)) { 6043 parents = []; 6044 } 6045 6046 if (!Type.exists(attributes)) { 6047 attributes = {}; 6048 } 6049 6050 for (i = 0; i < parents.length; i++) { 6051 if ( 6052 Type.isString(parents[i]) && 6053 !(elementType === 'text' && i === 2) && 6054 !(elementType === 'solidofrevolution3d' && i === 2) && 6055 !( 6056 (elementType === 'input' || 6057 elementType === 'checkbox' || 6058 elementType === 'button') && 6059 (i === 2 || i === 3) 6060 ) && 6061 !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the 6062 // variable name 6063 !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y' 6064 !(elementType === 'implicitcurve') 6065 ) { 6066 parents[i] = this.select(parents[i]); 6067 } 6068 } 6069 6070 if (Type.isFunction(JXG.elements[elementType])) { 6071 el = JXG.elements[elementType](this, parents, attributes); 6072 } else { 6073 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType); 6074 } 6075 6076 if (!Type.exists(el)) { 6077 JXG.debug('JSXGraph: create: failure creating ' + elementType); 6078 return el; 6079 } 6080 6081 if (el.prepareUpdate && el.update && el.updateRenderer) { 6082 el.fullUpdate(); 6083 } 6084 return el; 6085 }, 6086 6087 /** 6088 * Deprecated name for {@link JXG.Board.create}. 6089 * @deprecated 6090 */ 6091 createElement: function () { 6092 JXG.deprecated('Board.createElement()', 'Board.create()'); 6093 return this.create.apply(this, arguments); 6094 }, 6095 6096 /** 6097 * Delete the elements drawn as part of a trace of an element. 6098 * @returns {JXG.Board} Reference to the board 6099 */ 6100 clearTraces: function () { 6101 var el; 6102 6103 for (el = 0; el < this.objectsList.length; el++) { 6104 this.objectsList[el].clearTrace(); 6105 } 6106 6107 this.numTraces = 0; 6108 return this; 6109 }, 6110 6111 /** 6112 * Stop updates of the board. 6113 * @returns {JXG.Board} Reference to the board 6114 */ 6115 suspendUpdate: function () { 6116 if (!this.inUpdate) { 6117 this.isSuspendedUpdate = true; 6118 } 6119 return this; 6120 }, 6121 6122 /** 6123 * Enable updates of the board. 6124 * @returns {JXG.Board} Reference to the board 6125 */ 6126 unsuspendUpdate: function () { 6127 if (this.isSuspendedUpdate) { 6128 this.isSuspendedUpdate = false; 6129 this.fullUpdate(); 6130 } 6131 return this; 6132 }, 6133 6134 /** 6135 * Set the bounding box of the board. 6136 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 6137 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 6138 * the resulting viewport may be larger. 6139 * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset' 6140 * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0). 6141 * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing. 6142 * @returns {JXG.Board} Reference to the board 6143 */ 6144 setBoundingBox: function (bbox, keepaspectratio, setZoom) { 6145 var h, w, ux, uy, 6146 offX = 0, 6147 offY = 0, 6148 zoom_ratio = 1, 6149 ratio, dx, dy, prev_w, prev_h, 6150 dim = Env.getDimensions(this.container, this.document); 6151 6152 if (!Type.isArray(bbox)) { 6153 return this; 6154 } 6155 6156 if ( 6157 bbox[0] < this.maxboundingbox[0] || 6158 bbox[1] > this.maxboundingbox[1] || 6159 bbox[2] > this.maxboundingbox[2] || 6160 bbox[3] < this.maxboundingbox[3] 6161 ) { 6162 return this; 6163 } 6164 6165 if (!Type.exists(setZoom)) { 6166 setZoom = 'reset'; 6167 } 6168 6169 ux = this.unitX; 6170 uy = this.unitY; 6171 this.canvasWidth = parseFloat(dim.width); // parseInt(dim.width, 10); 6172 this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10); 6173 w = this.canvasWidth; 6174 h = this.canvasHeight; 6175 if (keepaspectratio) { 6176 if (this.keepaspectratio) { 6177 ratio = ux / uy; // Keep this ratio if keepaspectratio was true 6178 if (isNaN(ratio)) { 6179 ratio = 1.0; 6180 } 6181 } else { 6182 ratio = 1.0; 6183 } 6184 if (setZoom === 'keep') { 6185 zoom_ratio = this.zoomX / this.zoomY; 6186 } 6187 dx = bbox[2] - bbox[0]; 6188 dy = bbox[1] - bbox[3]; 6189 prev_w = ux * dx; 6190 prev_h = uy * dy; 6191 if (w >= h) { 6192 if (prev_w >= prev_h) { 6193 this.unitY = h / dy; 6194 this.unitX = this.unitY * ratio; 6195 } else { 6196 // Switch dominating interval 6197 this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio; 6198 this.unitX = this.unitY * ratio; 6199 } 6200 } else { 6201 if (prev_h > prev_w) { 6202 this.unitX = w / dx; 6203 this.unitY = this.unitX / ratio; 6204 } else { 6205 // Switch dominating interval 6206 this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio; 6207 this.unitY = this.unitX / ratio; 6208 } 6209 } 6210 // Add the additional units in equal portions left and right 6211 offX = (w / this.unitX - dx) * 0.5; 6212 // Add the additional units in equal portions above and below 6213 offY = (h / this.unitY - dy) * 0.5; 6214 this.keepaspectratio = true; 6215 } else { 6216 this.unitX = w / (bbox[2] - bbox[0]); 6217 this.unitY = h / (bbox[1] - bbox[3]); 6218 this.keepaspectratio = false; 6219 } 6220 6221 this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY)); 6222 6223 if (setZoom === 'update') { 6224 this.zoomX *= this.unitX / ux; 6225 this.zoomY *= this.unitY / uy; 6226 } else if (setZoom === 'reset') { 6227 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0; 6228 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0; 6229 } 6230 6231 return this; 6232 }, 6233 6234 /** 6235 * Get the bounding box of the board. 6236 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 6237 */ 6238 getBoundingBox: function () { 6239 var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords, 6240 lr = new Coords( 6241 Const.COORDS_BY_SCREEN, 6242 [this.canvasWidth, this.canvasHeight], 6243 this 6244 ).usrCoords; 6245 return [ul[1], ul[2], lr[1], lr[2]]; 6246 }, 6247 6248 /** 6249 * Sets the value of attribute <tt>key</tt> to <tt>value</tt>. 6250 * @param {String} key The attribute's name. 6251 * @param value The new value 6252 * @private 6253 */ 6254 _set: function (key, value) { 6255 key = key.toLocaleLowerCase(); 6256 6257 if ( 6258 value !== null && 6259 Type.isObject(value) && 6260 !Type.exists(value.id) && 6261 !Type.exists(value.name) 6262 ) { 6263 // value is of type {prop: val, prop: val,...} 6264 // Convert these attributes to lowercase, too 6265 // this.attr[key] = {}; 6266 // for (el in value) { 6267 // if (value.hasOwnProperty(el)) { 6268 // this.attr[key][el.toLocaleLowerCase()] = value[el]; 6269 // } 6270 // } 6271 Type.mergeAttr(this.attr[key], value); 6272 } else { 6273 this.attr[key] = value; 6274 } 6275 }, 6276 6277 /** 6278 * Sets an arbitrary number of attributes. This method has one or more 6279 * parameters of the following types: 6280 * <ul> 6281 * <li> object: {key1:value1,key2:value2,...} 6282 * <li> string: 'key:value' 6283 * <li> array: ['key', value] 6284 * </ul> 6285 * Some board attributes are immutable, like e.g. the renderer type. 6286 * 6287 * @param {Object} attributes An object with attributes. 6288 * @returns {JXG.Board} Reference to the board 6289 * 6290 * @example 6291 * const board = JXG.JSXGraph.initBoard('jxgbox', { 6292 * boundingbox: [-5, 5, 5, -5], 6293 * keepAspectRatio: false, 6294 * axis:true, 6295 * showFullscreen: true, 6296 * showScreenshot: true, 6297 * showCopyright: false 6298 * }); 6299 * 6300 * board.setAttribute({ 6301 * animationDelay: 10, 6302 * boundingbox: [-10, 5, 10, -5], 6303 * defaultAxes: { 6304 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6305 * }, 6306 * description: 'test', 6307 * fullscreen: { 6308 * scale: 0.5 6309 * }, 6310 * intl: { 6311 * enabled: true, 6312 * locale: 'de-DE' 6313 * } 6314 * }); 6315 * 6316 * board.setAttribute({ 6317 * selection: { 6318 * enabled: true, 6319 * fillColor: 'blue' 6320 * }, 6321 * showInfobox: false, 6322 * zoomX: 0.5, 6323 * zoomY: 2, 6324 * fullscreen: { symbol: 'x' }, 6325 * screenshot: { symbol: 'y' }, 6326 * showCopyright: true, 6327 * showFullscreen: false, 6328 * showScreenshot: false, 6329 * showZoom: false, 6330 * showNavigation: false 6331 * }); 6332 * board.setAttribute('showCopyright:false'); 6333 * 6334 * var p = board.create('point', [1, 1], {size: 10, 6335 * label: { 6336 * fontSize: 24, 6337 * highlightStrokeOpacity: 0.1, 6338 * offset: [5, 0] 6339 * } 6340 * }); 6341 * 6342 * 6343 * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div> 6344 * <script type="text/javascript"> 6345 * (function() { 6346 * const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', { 6347 * boundingbox: [-5, 5, 5, -5], 6348 * keepAspectRatio: false, 6349 * axis:true, 6350 * showFullscreen: true, 6351 * showScreenshot: true, 6352 * showCopyright: false 6353 * }); 6354 * 6355 * board.setAttribute({ 6356 * animationDelay: 10, 6357 * boundingbox: [-10, 5, 10, -5], 6358 * defaultAxes: { 6359 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6360 * }, 6361 * description: 'test', 6362 * fullscreen: { 6363 * scale: 0.5 6364 * }, 6365 * intl: { 6366 * enabled: true, 6367 * locale: 'de-DE' 6368 * } 6369 * }); 6370 * 6371 * board.setAttribute({ 6372 * selection: { 6373 * enabled: true, 6374 * fillColor: 'blue' 6375 * }, 6376 * showInfobox: false, 6377 * zoomX: 0.5, 6378 * zoomY: 2, 6379 * fullscreen: { symbol: 'x' }, 6380 * screenshot: { symbol: 'y' }, 6381 * showCopyright: true, 6382 * showFullscreen: false, 6383 * showScreenshot: false, 6384 * showZoom: false, 6385 * showNavigation: false 6386 * }); 6387 * 6388 * board.setAttribute('showCopyright:false'); 6389 * 6390 * var p = board.create('point', [1, 1], {size: 10, 6391 * label: { 6392 * fontSize: 24, 6393 * highlightStrokeOpacity: 0.1, 6394 * offset: [5, 0] 6395 * } 6396 * }); 6397 * 6398 * 6399 * })(); 6400 * 6401 * </script><pre> 6402 * 6403 * 6404 */ 6405 setAttribute: function (attr) { 6406 var i, arg, pair, 6407 key, value, oldvalue,// j, le, 6408 node, 6409 attributes = {}; 6410 6411 // Normalize the user input 6412 for (i = 0; i < arguments.length; i++) { 6413 arg = arguments[i]; 6414 if (Type.isString(arg)) { 6415 // pairRaw is string of the form 'key:value' 6416 pair = arg.split(":"); 6417 attributes[Type.trim(pair[0])] = Type.trim(pair[1]); 6418 } else if (!Type.isArray(arg)) { 6419 // pairRaw consists of objects of the form {key1:value1,key2:value2,...} 6420 JXG.extend(attributes, arg); 6421 } else { 6422 // pairRaw consists of array [key,value] 6423 attributes[arg[0]] = arg[1]; 6424 } 6425 } 6426 6427 for (i in attributes) { 6428 if (attributes.hasOwnProperty(i)) { 6429 key = i.replace(/\s+/g, "").toLowerCase(); 6430 value = attributes[i]; 6431 } 6432 value = (value.toLowerCase && value.toLowerCase() === 'false') 6433 ? false 6434 : value; 6435 6436 oldvalue = this.attr[key]; 6437 if (oldvalue === value) { 6438 continue; 6439 } 6440 switch (key) { 6441 case 'axis': 6442 if (value === false) { 6443 if (Type.exists(this.defaultAxes)) { 6444 this.defaultAxes.x.setAttribute({ visible: false }); 6445 this.defaultAxes.y.setAttribute({ visible: false }); 6446 } 6447 } else { 6448 // TODO 6449 } 6450 break; 6451 case 'boundingbox': 6452 this.setBoundingBox(value, this.keepaspectratio); 6453 this._set(key, value); 6454 break; 6455 case 'defaultaxes': 6456 if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) { 6457 this.defaultAxes.x.setAttribute(value.x); 6458 } 6459 if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) { 6460 this.defaultAxes.y.setAttribute(value.y); 6461 } 6462 break; 6463 case 'description': 6464 this.document.getElementById(this.container + '_ARIAdescription') 6465 .innerHTML = value; 6466 this._set(key, value); 6467 break; 6468 case 'title': 6469 this.document.getElementById(this.container + '_ARIAlabel') 6470 .innerHTML = value; 6471 this._set(key, value); 6472 break; 6473 case 'keepaspectratio': 6474 this._set(key, value); 6475 this.setBoundingBox(this.getBoundingBox(), value, 'keep'); 6476 break; 6477 6478 /* eslint-disable no-fallthrough */ 6479 case 'document': 6480 case 'maxboundingbox': 6481 this[key] = value; 6482 this._set(key, value); 6483 break; 6484 6485 case 'zoomx': 6486 case 'zoomy': 6487 this[key] = value; 6488 this._set(key, value); 6489 this.setZoom(this.attr.zoomx, this.attr.zoomy); 6490 break; 6491 6492 case 'registerevents': 6493 case 'renderer': 6494 // immutable, i.e. ignored 6495 break; 6496 6497 case 'fullscreen': 6498 case 'screenshot': 6499 node = this.containerObj.ownerDocument.getElementById( 6500 this.container + '_navigation_' + key); 6501 if (node && Type.exists(value.symbol)) { 6502 node.innerHTML = Type.evaluate(value.symbol); 6503 } 6504 this._set(key, value); 6505 break; 6506 6507 case 'selection': 6508 value.visible = false; 6509 value.withLines = false; 6510 value.vertices = { visible: false }; 6511 this._set(key, value); 6512 break; 6513 6514 case 'showcopyright': 6515 if (this.renderer.type === 'svg') { 6516 node = this.containerObj.ownerDocument.getElementById( 6517 this.renderer.uniqName('licenseText') 6518 ); 6519 if (node) { 6520 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6521 } else if (Type.evaluate(value)) { 6522 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 6523 } 6524 } 6525 6526 default: 6527 if (Type.exists(this.attr[key])) { 6528 this._set(key, value); 6529 } 6530 break; 6531 /* eslint-enable no-fallthrough */ 6532 } 6533 } 6534 6535 // Redraw navbar to handle the remaining show* attributes 6536 this.containerObj.ownerDocument.getElementById( 6537 this.container + "_navigationbar" 6538 ).remove(); 6539 this.renderer.drawNavigationBar(this, this.attr.navbar); 6540 6541 this.triggerEventHandlers(["attribute"], [attributes, this]); 6542 this.fullUpdate(); 6543 6544 return this; 6545 }, 6546 6547 /** 6548 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 6549 * animated elements. This function tells the board about new elements to animate. 6550 * @param {JXG.GeometryElement} element The element which is to be animated. 6551 * @returns {JXG.Board} Reference to the board 6552 */ 6553 addAnimation: function (element) { 6554 var that = this; 6555 6556 this.animationObjects[element.id] = element; 6557 6558 if (!this.animationIntervalCode) { 6559 this.animationIntervalCode = window.setInterval(function () { 6560 that.animate(); 6561 }, element.board.attr.animationdelay); 6562 } 6563 6564 return this; 6565 }, 6566 6567 /** 6568 * Cancels all running animations. 6569 * @returns {JXG.Board} Reference to the board 6570 */ 6571 stopAllAnimation: function () { 6572 var el; 6573 6574 for (el in this.animationObjects) { 6575 if ( 6576 this.animationObjects.hasOwnProperty(el) && 6577 Type.exists(this.animationObjects[el]) 6578 ) { 6579 this.animationObjects[el] = null; 6580 delete this.animationObjects[el]; 6581 } 6582 } 6583 6584 window.clearInterval(this.animationIntervalCode); 6585 delete this.animationIntervalCode; 6586 6587 return this; 6588 }, 6589 6590 /** 6591 * General purpose animation function. This currently only supports moving points from one place to another. This 6592 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 6593 * @returns {JXG.Board} Reference to the board 6594 */ 6595 animate: function () { 6596 var props, 6597 el, 6598 o, 6599 newCoords, 6600 r, 6601 p, 6602 c, 6603 cbtmp, 6604 count = 0, 6605 obj = null; 6606 6607 for (el in this.animationObjects) { 6608 if ( 6609 this.animationObjects.hasOwnProperty(el) && 6610 Type.exists(this.animationObjects[el]) 6611 ) { 6612 count += 1; 6613 o = this.animationObjects[el]; 6614 6615 if (o.animationPath) { 6616 if (Type.isFunction(o.animationPath)) { 6617 newCoords = o.animationPath( 6618 new Date().getTime() - o.animationStart 6619 ); 6620 } else { 6621 newCoords = o.animationPath.pop(); 6622 } 6623 6624 if ( 6625 !Type.exists(newCoords) || 6626 (!Type.isArray(newCoords) && isNaN(newCoords)) 6627 ) { 6628 delete o.animationPath; 6629 } else { 6630 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 6631 o.fullUpdate(); 6632 obj = o; 6633 } 6634 } 6635 if (o.animationData) { 6636 c = 0; 6637 6638 for (r in o.animationData) { 6639 if (o.animationData.hasOwnProperty(r)) { 6640 p = o.animationData[r].pop(); 6641 6642 if (!Type.exists(p)) { 6643 delete o.animationData[p]; 6644 } else { 6645 c += 1; 6646 props = {}; 6647 props[r] = p; 6648 o.setAttribute(props); 6649 } 6650 } 6651 } 6652 6653 if (c === 0) { 6654 delete o.animationData; 6655 } 6656 } 6657 6658 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 6659 this.animationObjects[el] = null; 6660 delete this.animationObjects[el]; 6661 6662 if (Type.exists(o.animationCallback)) { 6663 cbtmp = o.animationCallback; 6664 o.animationCallback = null; 6665 cbtmp(); 6666 } 6667 } 6668 } 6669 } 6670 6671 if (count === 0) { 6672 window.clearInterval(this.animationIntervalCode); 6673 delete this.animationIntervalCode; 6674 } else { 6675 this.update(obj); 6676 } 6677 6678 return this; 6679 }, 6680 6681 /** 6682 * Migrate the dependency properties of the point src 6683 * to the point dest and delete the point src. 6684 * For example, a circle around the point src 6685 * receives the new center dest. The old center src 6686 * will be deleted. 6687 * @param {JXG.Point} src Original point which will be deleted 6688 * @param {JXG.Point} dest New point with the dependencies of src. 6689 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 6690 * dest element. 6691 * @returns {JXG.Board} Reference to the board 6692 */ 6693 migratePoint: function (src, dest, copyName) { 6694 var child, 6695 childId, 6696 prop, 6697 found, 6698 i, 6699 srcLabelId, 6700 srcHasLabel = false; 6701 6702 src = this.select(src); 6703 dest = this.select(dest); 6704 6705 if (Type.exists(src.label)) { 6706 srcLabelId = src.label.id; 6707 srcHasLabel = true; 6708 this.removeObject(src.label); 6709 } 6710 6711 for (childId in src.childElements) { 6712 if (src.childElements.hasOwnProperty(childId)) { 6713 child = src.childElements[childId]; 6714 found = false; 6715 6716 for (prop in child) { 6717 if (child.hasOwnProperty(prop)) { 6718 if (child[prop] === src) { 6719 child[prop] = dest; 6720 found = true; 6721 } 6722 } 6723 } 6724 6725 if (found) { 6726 delete src.childElements[childId]; 6727 } 6728 6729 for (i = 0; i < child.parents.length; i++) { 6730 if (child.parents[i] === src.id) { 6731 child.parents[i] = dest.id; 6732 } 6733 } 6734 6735 dest.addChild(child); 6736 } 6737 } 6738 6739 // The destination object should receive the name 6740 // and the label of the originating (src) object 6741 if (copyName) { 6742 if (srcHasLabel) { 6743 delete dest.childElements[srcLabelId]; 6744 delete dest.descendants[srcLabelId]; 6745 } 6746 6747 if (dest.label) { 6748 this.removeObject(dest.label); 6749 } 6750 6751 delete this.elementsByName[dest.name]; 6752 dest.name = src.name; 6753 if (srcHasLabel) { 6754 dest.createLabel(); 6755 } 6756 } 6757 6758 this.removeObject(src); 6759 6760 if (Type.exists(dest.name) && dest.name !== '') { 6761 this.elementsByName[dest.name] = dest; 6762 } 6763 6764 this.fullUpdate(); 6765 6766 return this; 6767 }, 6768 6769 /** 6770 * Initializes color blindness simulation. 6771 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 6772 * @returns {JXG.Board} Reference to the board 6773 */ 6774 emulateColorblindness: function (deficiency) { 6775 var e, o; 6776 6777 if (!Type.exists(deficiency)) { 6778 deficiency = 'none'; 6779 } 6780 6781 if (this.currentCBDef === deficiency) { 6782 return this; 6783 } 6784 6785 for (e in this.objects) { 6786 if (this.objects.hasOwnProperty(e)) { 6787 o = this.objects[e]; 6788 6789 if (deficiency !== 'none') { 6790 if (this.currentCBDef === 'none') { 6791 // this could be accomplished by JXG.extend, too. But do not use 6792 // JXG.deepCopy as this could result in an infinite loop because in 6793 // visProp there could be geometry elements which contain the board which 6794 // contains all objects which contain board etc. 6795 o.visPropOriginal = { 6796 strokecolor: o.visProp.strokecolor, 6797 fillcolor: o.visProp.fillcolor, 6798 highlightstrokecolor: o.visProp.highlightstrokecolor, 6799 highlightfillcolor: o.visProp.highlightfillcolor 6800 }; 6801 } 6802 o.setAttribute({ 6803 strokecolor: Color.rgb2cb( 6804 Type.evaluate(o.visPropOriginal.strokecolor), 6805 deficiency 6806 ), 6807 fillcolor: Color.rgb2cb( 6808 Type.evaluate(o.visPropOriginal.fillcolor), 6809 deficiency 6810 ), 6811 highlightstrokecolor: Color.rgb2cb( 6812 Type.evaluate(o.visPropOriginal.highlightstrokecolor), 6813 deficiency 6814 ), 6815 highlightfillcolor: Color.rgb2cb( 6816 Type.evaluate(o.visPropOriginal.highlightfillcolor), 6817 deficiency 6818 ) 6819 }); 6820 } else if (Type.exists(o.visPropOriginal)) { 6821 JXG.extend(o.visProp, o.visPropOriginal); 6822 } 6823 } 6824 } 6825 this.currentCBDef = deficiency; 6826 this.update(); 6827 6828 return this; 6829 }, 6830 6831 /** 6832 * Select a single or multiple elements at once. 6833 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 6834 * be used as a filter to return multiple elements at once filtered by the properties of the object. 6835 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 6836 * The advanced filters consisting of objects or functions are ignored. 6837 * @returns {JXG.GeometryElement|JXG.Composition} 6838 * @example 6839 * // select the element with name A 6840 * board.select('A'); 6841 * 6842 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 6843 * board.select({ 6844 * strokeColor: 'red' 6845 * }); 6846 * 6847 * // select all points on or below the x axis and make them black. 6848 * board.select({ 6849 * elementClass: JXG.OBJECT_CLASS_POINT, 6850 * Y: function (v) { 6851 * return v <= 0; 6852 * } 6853 * }).setAttribute({color: 'black'}); 6854 * 6855 * // select all elements 6856 * board.select(function (el) { 6857 * return true; 6858 * }); 6859 */ 6860 select: function (str, onlyByIdOrName) { 6861 var flist, 6862 olist, 6863 i, 6864 l, 6865 s = str; 6866 6867 if (s === null) { 6868 return s; 6869 } 6870 6871 // It's a string, most likely an id or a name. 6872 if (Type.isString(s) && s !== '') { 6873 // Search by ID 6874 if (Type.exists(this.objects[s])) { 6875 s = this.objects[s]; 6876 // Search by name 6877 } else if (Type.exists(this.elementsByName[s])) { 6878 s = this.elementsByName[s]; 6879 // Search by group ID 6880 } else if (Type.exists(this.groups[s])) { 6881 s = this.groups[s]; 6882 } 6883 6884 // It's a function or an object, but not an element 6885 } else if ( 6886 !onlyByIdOrName && 6887 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 6888 ) { 6889 flist = Type.filterElements(this.objectsList, s); 6890 6891 olist = {}; 6892 l = flist.length; 6893 for (i = 0; i < l; i++) { 6894 olist[flist[i].id] = flist[i]; 6895 } 6896 s = new Composition(olist); 6897 6898 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 6899 } else if ( 6900 Type.isObject(s) && 6901 Type.exists(s.id) && 6902 !Type.exists(this.objects[s.id]) 6903 ) { 6904 s = null; 6905 } 6906 6907 return s; 6908 }, 6909 6910 /** 6911 * Checks if the given point is inside the boundingbox. 6912 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 6913 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 6914 * @returns {Boolean} 6915 */ 6916 hasPoint: function (x, y) { 6917 var px = x, 6918 py = y, 6919 bbox = this.getBoundingBox(); 6920 6921 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 6922 px = x.usrCoords[1]; 6923 py = x.usrCoords[2]; 6924 } 6925 6926 return !!( 6927 Type.isNumber(px) && 6928 Type.isNumber(py) && 6929 bbox[0] < px && 6930 px < bbox[2] && 6931 bbox[1] > py && 6932 py > bbox[3] 6933 ); 6934 }, 6935 6936 /** 6937 * Update CSS transformations of type scaling. It is used to correct the mouse position 6938 * in {@link JXG.Board.getMousePosition}. 6939 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 6940 * 6941 * It is up to the user to call this method after an update of the CSS transformation 6942 * in the DOM. 6943 */ 6944 updateCSSTransforms: function () { 6945 var obj = this.containerObj, 6946 o = obj, 6947 o2 = obj; 6948 6949 this.cssTransMat = Env.getCSSTransformMatrix(o); 6950 6951 // Newer variant of walking up the tree. 6952 // We walk up all parent nodes and collect possible CSS transforms. 6953 // Works also for ShadowDOM 6954 if (Type.exists(o.getRootNode)) { 6955 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 6956 while (o) { 6957 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 6958 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 6959 } 6960 this.cssTransMat = Mat.inverse(this.cssTransMat); 6961 } else { 6962 /* 6963 * This is necessary for IE11 6964 */ 6965 o = o.offsetParent; 6966 while (o) { 6967 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 6968 6969 o2 = o2.parentNode; 6970 while (o2 !== o) { 6971 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 6972 o2 = o2.parentNode; 6973 } 6974 o = o.offsetParent; 6975 } 6976 this.cssTransMat = Mat.inverse(this.cssTransMat); 6977 } 6978 return this; 6979 }, 6980 6981 /** 6982 * Start selection mode. This function can either be triggered from outside or by 6983 * a down event together with correct key pressing. The default keys are 6984 * shift+ctrl. But this can be changed in the options. 6985 * 6986 * Starting from out side can be realized for example with a button like this: 6987 * <pre> 6988 * <button onclick='board.startSelectionMode()'>Start</button> 6989 * </pre> 6990 * @example 6991 * // 6992 * // Set a new bounding box from the selection rectangle 6993 * // 6994 * var board = JXG.JSXGraph.initBoard('jxgbox', { 6995 * boundingBox:[-3,2,3,-2], 6996 * keepAspectRatio: false, 6997 * axis:true, 6998 * selection: { 6999 * enabled: true, 7000 * needShift: false, 7001 * needCtrl: true, 7002 * withLines: false, 7003 * vertices: { 7004 * visible: false 7005 * }, 7006 * fillColor: '#ffff00', 7007 * } 7008 * }); 7009 * 7010 * var f = function f(x) { return Math.cos(x); }, 7011 * curve = board.create('functiongraph', [f]); 7012 * 7013 * board.on('stopselecting', function(){ 7014 * var box = board.stopSelectionMode(), 7015 * 7016 * // bbox has the coordinates of the selection rectangle. 7017 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7018 * // are homogeneous coordinates. 7019 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7020 * 7021 * // Set a new bounding box 7022 * board.setBoundingBox(bbox, false); 7023 * }); 7024 * 7025 * 7026 * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div> 7027 * <script type='text/javascript'> 7028 * (function() { 7029 * // 7030 * // Set a new bounding box from the selection rectangle 7031 * // 7032 * var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 7033 * boundingBox:[-3,2,3,-2], 7034 * keepAspectRatio: false, 7035 * axis:true, 7036 * selection: { 7037 * enabled: true, 7038 * needShift: false, 7039 * needCtrl: true, 7040 * withLines: false, 7041 * vertices: { 7042 * visible: false 7043 * }, 7044 * fillColor: '#ffff00', 7045 * } 7046 * }); 7047 * 7048 * var f = function f(x) { return Math.cos(x); }, 7049 * curve = board.create('functiongraph', [f]); 7050 * 7051 * board.on('stopselecting', function(){ 7052 * var box = board.stopSelectionMode(), 7053 * 7054 * // bbox has the coordinates of the selection rectangle. 7055 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7056 * // are homogeneous coordinates. 7057 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7058 * 7059 * // Set a new bounding box 7060 * board.setBoundingBox(bbox, false); 7061 * }); 7062 * })(); 7063 * 7064 * </script><pre> 7065 * 7066 */ 7067 startSelectionMode: function () { 7068 this.selectingMode = true; 7069 this.selectionPolygon.setAttribute({ visible: true }); 7070 this.selectingBox = [ 7071 [0, 0], 7072 [0, 0] 7073 ]; 7074 this._setSelectionPolygonFromBox(); 7075 this.selectionPolygon.fullUpdate(); 7076 }, 7077 7078 /** 7079 * Finalize the selection: disable selection mode and return the coordinates 7080 * of the selection rectangle. 7081 * @returns {Array} Coordinates of the selection rectangle. The array 7082 * contains two {@link JXG.Coords} objects. One the upper left corner and 7083 * the second for the lower right corner. 7084 */ 7085 stopSelectionMode: function () { 7086 this.selectingMode = false; 7087 this.selectionPolygon.setAttribute({ visible: false }); 7088 return [ 7089 this.selectionPolygon.vertices[0].coords, 7090 this.selectionPolygon.vertices[2].coords 7091 ]; 7092 }, 7093 7094 /** 7095 * Start the selection of a region. 7096 * @private 7097 * @param {Array} pos Screen coordiates of the upper left corner of the 7098 * selection rectangle. 7099 */ 7100 _startSelecting: function (pos) { 7101 this.isSelecting = true; 7102 this.selectingBox = [ 7103 [pos[0], pos[1]], 7104 [pos[0], pos[1]] 7105 ]; 7106 this._setSelectionPolygonFromBox(); 7107 }, 7108 7109 /** 7110 * Update the selection rectangle during a move event. 7111 * @private 7112 * @param {Array} pos Screen coordiates of the move event 7113 */ 7114 _moveSelecting: function (pos) { 7115 if (this.isSelecting) { 7116 this.selectingBox[1] = [pos[0], pos[1]]; 7117 this._setSelectionPolygonFromBox(); 7118 this.selectionPolygon.fullUpdate(); 7119 } 7120 }, 7121 7122 /** 7123 * Update the selection rectangle during an up event. Stop selection. 7124 * @private 7125 * @param {Object} evt Event object 7126 */ 7127 _stopSelecting: function (evt) { 7128 var pos = this.getMousePosition(evt); 7129 7130 this.isSelecting = false; 7131 this.selectingBox[1] = [pos[0], pos[1]]; 7132 this._setSelectionPolygonFromBox(); 7133 }, 7134 7135 /** 7136 * Update the Selection rectangle. 7137 * @private 7138 */ 7139 _setSelectionPolygonFromBox: function () { 7140 var A = this.selectingBox[0], 7141 B = this.selectingBox[1]; 7142 7143 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7144 A[0], 7145 A[1] 7146 ]); 7147 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7148 A[0], 7149 B[1] 7150 ]); 7151 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7152 B[0], 7153 B[1] 7154 ]); 7155 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7156 B[0], 7157 A[1] 7158 ]); 7159 }, 7160 7161 /** 7162 * Test if a down event should start a selection. Test if the 7163 * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called. 7164 * @param {Object} evt Event object 7165 */ 7166 _testForSelection: function (evt) { 7167 if (this._isRequiredKeyPressed(evt, 'selection')) { 7168 if (!Type.exists(this.selectionPolygon)) { 7169 this._createSelectionPolygon(this.attr); 7170 } 7171 this.startSelectionMode(); 7172 } 7173 }, 7174 7175 /** 7176 * Create the internal selection polygon, which will be available as board.selectionPolygon. 7177 * @private 7178 * @param {Object} attr board attributes, e.g. the subobject board.attr. 7179 * @returns {Object} pointer to the board to enable chaining. 7180 */ 7181 _createSelectionPolygon: function (attr) { 7182 var selectionattr; 7183 7184 if (!Type.exists(this.selectionPolygon)) { 7185 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 7186 if (selectionattr.enabled === true) { 7187 this.selectionPolygon = this.create( 7188 'polygon', 7189 [ 7190 [0, 0], 7191 [0, 0], 7192 [0, 0], 7193 [0, 0] 7194 ], 7195 selectionattr 7196 ); 7197 } 7198 } 7199 7200 return this; 7201 }, 7202 7203 /* ************************** 7204 * EVENT DEFINITION 7205 * for documentation purposes 7206 * ************************** */ 7207 7208 //region Event handler documentation 7209 7210 /** 7211 * @event 7212 * @description Whenever the {@link JXG.Board#setAttribute} is called. 7213 * @name JXG.Board#attribute 7214 * @param {Event} e The browser's event object. 7215 */ 7216 __evt__attribute: function (e) { }, 7217 7218 /** 7219 * @event 7220 * @description Whenever the user starts to touch or click the board. 7221 * @name JXG.Board#down 7222 * @param {Event} e The browser's event object. 7223 */ 7224 __evt__down: function (e) { }, 7225 7226 /** 7227 * @event 7228 * @description Whenever the user starts to click on the board. 7229 * @name JXG.Board#mousedown 7230 * @param {Event} e The browser's event object. 7231 */ 7232 __evt__mousedown: function (e) { }, 7233 7234 /** 7235 * @event 7236 * @description Whenever the user taps the pen on the board. 7237 * @name JXG.Board#pendown 7238 * @param {Event} e The browser's event object. 7239 */ 7240 __evt__pendown: function (e) { }, 7241 7242 /** 7243 * @event 7244 * @description Whenever the user starts to click on the board with a 7245 * device sending pointer events. 7246 * @name JXG.Board#pointerdown 7247 * @param {Event} e The browser's event object. 7248 */ 7249 __evt__pointerdown: function (e) { }, 7250 7251 /** 7252 * @event 7253 * @description Whenever the user starts to touch the board. 7254 * @name JXG.Board#touchstart 7255 * @param {Event} e The browser's event object. 7256 */ 7257 __evt__touchstart: function (e) { }, 7258 7259 /** 7260 * @event 7261 * @description Whenever the user stops to touch or click the board. 7262 * @name JXG.Board#up 7263 * @param {Event} e The browser's event object. 7264 */ 7265 __evt__up: function (e) { }, 7266 7267 /** 7268 * @event 7269 * @description Whenever the user releases the mousebutton over the board. 7270 * @name JXG.Board#mouseup 7271 * @param {Event} e The browser's event object. 7272 */ 7273 __evt__mouseup: function (e) { }, 7274 7275 /** 7276 * @event 7277 * @description Whenever the user releases the mousebutton over the board with a 7278 * device sending pointer events. 7279 * @name JXG.Board#pointerup 7280 * @param {Event} e The browser's event object. 7281 */ 7282 __evt__pointerup: function (e) { }, 7283 7284 /** 7285 * @event 7286 * @description Whenever the user stops touching the board. 7287 * @name JXG.Board#touchend 7288 * @param {Event} e The browser's event object. 7289 */ 7290 __evt__touchend: function (e) { }, 7291 7292 /** 7293 * @event 7294 * @description Whenever the user clicks on the board. 7295 * @name JXG.Board#click 7296 * @see JXG.Board#clickDelay 7297 * @param {Event} e The browser's event object. 7298 */ 7299 __evt__click: function (e) { }, 7300 7301 /** 7302 * @event 7303 * @description Whenever the user double clicks on the board. 7304 * This event works on desktop browser, but is undefined 7305 * on mobile browsers. 7306 * @name JXG.Board#dblclick 7307 * @see JXG.Board#clickDelay 7308 * @see JXG.Board#dblClickSuppressClick 7309 * @param {Event} e The browser's event object. 7310 */ 7311 __evt__dblclick: function (e) { }, 7312 7313 /** 7314 * @event 7315 * @description Whenever the user clicks on the board with a mouse device. 7316 * @name JXG.Board#mouseclick 7317 * @param {Event} e The browser's event object. 7318 */ 7319 __evt__mouseclick: function (e) { }, 7320 7321 /** 7322 * @event 7323 * @description Whenever the user double clicks on the board with a mouse device. 7324 * @name JXG.Board#mousedblclick 7325 * @see JXG.Board#clickDelay 7326 * @param {Event} e The browser's event object. 7327 */ 7328 __evt__mousedblclick: function (e) { }, 7329 7330 /** 7331 * @event 7332 * @description Whenever the user clicks on the board with a pointer device. 7333 * @name JXG.Board#pointerclick 7334 * @param {Event} e The browser's event object. 7335 */ 7336 __evt__pointerclick: function (e) { }, 7337 7338 /** 7339 * @event 7340 * @description Whenever the user double clicks on the board with a pointer device. 7341 * This event works on desktop browser, but is undefined 7342 * on mobile browsers. 7343 * @name JXG.Board#pointerdblclick 7344 * @see JXG.Board#clickDelay 7345 * @param {Event} e The browser's event object. 7346 */ 7347 __evt__pointerdblclick: function (e) { }, 7348 7349 /** 7350 * @event 7351 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 7352 * @name JXG.Board#move 7353 * @param {Event} e The browser's event object. 7354 * @param {Number} mode The mode the board currently is in 7355 * @see JXG.Board#mode 7356 */ 7357 __evt__move: function (e, mode) { }, 7358 7359 /** 7360 * @event 7361 * @description This event is fired whenever the user is moving the mouse over the board. 7362 * @name JXG.Board#mousemove 7363 * @param {Event} e The browser's event object. 7364 * @param {Number} mode The mode the board currently is in 7365 * @see JXG.Board#mode 7366 */ 7367 __evt__mousemove: function (e, mode) { }, 7368 7369 /** 7370 * @event 7371 * @description This event is fired whenever the user is moving the pen over the board. 7372 * @name JXG.Board#penmove 7373 * @param {Event} e The browser's event object. 7374 * @param {Number} mode The mode the board currently is in 7375 * @see JXG.Board#mode 7376 */ 7377 __evt__penmove: function (e, mode) { }, 7378 7379 /** 7380 * @event 7381 * @description This event is fired whenever the user is moving the mouse over the board with a 7382 * device sending pointer events. 7383 * @name JXG.Board#pointermove 7384 * @param {Event} e The browser's event object. 7385 * @param {Number} mode The mode the board currently is in 7386 * @see JXG.Board#mode 7387 */ 7388 __evt__pointermove: function (e, mode) { }, 7389 7390 /** 7391 * @event 7392 * @description This event is fired whenever the user is moving the finger over the board. 7393 * @name JXG.Board#touchmove 7394 * @param {Event} e The browser's event object. 7395 * @param {Number} mode The mode the board currently is in 7396 * @see JXG.Board#mode 7397 */ 7398 __evt__touchmove: function (e, mode) { }, 7399 7400 /** 7401 * @event 7402 * @description This event is fired whenever the user is moving an element over the board by 7403 * pressing arrow keys on a keyboard. 7404 * @name JXG.Board#keymove 7405 * @param {Event} e The browser's event object. 7406 * @param {Number} mode The mode the board currently is in 7407 * @see JXG.Board#mode 7408 */ 7409 __evt__keymove: function (e, mode) { }, 7410 7411 /** 7412 * @event 7413 * @description Whenever an element is highlighted this event is fired. 7414 * @name JXG.Board#hit 7415 * @param {Event} e The browser's event object. 7416 * @param {JXG.GeometryElement} el The hit element. 7417 * @param target 7418 * 7419 * @example 7420 * var c = board.create('circle', [[1, 1], 2]); 7421 * board.on('hit', function(evt, el) { 7422 * console.log('Hit element', el); 7423 * }); 7424 * 7425 * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7426 * <script type='text/javascript'> 7427 * (function() { 7428 * var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723', 7429 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 7430 * var c = board.create('circle', [[1, 1], 2]); 7431 * board.on('hit', function(evt, el) { 7432 * console.log('Hit element', el); 7433 * }); 7434 * 7435 * })(); 7436 * 7437 * </script><pre> 7438 */ 7439 __evt__hit: function (e, el, target) { }, 7440 7441 /** 7442 * @event 7443 * @description Whenever an element is highlighted this event is fired. 7444 * @name JXG.Board#mousehit 7445 * @see JXG.Board#hit 7446 * @param {Event} e The browser's event object. 7447 * @param {JXG.GeometryElement} el The hit element. 7448 * @param target 7449 */ 7450 __evt__mousehit: function (e, el, target) { }, 7451 7452 /** 7453 * @event 7454 * @description This board is updated. 7455 * @name JXG.Board#update 7456 */ 7457 __evt__update: function () { }, 7458 7459 /** 7460 * @event 7461 * @description The bounding box of the board has changed. 7462 * @name JXG.Board#boundingbox 7463 */ 7464 __evt__boundingbox: function () { }, 7465 7466 /** 7467 * @event 7468 * @description Select a region is started during a down event or by calling 7469 * {@link JXG.Board.startSelectionMode} 7470 * @name JXG.Board#startselecting 7471 */ 7472 __evt__startselecting: function () { }, 7473 7474 /** 7475 * @event 7476 * @description Select a region is started during a down event 7477 * from a device sending mouse events or by calling 7478 * {@link JXG.Board.startSelectionMode}. 7479 * @name JXG.Board#mousestartselecting 7480 */ 7481 __evt__mousestartselecting: function () { }, 7482 7483 /** 7484 * @event 7485 * @description Select a region is started during a down event 7486 * from a device sending pointer events or by calling 7487 * {@link JXG.Board.startSelectionMode}. 7488 * @name JXG.Board#pointerstartselecting 7489 */ 7490 __evt__pointerstartselecting: function () { }, 7491 7492 /** 7493 * @event 7494 * @description Select a region is started during a down event 7495 * from a device sending touch events or by calling 7496 * {@link JXG.Board.startSelectionMode}. 7497 * @name JXG.Board#touchstartselecting 7498 */ 7499 __evt__touchstartselecting: function () { }, 7500 7501 /** 7502 * @event 7503 * @description Selection of a region is stopped during an up event. 7504 * @name JXG.Board#stopselecting 7505 */ 7506 __evt__stopselecting: function () { }, 7507 7508 /** 7509 * @event 7510 * @description Selection of a region is stopped during an up event 7511 * from a device sending mouse events. 7512 * @name JXG.Board#mousestopselecting 7513 */ 7514 __evt__mousestopselecting: function () { }, 7515 7516 /** 7517 * @event 7518 * @description Selection of a region is stopped during an up event 7519 * from a device sending pointer events. 7520 * @name JXG.Board#pointerstopselecting 7521 */ 7522 __evt__pointerstopselecting: function () { }, 7523 7524 /** 7525 * @event 7526 * @description Selection of a region is stopped during an up event 7527 * from a device sending touch events. 7528 * @name JXG.Board#touchstopselecting 7529 */ 7530 __evt__touchstopselecting: function () { }, 7531 7532 /** 7533 * @event 7534 * @description A move event while selecting of a region is active. 7535 * @name JXG.Board#moveselecting 7536 */ 7537 __evt__moveselecting: function () { }, 7538 7539 /** 7540 * @event 7541 * @description A move event while selecting of a region is active 7542 * from a device sending mouse events. 7543 * @name JXG.Board#mousemoveselecting 7544 */ 7545 __evt__mousemoveselecting: function () { }, 7546 7547 /** 7548 * @event 7549 * @description Select a region is started during a down event 7550 * from a device sending mouse events. 7551 * @name JXG.Board#pointermoveselecting 7552 */ 7553 __evt__pointermoveselecting: function () { }, 7554 7555 /** 7556 * @event 7557 * @description Select a region is started during a down event 7558 * from a device sending touch events. 7559 * @name JXG.Board#touchmoveselecting 7560 */ 7561 __evt__touchmoveselecting: function () { }, 7562 7563 /** 7564 * @ignore 7565 */ 7566 __evt: function () { }, 7567 7568 //endregion 7569 7570 /** 7571 * Expand the JSXGraph construction to fullscreen. 7572 * In order to preserve the proportions of the JSXGraph element, 7573 * a wrapper div is created which is set to fullscreen. 7574 * This function is called when fullscreen mode is triggered 7575 * <b>and</b> when it is closed. 7576 * <p> 7577 * The wrapping div has the CSS class 'jxgbox_wrap_private' which is 7578 * defined in the file 'jsxgraph.css' 7579 * <p> 7580 * This feature is not available on iPhones (as of December 2021). 7581 * 7582 * @param {String} id (Optional) id of the div element which is brought to fullscreen. 7583 * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick 7584 * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied. 7585 * 7586 * @return {JXG.Board} Reference to the board 7587 * 7588 * @example 7589 * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div> 7590 * <button onClick='board.toFullscreen()'>Fullscreen</button> 7591 * 7592 * <script language='Javascript' type='text/javascript'> 7593 * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]}); 7594 * var p = board.create('point', [0, 1]); 7595 * </script> 7596 * 7597 * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7598 * <script type='text/javascript'> 7599 * var board_d5bab8b6; 7600 * (function() { 7601 * var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723', 7602 * {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false}); 7603 * var p = board.create('point', [0, 1]); 7604 * board_d5bab8b6 = board; 7605 * })(); 7606 * </script> 7607 * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button> 7608 * <pre> 7609 * 7610 * @example 7611 * <div id='outer' style='max-width: 500px; margin: 0 auto;'> 7612 * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div> 7613 * </div> 7614 * <button onClick='board.toFullscreen('outer')'>Fullscreen</button> 7615 * 7616 * <script language='Javascript' type='text/javascript'> 7617 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7618 * axis:true, 7619 * boundingbox:[-5,5,5,-5], 7620 * fullscreen: { id: 'outer' }, 7621 * showFullscreen: true 7622 * }); 7623 * var p = board.create('point', [-2, 3], {}); 7624 * </script> 7625 * 7626 * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'> 7627 * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div> 7628 * </div> 7629 * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button> 7630 * <script type='text/javascript'> 7631 * var board_JXG7103f6be; 7632 * (function() { 7633 * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac', 7634 * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true, 7635 * showcopyright: false, shownavigation: false}); 7636 * var p = board.create('point', [-2, 3], {}); 7637 * board_JXG7103f6be = board; 7638 * })(); 7639 * 7640 * </script><pre> 7641 * 7642 * 7643 */ 7644 toFullscreen: function (id) { 7645 var wrap_id, 7646 wrap_node, 7647 inner_node, 7648 dim, 7649 doc = this.document, 7650 fullscreenElement; 7651 7652 id = id || this.container; 7653 this._fullscreen_inner_id = id; 7654 inner_node = doc.getElementById(id); 7655 wrap_id = 'fullscreenwrap_' + id; 7656 7657 if (!Type.exists(inner_node._cssFullscreenStore)) { 7658 // Store the actual, absolute size of the div 7659 // This is used in scaleJSXGraphDiv 7660 dim = this.containerObj.getBoundingClientRect(); 7661 inner_node._cssFullscreenStore = { 7662 w: dim.width, 7663 h: dim.height 7664 }; 7665 } 7666 7667 // Wrap a div around the JSXGraph div. 7668 // It is removed when fullscreen mode is closed. 7669 if (doc.getElementById(wrap_id)) { 7670 wrap_node = doc.getElementById(wrap_id); 7671 } else { 7672 wrap_node = document.createElement('div'); 7673 wrap_node.classList.add('JXG_wrap_private'); 7674 wrap_node.setAttribute('id', wrap_id); 7675 inner_node.parentNode.insertBefore(wrap_node, inner_node); 7676 wrap_node.appendChild(inner_node); 7677 } 7678 7679 // Trigger fullscreen mode 7680 wrap_node.requestFullscreen = 7681 wrap_node.requestFullscreen || 7682 wrap_node.webkitRequestFullscreen || 7683 wrap_node.mozRequestFullScreen || 7684 wrap_node.msRequestFullscreen; 7685 7686 if (doc.fullscreenElement !== undefined) { 7687 fullscreenElement = doc.fullscreenElement; 7688 } else if (doc.webkitFullscreenElement !== undefined) { 7689 fullscreenElement = doc.webkitFullscreenElement; 7690 } else { 7691 fullscreenElement = doc.msFullscreenElement; 7692 } 7693 7694 if (fullscreenElement === null) { 7695 // Start fullscreen mode 7696 if (wrap_node.requestFullscreen) { 7697 wrap_node.requestFullscreen(); 7698 this.startFullscreenResizeObserver(wrap_node); 7699 } 7700 } else { 7701 this.stopFullscreenResizeObserver(wrap_node); 7702 if (Type.exists(document.exitFullscreen)) { 7703 document.exitFullscreen(); 7704 } else if (Type.exists(document.webkitExitFullscreen)) { 7705 document.webkitExitFullscreen(); 7706 } 7707 } 7708 7709 return this; 7710 }, 7711 7712 /** 7713 * If fullscreen mode is toggled, the possible CSS transformations 7714 * which are applied to the JSXGraph canvas have to be reread. 7715 * Otherwise the position of upper left corner is wrongly interpreted. 7716 * 7717 * @param {Object} evt fullscreen event object (unused) 7718 */ 7719 fullscreenListener: function (evt) { 7720 var inner_id, 7721 inner_node, 7722 fullscreenElement, 7723 doc = this.document; 7724 7725 inner_id = this._fullscreen_inner_id; 7726 if (!Type.exists(inner_id)) { 7727 return; 7728 } 7729 7730 if (doc.fullscreenElement !== undefined) { 7731 fullscreenElement = doc.fullscreenElement; 7732 } else if (doc.webkitFullscreenElement !== undefined) { 7733 fullscreenElement = doc.webkitFullscreenElement; 7734 } else { 7735 fullscreenElement = doc.msFullscreenElement; 7736 } 7737 7738 inner_node = doc.getElementById(inner_id); 7739 // If full screen mode is started we have to remove CSS margin around the JSXGraph div. 7740 // Otherwise, the positioning of the fullscreen div will be false. 7741 // When leaving the fullscreen mode, the margin is put back in. 7742 if (fullscreenElement) { 7743 // Just entered fullscreen mode 7744 7745 // Store the original data. 7746 // Further, the CSS margin has to be removed when in fullscreen mode, 7747 // and must be restored later. 7748 // 7749 // Obsolete: 7750 // It is used in AbstractRenderer.updateText to restore the scaling matrix 7751 // which is removed by MathJax. 7752 inner_node._cssFullscreenStore.id = fullscreenElement.id; 7753 inner_node._cssFullscreenStore.isFullscreen = true; 7754 inner_node._cssFullscreenStore.margin = inner_node.style.margin; 7755 inner_node._cssFullscreenStore.width = inner_node.style.width; 7756 inner_node._cssFullscreenStore.height = inner_node.style.height; 7757 inner_node._cssFullscreenStore.transform = inner_node.style.transform; 7758 // Be sure to replace relative width / height units by absolute units 7759 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px'; 7760 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px'; 7761 inner_node.style.margin = ''; 7762 7763 // Do the shifting and scaling via CSS properties 7764 // We do this after fullscreen mode has been established to get the correct size 7765 // of the JSXGraph div. 7766 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 7767 Type.evaluate(this.attr.fullscreen.scale)); 7768 7769 // Clear this.doc.fullscreenElement, because Safari doesn't to it and 7770 // when leaving full screen mode it is still set. 7771 fullscreenElement = null; 7772 } else if (Type.exists(inner_node._cssFullscreenStore)) { 7773 // Just left the fullscreen mode 7774 7775 inner_node._cssFullscreenStore.isFullscreen = false; 7776 inner_node.style.margin = inner_node._cssFullscreenStore.margin; 7777 inner_node.style.width = inner_node._cssFullscreenStore.width; 7778 inner_node.style.height = inner_node._cssFullscreenStore.height; 7779 inner_node.style.transform = inner_node._cssFullscreenStore.transform; 7780 inner_node._cssFullscreenStore = null; 7781 7782 // Remove the wrapper div 7783 inner_node.parentElement.replaceWith(inner_node); 7784 } 7785 7786 this.updateCSSTransforms(); 7787 }, 7788 7789 /** 7790 * Start resize observer to handle 7791 * orientation changes in fullscreen mode. 7792 * 7793 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 7794 * around the JSXGraph div. 7795 * @returns {JXG.Board} Reference to the board 7796 * @private 7797 * @see JXG.Board#toFullscreen 7798 * 7799 */ 7800 startFullscreenResizeObserver: function(node) { 7801 var that = this; 7802 7803 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 7804 return this; 7805 } 7806 7807 this.resizeObserver = new ResizeObserver(function (entries) { 7808 var inner_id, 7809 fullscreenElement, 7810 doc = that.document; 7811 7812 if (!that._isResizing) { 7813 that._isResizing = true; 7814 window.setTimeout(function () { 7815 try { 7816 inner_id = that._fullscreen_inner_id; 7817 if (doc.fullscreenElement !== undefined) { 7818 fullscreenElement = doc.fullscreenElement; 7819 } else if (doc.webkitFullscreenElement !== undefined) { 7820 fullscreenElement = doc.webkitFullscreenElement; 7821 } else { 7822 fullscreenElement = doc.msFullscreenElement; 7823 } 7824 if (fullscreenElement !== null) { 7825 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 7826 Type.evaluate(that.attr.fullscreen.scale)); 7827 } 7828 } catch (err) { 7829 that.stopFullscreenResizeObserver(node); 7830 } finally { 7831 that._isResizing = false; 7832 } 7833 }, that.attr.resize.throttle); 7834 } 7835 }); 7836 this.resizeObserver.observe(node); 7837 return this; 7838 }, 7839 7840 /** 7841 * Remove resize observer to handle orientation changes in fullscreen mode. 7842 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 7843 * around the JSXGraph div. 7844 * @returns {JXG.Board} Reference to the board 7845 * @private 7846 * @see JXG.Board#toFullscreen 7847 */ 7848 stopFullscreenResizeObserver: function(node) { 7849 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 7850 return this; 7851 } 7852 7853 if (Type.exists(this.resizeObserver)) { 7854 this.resizeObserver.unobserve(node); 7855 } 7856 return this; 7857 }, 7858 7859 /** 7860 * Add user activity to the array 'board.userLog'. 7861 * 7862 * @param {String} type Event type, e.g. 'drag' 7863 * @param {Object} obj JSXGraph element object 7864 * 7865 * @see JXG.Board#userLog 7866 * @return {JXG.Board} Reference to the board 7867 */ 7868 addLogEntry: function (type, obj, pos) { 7869 var t, id, 7870 last = this.userLog.length - 1; 7871 7872 if (Type.exists(obj.elementClass)) { 7873 id = obj.id; 7874 } 7875 if (Type.evaluate(this.attr.logging.enabled)) { 7876 t = (new Date()).getTime(); 7877 if (last >= 0 && 7878 this.userLog[last].type === type && 7879 this.userLog[last].id === id && 7880 // Distinguish consecutive drag events of 7881 // the same element 7882 t - this.userLog[last].end < 500) { 7883 7884 this.userLog[last].end = t; 7885 this.userLog[last].endpos = pos; 7886 } else { 7887 this.userLog.push({ 7888 type: type, 7889 id: id, 7890 start: t, 7891 startpos: pos, 7892 end: t, 7893 endpos: pos, 7894 bbox: this.getBoundingBox(), 7895 canvas: [this.canvasWidth, this.canvasHeight], 7896 zoom: [this.zoomX, this.zoomY] 7897 }); 7898 } 7899 } 7900 return this; 7901 }, 7902 7903 /** 7904 * Function to animate a curve rolling on another curve. 7905 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 7906 * @param {Curve} c2 JSXGraph curve which rolls on c1. 7907 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 7908 * rolling process 7909 * @param {Number} stepsize Increase in t in each step for the curve c1 7910 * @param {Number} direction 7911 * @param {Number} time Delay time for setInterval() 7912 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 7913 * all points which define c2 and gliders on c2. 7914 * 7915 * @example 7916 * 7917 * // Line which will be the floor to roll upon. 7918 * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 7919 * // Center of the rolling circle 7920 * var C = board.create('point',[0,2],{name:'C'}); 7921 * // Starting point of the rolling circle 7922 * var P = board.create('point',[0,1],{name:'P', trace:true}); 7923 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 7924 * var circle = board.create('curve',[ 7925 * function (t){var d = P.Dist(C), 7926 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 7927 * t += beta; 7928 * return C.X()+d*Math.cos(t); 7929 * }, 7930 * function (t){var d = P.Dist(C), 7931 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 7932 * t += beta; 7933 * return C.Y()+d*Math.sin(t); 7934 * }, 7935 * 0,2*Math.PI], 7936 * {strokeWidth:6, strokeColor:'green'}); 7937 * 7938 * // Point on circle 7939 * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 7940 * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 7941 * roll.start() // Start the rolling, to be stopped by roll.stop() 7942 * 7943 * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div> 7944 * <script type='text/javascript'> 7945 * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 7946 * // Line which will be the floor to roll upon. 7947 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 7948 * // Center of the rolling circle 7949 * var C = brd.create('point',[0,2],{name:'C'}); 7950 * // Starting point of the rolling circle 7951 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 7952 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 7953 * var circle = brd.create('curve',[ 7954 * function (t){var d = P.Dist(C), 7955 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 7956 * t += beta; 7957 * return C.X()+d*Math.cos(t); 7958 * }, 7959 * function (t){var d = P.Dist(C), 7960 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 7961 * t += beta; 7962 * return C.Y()+d*Math.sin(t); 7963 * }, 7964 * 0,2*Math.PI], 7965 * {strokeWidth:6, strokeColor:'green'}); 7966 * 7967 * // Point on circle 7968 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 7969 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 7970 * roll.start() // Start the rolling, to be stopped by roll.stop() 7971 * </script><pre> 7972 */ 7973 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 7974 var brd = this, 7975 Roulette = function () { 7976 var alpha = 0, 7977 Tx = 0, 7978 Ty = 0, 7979 t1 = start_c1, 7980 t2 = Numerics.root( 7981 function (t) { 7982 var c1x = c1.X(t1), 7983 c1y = c1.Y(t1), 7984 c2x = c2.X(t), 7985 c2y = c2.Y(t); 7986 7987 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 7988 }, 7989 [0, Math.PI * 2] 7990 ), 7991 t1_new = 0.0, 7992 t2_new = 0.0, 7993 c1dist, 7994 rotation = brd.create( 7995 'transform', 7996 [ 7997 function () { 7998 return alpha; 7999 } 8000 ], 8001 { type: 'rotate' } 8002 ), 8003 rotationLocal = brd.create( 8004 'transform', 8005 [ 8006 function () { 8007 return alpha; 8008 }, 8009 function () { 8010 return c1.X(t1); 8011 }, 8012 function () { 8013 return c1.Y(t1); 8014 } 8015 ], 8016 { type: 'rotate' } 8017 ), 8018 translate = brd.create( 8019 'transform', 8020 [ 8021 function () { 8022 return Tx; 8023 }, 8024 function () { 8025 return Ty; 8026 } 8027 ], 8028 { type: 'translate' } 8029 ), 8030 // arc length via Simpson's rule. 8031 arclen = function (c, a, b) { 8032 var cpxa = Numerics.D(c.X)(a), 8033 cpya = Numerics.D(c.Y)(a), 8034 cpxb = Numerics.D(c.X)(b), 8035 cpyb = Numerics.D(c.Y)(b), 8036 cpxab = Numerics.D(c.X)((a + b) * 0.5), 8037 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 8038 fa = Mat.hypot(cpxa, cpya), 8039 fb = Mat.hypot(cpxb, cpyb), 8040 fab = Mat.hypot(cpxab, cpyab); 8041 8042 return ((fa + 4 * fab + fb) * (b - a)) / 6; 8043 }, 8044 exactDist = function (t) { 8045 return c1dist - arclen(c2, t2, t); 8046 }, 8047 beta = Math.PI / 18, 8048 beta9 = beta * 9, 8049 interval = null; 8050 8051 this.rolling = function () { 8052 var h, g, hp, gp, z; 8053 8054 t1_new = t1 + direction * stepsize; 8055 8056 // arc length between c1(t1) and c1(t1_new) 8057 c1dist = arclen(c1, t1, t1_new); 8058 8059 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 8060 t2_new = Numerics.root(exactDist, t2); 8061 8062 // c1(t) as complex number 8063 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 8064 8065 // c2(t) as complex number 8066 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 8067 8068 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 8069 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 8070 8071 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 8072 z = Complex.C.div(hp, gp); 8073 8074 alpha = Math.atan2(z.imaginary, z.real); 8075 // Normalizing the quotient 8076 z.div(Complex.C.abs(z)); 8077 z.mult(g); 8078 Tx = h.real - z.real; 8079 8080 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 8081 Ty = h.imaginary - z.imaginary; 8082 8083 // -(10-90) degrees: make corners roll smoothly 8084 if (alpha < -beta && alpha > -beta9) { 8085 alpha = -beta; 8086 rotationLocal.applyOnce(pointlist); 8087 } else if (alpha > beta && alpha < beta9) { 8088 alpha = beta; 8089 rotationLocal.applyOnce(pointlist); 8090 } else { 8091 rotation.applyOnce(pointlist); 8092 translate.applyOnce(pointlist); 8093 t1 = t1_new; 8094 t2 = t2_new; 8095 } 8096 brd.update(); 8097 }; 8098 8099 this.start = function () { 8100 if (time > 0) { 8101 interval = window.setInterval(this.rolling, time); 8102 } 8103 return this; 8104 }; 8105 8106 this.stop = function () { 8107 window.clearInterval(interval); 8108 return this; 8109 }; 8110 return this; 8111 }; 8112 return new Roulette(); 8113 } 8114 } 8115 ); 8116 8117 export default JXG.Board; 8118