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