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