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