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