1 /* 2 Copyright 2008-2025 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/ 33 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods 38 * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc. 39 */ 40 41 import JXG from '../jxg.js'; 42 import Const from './constants.js'; 43 import Coords from './coords.js'; 44 import Options from '../options.js'; 45 import Numerics from '../math/numerics.js'; 46 import Mat from '../math/math.js'; 47 import Geometry from '../math/geometry.js'; 48 import Complex from '../math/complex.js'; 49 import Statistics from '../math/statistics.js'; 50 import JessieCode from '../parser/jessiecode.js'; 51 import Color from '../utils/color.js'; 52 import Type from '../utils/type.js'; 53 import EventEmitter from '../utils/event.js'; 54 import Env from '../utils/env.js'; 55 import Composition from './composition.js'; 56 57 /** 58 * Constructs a new Board object. 59 * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric 60 * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly. 61 * Please use {@link JXG.JSXGraph.initBoard} to initialize a board. 62 * @constructor 63 * @param {String|Object} container The id of or reference to the HTML DOM element 64 * the board is drawn in. This is usually a HTML div. If it is the reference to an HTML element and this element does not have an attribute "id", 65 * this attribute "id" is set to a random value. 66 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 67 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 68 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 69 * @param {Number} zoomX Zoom factor in x-axis direction 70 * @param {Number} zoomY Zoom factor in y-axis direction 71 * @param {Number} unitX Units in x-axis direction 72 * @param {Number} unitY Units in y-axis direction 73 * @param {Number} canvasWidth The width of canvas 74 * @param {Number} canvasHeight The height of canvas 75 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard} 76 * @borrows JXG.EventEmitter#on as this.on 77 * @borrows JXG.EventEmitter#off as this.off 78 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 79 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 80 */ 81 JXG.Board = function (container, renderer, id, 82 origin, zoomX, zoomY, unitX, unitY, 83 canvasWidth, canvasHeight, attributes) { 84 /** 85 * Board is in no special mode, objects are highlighted on mouse over and objects may be 86 * clicked to start drag&drop. 87 * @type Number 88 * @constant 89 */ 90 this.BOARD_MODE_NONE = 0x0000; 91 92 /** 93 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 94 * {@link JXG.Board#mouse} is updated on mouse movement. 95 * @type Number 96 * @constant 97 */ 98 this.BOARD_MODE_DRAG = 0x0001; 99 100 /** 101 * In this mode a mouse move changes the origin's screen coordinates. 102 * @type Number 103 * @constant 104 */ 105 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 106 107 /** 108 * Update is made with high quality, e.g. graphs are evaluated at much more points. 109 * @type Number 110 * @constant 111 * @see JXG.Board#updateQuality 112 */ 113 this.BOARD_MODE_ZOOM = 0x0011; 114 115 /** 116 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 117 * @type Number 118 * @constant 119 * @see JXG.Board#updateQuality 120 */ 121 this.BOARD_QUALITY_LOW = 0x1; 122 123 /** 124 * Update is made with high quality, e.g. graphs are evaluated at much more points. 125 * @type Number 126 * @constant 127 * @see JXG.Board#updateQuality 128 */ 129 this.BOARD_QUALITY_HIGH = 0x2; 130 131 /** 132 * Pointer to the document element containing the board. 133 * @type Object 134 */ 135 if (Type.exists(attributes.document) && attributes.document !== false) { 136 this.document = attributes.document; 137 } else if (Env.isBrowser) { 138 this.document = document; 139 } 140 141 /** 142 * The html-id of the html element containing the board. 143 * @type String 144 */ 145 this.container = ''; // container 146 147 /** 148 * ID of the board 149 * @type String 150 */ 151 this.id = ''; 152 153 /** 154 * Pointer to the html element containing the board. 155 * @type Object 156 */ 157 this.containerObj = null; // (Env.isBrowser ? this.document.getElementById(this.container) : null); 158 159 // Set this.container and this.containerObj 160 if (Type.isString(container)) { 161 // Hosting div is given as string 162 this.container = container; // container 163 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 164 165 } else if (Env.isBrowser) { 166 167 // Hosting div is given as object pointer 168 this.containerObj = container; 169 this.container = this.containerObj.getAttribute('id'); 170 if (this.container === null) { 171 // Set random ID to this.container, but not to the DOM element 172 173 this.container = 'null' + parseInt(Math.random() * 16777216).toString(); 174 } 175 } 176 177 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 178 throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.'); 179 } 180 181 // TODO 182 // Why do we need this.id AND this.container? 183 // There was never a board attribute "id". 184 // The origin seems to be that in the geonext renderer we use a separate id, extracted from the GEONExT file. 185 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 186 // If the given id is not valid, generate an unique id 187 this.id = id; 188 } else { 189 this.id = this.generateId(); 190 } 191 192 /** 193 * A reference to this boards renderer. 194 * @type JXG.AbstractRenderer 195 * @name JXG.Board#renderer 196 * @private 197 * @ignore 198 */ 199 this.renderer = renderer; 200 201 /** 202 * Grids keeps track of all grids attached to this board. 203 * @type Array 204 * @private 205 */ 206 this.grids = []; 207 208 /** 209 * Copy of the default options 210 * @type JXG.Options 211 */ 212 this.options = Type.deepCopy(Options); // A possible theme is not yet merged in 213 214 /** 215 * Board attributes 216 * @type Object 217 */ 218 this.attr = attributes; 219 220 if (this.attr.theme !== 'default' && Type.exists(JXG.themes[this.attr.theme])) { 221 Type.mergeAttr(this.options, JXG.themes[this.attr.theme], true); 222 } 223 224 /** 225 * Dimension of the board. 226 * @default 2 227 * @type Number 228 */ 229 this.dimension = 2; 230 this.jc = new JessieCode(); 231 this.jc.use(this); 232 233 /** 234 * Coordinates of the boards origin. This a object with the two properties 235 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 236 * stores the boards origin in homogeneous screen coordinates. 237 * @type Object 238 * @private 239 */ 240 this.origin = {}; 241 this.origin.usrCoords = [1, 0, 0]; 242 this.origin.scrCoords = [1, origin[0], origin[1]]; 243 244 /** 245 * Zoom factor in X direction. It only stores the zoom factor to be able 246 * to get back to 100% in zoom100(). 247 * @name JXG.Board.zoomX 248 * @type Number 249 * @private 250 * @ignore 251 */ 252 this.zoomX = zoomX; 253 254 /** 255 * Zoom factor in Y direction. It only stores the zoom factor to be able 256 * to get back to 100% in zoom100(). 257 * @name JXG.Board.zoomY 258 * @type Number 259 * @private 260 * @ignore 261 */ 262 this.zoomY = zoomY; 263 264 /** 265 * The number of pixels which represent one unit in user-coordinates in x direction. 266 * @type Number 267 * @private 268 */ 269 this.unitX = unitX * this.zoomX; 270 271 /** 272 * The number of pixels which represent one unit in user-coordinates in y direction. 273 * @type Number 274 * @private 275 */ 276 this.unitY = unitY * this.zoomY; 277 278 /** 279 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 280 * width/height ratio of the canvas. 281 * @type Boolean 282 * @private 283 */ 284 this.keepaspectratio = false; 285 286 /** 287 * Canvas width. 288 * @type Number 289 * @private 290 */ 291 this.canvasWidth = canvasWidth; 292 293 /** 294 * Canvas Height 295 * @type Number 296 * @private 297 */ 298 this.canvasHeight = canvasHeight; 299 300 EventEmitter.eventify(this); 301 302 this.hooks = []; 303 304 /** 305 * An array containing all other boards that are updated after this board has been updated. 306 * @type Array 307 * @see JXG.Board#addChild 308 * @see JXG.Board#removeChild 309 */ 310 this.dependentBoards = []; 311 312 /** 313 * During the update process this is set to false to prevent an endless loop. 314 * @default false 315 * @type Boolean 316 */ 317 this.inUpdate = false; 318 319 /** 320 * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object. 321 * @type Object 322 */ 323 this.objects = {}; 324 325 /** 326 * An array containing all geometric objects on the board in the order of construction. 327 * @type Array 328 */ 329 this.objectsList = []; 330 331 /** 332 * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object. 333 * @type Object 334 */ 335 this.groups = {}; 336 337 /** 338 * Stores all the objects that are currently running an animation. 339 * @type Object 340 */ 341 this.animationObjects = {}; 342 343 /** 344 * An associative array containing all highlighted elements belonging to the board. 345 * @type Object 346 */ 347 this.highlightedObjects = {}; 348 349 /** 350 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 351 * @type Number 352 */ 353 this.numObjects = 0; 354 355 /** 356 * An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object. 357 * @type Object 358 */ 359 this.elementsByName = {}; 360 361 /** 362 * The board mode the board is currently in. Possible values are 363 * <ul> 364 * <li>JXG.Board.BOARD_MODE_NONE</li> 365 * <li>JXG.Board.BOARD_MODE_DRAG</li> 366 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 367 * </ul> 368 * @type Number 369 */ 370 this.mode = this.BOARD_MODE_NONE; 371 372 /** 373 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 374 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 375 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 376 * evaluation points when plotting functions. Possible values are 377 * <ul> 378 * <li>BOARD_QUALITY_LOW</li> 379 * <li>BOARD_QUALITY_HIGH</li> 380 * </ul> 381 * @type Number 382 * @see JXG.Board#mode 383 */ 384 this.updateQuality = this.BOARD_QUALITY_HIGH; 385 386 /** 387 * If true updates are skipped. 388 * @type Boolean 389 */ 390 this.isSuspendedRedraw = false; 391 392 this.calculateSnapSizes(); 393 394 /** 395 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 396 * @type Number 397 * @see JXG.Board#drag_dy 398 */ 399 this.drag_dx = 0; 400 401 /** 402 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 403 * @type Number 404 * @see JXG.Board#drag_dx 405 */ 406 this.drag_dy = 0; 407 408 /** 409 * The last position where a drag event has been fired. 410 * @type Array 411 * @see JXG.Board#moveObject 412 */ 413 this.drag_position = [0, 0]; 414 415 /** 416 * References to the object that is dragged with the mouse on the board. 417 * @type JXG.GeometryElement 418 * @see JXG.Board#touches 419 */ 420 this.mouse = {}; 421 422 /** 423 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 424 * @type Array 425 * @see JXG.Board#mouse 426 */ 427 this.touches = []; 428 429 /** 430 * A string containing the XML text of the construction. 431 * This is set in {@link JXG.FileReader.parseString}. 432 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 433 * @type String 434 */ 435 this.xmlString = ''; 436 437 /** 438 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 439 * @type Array 440 */ 441 this.cPos = []; 442 443 /** 444 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 445 * touchStart because Android's Webkit browser fires too much of them. 446 * @type Number 447 */ 448 this.touchMoveLast = 0; 449 450 /** 451 * Contains the pointerId of the last touchMove event which was not thrown away or since 452 * touchStart because Android's Webkit browser fires too much of them. 453 * @type Number 454 */ 455 this.touchMoveLastId = Infinity; 456 457 /** 458 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 459 * @type Number 460 */ 461 this.positionAccessLast = 0; 462 463 /** 464 * Collects all elements that triggered a mouse down event. 465 * @type Array 466 */ 467 this.downObjects = []; 468 this.clickObjects = {}; 469 470 /** 471 * Collects all elements that have keyboard focus. Should be either one or no element. 472 * Elements are stored with their id. 473 * @type Array 474 */ 475 this.focusObjects = []; 476 477 if (this.attr.showcopyright) { 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) === true || Type.evaluate(this.attr.browserpan.enabled) === true) && 2899 // One-finger pan has priority over browserPan 2900 (Type.evaluate(this.attr.pan.enabled) === false || Type.evaluate(this.attr.pan.needtwofingers) === true) 2901 ) { 2902 // ta = 'pan-x pan-y'; // JSXGraph allows browser scrolling 2903 ta = 'auto'; // JSXGraph allows browser scrolling 2904 } 2905 this.containerObj.style.touchAction = ta; 2906 2907 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 2908 2909 return true; 2910 }, 2911 2912 /** 2913 * Internal handling of click events for pointers and mouse. 2914 * 2915 * @param {Event} evt The browsers event object. 2916 * @param {Array} evtArray list of event names 2917 * @private 2918 */ 2919 _handleClicks: function(evt, evtArray) { 2920 var that = this, 2921 el, delay, suppress; 2922 2923 if (this.selectingMode) { 2924 evt.stopPropagation(); 2925 return; 2926 } 2927 2928 delay = Type.evaluate(this.attr.clickdelay); 2929 suppress = Type.evaluate(this.attr.dblclicksuppressclick); 2930 2931 if (suppress) { 2932 // dblclick suppresses previous click events 2933 this._preventSingleClick = false; 2934 2935 // Wait if there is a dblclick event. 2936 // If not fire a click event 2937 this._singleClickTimer = setTimeout(function() { 2938 if (!that._preventSingleClick) { 2939 // Fire click event and remove element from click list 2940 that.triggerEventHandlers(evtArray, [evt]); 2941 for (el in that.clickObjects) { 2942 if (that.clickObjects.hasOwnProperty(el)) { 2943 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2944 delete that.clickObjects[el]; 2945 } 2946 } 2947 } 2948 }, delay); 2949 } else { 2950 // dblclick is preceded by two click events 2951 2952 // Fire click events 2953 that.triggerEventHandlers(evtArray, [evt]); 2954 for (el in that.clickObjects) { 2955 if (that.clickObjects.hasOwnProperty(el)) { 2956 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2957 } 2958 } 2959 2960 // Clear list of clicked elements with a delay 2961 setTimeout(function() { 2962 for (el in that.clickObjects) { 2963 if (that.clickObjects.hasOwnProperty(el)) { 2964 delete that.clickObjects[el]; 2965 } 2966 } 2967 }, delay); 2968 } 2969 evt.stopPropagation(); 2970 }, 2971 2972 /** 2973 * Internal handling of dblclick events for pointers and mouse. 2974 * 2975 * @param {Event} evt The browsers event object. 2976 * @param {Array} evtArray list of event names 2977 * @private 2978 */ 2979 _handleDblClicks: function(evt, evtArray) { 2980 var el; 2981 2982 if (this.selectingMode) { 2983 evt.stopPropagation(); 2984 return; 2985 } 2986 2987 // Notify that a dblclick has happened 2988 this._preventSingleClick = true; 2989 clearTimeout(this._singleClickTimer); 2990 2991 // Fire dblclick event 2992 this.triggerEventHandlers(evtArray, [evt]); 2993 for (el in this.clickObjects) { 2994 if (this.clickObjects.hasOwnProperty(el)) { 2995 this.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 2996 delete this.clickObjects[el]; 2997 } 2998 } 2999 3000 evt.stopPropagation(); 3001 }, 3002 3003 /** 3004 * This method is called by the browser when a pointer device clicks on the screen. 3005 * @param {Event} evt The browsers event object. 3006 */ 3007 pointerClickListener: function (evt) { 3008 this._handleClicks(evt, ['click', 'pointerclick']); 3009 }, 3010 3011 /** 3012 * This method is called by the browser when a pointer device double clicks on the screen. 3013 * @param {Event} evt The browsers event object. 3014 */ 3015 pointerDblClickListener: function (evt) { 3016 this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']); 3017 }, 3018 3019 /** 3020 * This method is called by the browser when the mouse device clicks on the screen. 3021 * @param {Event} evt The browsers event object. 3022 */ 3023 mouseClickListener: function (evt) { 3024 this._handleClicks(evt, ['click', 'mouseclick']); 3025 }, 3026 3027 /** 3028 * This method is called by the browser when the mouse device double clicks on the screen. 3029 * @param {Event} evt The browsers event object. 3030 */ 3031 mouseDblClickListener: function (evt) { 3032 this._handleDblClicks(evt, ['dblclick', 'mousedblclick']); 3033 }, 3034 3035 // /** 3036 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 3037 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 3038 // * to the border of the board, this pointerout event will be ignored. 3039 // * @param {Event} evt 3040 // * @return {Boolean} 3041 // */ 3042 // pointerOutListener: function (evt) { 3043 // if (evt.target === this.containerObj || 3044 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 3045 // this.pointerUpListener(evt); 3046 // } 3047 // return this.mode === this.BOARD_MODE_NONE; 3048 // }, 3049 3050 /** 3051 * Called periodically by the browser while the user moves a pointing device across the screen. 3052 * @param {Event} evt 3053 * @returns {Boolean} 3054 */ 3055 pointerMoveListener: function (evt) { 3056 var i, j, pos, eps, 3057 touchTargets, 3058 type = 'mouse'; // in case of no browser 3059 3060 if ( 3061 this._getPointerInputDevice(evt) === 'touch' && 3062 !this._isPointerRegistered(evt) 3063 ) { 3064 // Test, if there was a previous down event of this _getPointerId 3065 // (in case it is a touch event). 3066 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 3067 return this.BOARD_MODE_NONE; 3068 } 3069 3070 if (!this.checkFrameRate(evt)) { 3071 return false; 3072 } 3073 3074 if (this.mode !== this.BOARD_MODE_DRAG) { 3075 this.dehighlightAll(); 3076 this.displayInfobox(false); 3077 } 3078 3079 if (this.mode !== this.BOARD_MODE_NONE) { 3080 evt.preventDefault(); 3081 evt.stopPropagation(); 3082 } 3083 3084 this.updateQuality = this.BOARD_QUALITY_LOW; 3085 // Mouse, touch or pen device 3086 this._inputDevice = this._getPointerInputDevice(evt); 3087 type = this._inputDevice; 3088 this.options.precision.hasPoint = this.options.precision[type]; 3089 eps = this.options.precision.hasPoint * 0.3333; 3090 3091 pos = this.getMousePosition(evt); 3092 // Ignore pointer move event if too close at the border 3093 // and setPointerCapture is off 3094 if (Type.evaluate(this.attr.movetarget) === null && 3095 pos[0] <= eps || pos[1] <= eps || 3096 pos[0] >= this.canvasWidth - eps || 3097 pos[1] >= this.canvasHeight - eps 3098 ) { 3099 return this.mode === this.BOARD_MODE_NONE; 3100 } 3101 3102 // selection 3103 if (this.selectingMode) { 3104 this._moveSelecting(pos); 3105 this.triggerEventHandlers( 3106 ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], 3107 [evt, this.mode] 3108 ); 3109 } else if (!this.mouseOriginMove(evt)) { 3110 if (this.mode === this.BOARD_MODE_DRAG) { 3111 // Run through all jsxgraph elements which are touched by at least one finger. 3112 for (i = 0; i < this.touches.length; i++) { 3113 touchTargets = this.touches[i].targets; 3114 // Run through all touch events which have been started on this jsxgraph element. 3115 for (j = 0; j < touchTargets.length; j++) { 3116 if (touchTargets[j].num === evt.pointerId) { 3117 touchTargets[j].X = pos[0]; 3118 touchTargets[j].Y = pos[1]; 3119 3120 if (touchTargets.length === 1) { 3121 // Touch by one finger: this is possible for all elements that can be dragged 3122 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 3123 } else if (touchTargets.length === 2) { 3124 // Touch by two fingers: e.g. moving lines 3125 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 3126 3127 touchTargets[j].Xprev = pos[0]; 3128 touchTargets[j].Yprev = pos[1]; 3129 } 3130 3131 // There is only one pointer in the evt object, so there's no point in looking further 3132 break; 3133 } 3134 } 3135 } 3136 } else { 3137 if (this._getPointerInputDevice(evt) === 'touch') { 3138 this._pointerStorePosition(evt); 3139 3140 if (this._board_touches.length === 2) { 3141 evt.touches = this._board_touches; 3142 this.gestureChangeListener(evt); 3143 } 3144 } 3145 3146 // Move event without dragging an element 3147 this.highlightElements(pos[0], pos[1], evt, -1); 3148 } 3149 } 3150 3151 // Hiding the infobox is commented out, since it prevents showing the infobox 3152 // on IE 11+ on 'over' 3153 //if (this.mode !== this.BOARD_MODE_DRAG) { 3154 //this.displayInfobox(false); 3155 //} 3156 this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]); 3157 this.updateQuality = this.BOARD_QUALITY_HIGH; 3158 3159 return this.mode === this.BOARD_MODE_NONE; 3160 }, 3161 3162 /** 3163 * Triggered as soon as the user stops touching the device with at least one finger. 3164 * 3165 * @param {Event} evt 3166 * @returns {Boolean} 3167 */ 3168 pointerUpListener: function (evt) { 3169 var i, j, found, eh, 3170 touchTargets, 3171 updateNeeded = false; 3172 3173 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 3174 this.displayInfobox(false); 3175 3176 if (evt) { 3177 for (i = 0; i < this.touches.length; i++) { 3178 touchTargets = this.touches[i].targets; 3179 for (j = 0; j < touchTargets.length; j++) { 3180 if (touchTargets[j].num === evt.pointerId) { 3181 touchTargets.splice(j, 1); 3182 if (touchTargets.length === 0) { 3183 this.touches.splice(i, 1); 3184 } 3185 break; 3186 } 3187 } 3188 } 3189 } 3190 3191 this.originMoveEnd(); 3192 this.update(); 3193 3194 // selection 3195 if (this.selectingMode) { 3196 this._stopSelecting(evt); 3197 this.triggerEventHandlers( 3198 ['touchstopselecting', 'pointerstopselecting', 'stopselecting'], 3199 [evt] 3200 ); 3201 this.stopSelectionMode(); 3202 } else { 3203 for (i = this.downObjects.length - 1; i > -1; i--) { 3204 found = false; 3205 for (j = 0; j < this.touches.length; j++) { 3206 if (this.touches[j].obj.id === this.downObjects[i].id) { 3207 found = true; 3208 } 3209 } 3210 if (!found) { 3211 this.downObjects[i].triggerEventHandlers( 3212 ['touchend', 'up', 'pointerup', 'MSPointerUp'], 3213 [evt] 3214 ); 3215 if (!Type.exists(this.downObjects[i].coords)) { 3216 // snapTo methods have to be called e.g. for line elements here. 3217 // For coordsElements there might be a conflict with 3218 // attractors, see commit from 2022.04.08, 11:12:18. 3219 this.downObjects[i].snapToGrid(); 3220 this.downObjects[i].snapToPoints(); 3221 updateNeeded = true; 3222 } 3223 3224 // Check if we have to keep the element for a click or dblclick event 3225 // Otherwise remove it from downObjects 3226 eh = this.downObjects[i].eventHandlers; 3227 if ((Type.exists(eh.click) && eh.click.length > 0) || 3228 (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) || 3229 (Type.exists(eh.dblclick) && eh.dblclick.length > 0) || 3230 (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0) 3231 ) { 3232 this.clickObjects[this.downObjects[i].id] = this.downObjects[i]; 3233 } 3234 this.downObjects.splice(i, 1); 3235 } 3236 } 3237 } 3238 3239 if (this.hasPointerUp) { 3240 if (window.navigator.msPointerEnabled) { 3241 // IE10- 3242 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 3243 } else { 3244 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 3245 Env.removeEvent( 3246 this.document, 3247 'pointercancel', 3248 this.pointerUpListener, 3249 this 3250 ); 3251 } 3252 this.hasPointerUp = false; 3253 } 3254 3255 // After one finger leaves the screen the gesture is stopped. 3256 this._pointerClearTouches(evt.pointerId); 3257 if (this._getPointerInputDevice(evt) !== 'touch') { 3258 this.dehighlightAll(); 3259 } 3260 3261 if (updateNeeded) { 3262 this.update(); 3263 } 3264 3265 return true; 3266 }, 3267 3268 /** 3269 * Triggered by the pointerleave event. This is needed in addition to 3270 * {@link JXG.Board#pointerUpListener} in the situation that a pen is used 3271 * and after an up event the pen leaves the hover range vertically. Here, it happens that 3272 * after the pointerup event further pointermove events are fired and elements get highlighted. 3273 * This highlighting has to be cancelled. 3274 * 3275 * @param {Event} evt 3276 * @returns {Boolean} 3277 */ 3278 pointerLeaveListener: function (evt) { 3279 this.displayInfobox(false); 3280 this.dehighlightAll(); 3281 3282 return true; 3283 }, 3284 3285 /** 3286 * Touch-Events 3287 */ 3288 3289 /** 3290 * This method is called by the browser when a finger touches the surface of the touch-device. 3291 * @param {Event} evt The browsers event object. 3292 * @returns {Boolean} ... 3293 */ 3294 touchStartListener: function (evt) { 3295 var i, j, k, 3296 pos, elements, obj, 3297 eps = this.options.precision.touch, 3298 evtTouches = evt['touches'], 3299 found, 3300 targets, target, 3301 touchTargets; 3302 3303 if (!this.hasTouchEnd) { 3304 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 3305 this.hasTouchEnd = true; 3306 } 3307 3308 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 3309 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 3310 3311 // prevent accidental selection of text 3312 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3313 this.document.selection.empty(); 3314 } else if (window.getSelection) { 3315 window.getSelection().removeAllRanges(); 3316 } 3317 3318 // multitouch 3319 this._inputDevice = 'touch'; 3320 this.options.precision.hasPoint = this.options.precision.touch; 3321 3322 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 3323 // 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 3324 // 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 3325 // 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 3326 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 3327 // * points have higher priority over other elements. 3328 // * 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 3329 // this element and add them. 3330 // ADDENDUM 11/10/11: 3331 // (1) run through the touches control object, 3332 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 3333 // for every target in our touches objects 3334 // (3) if one of the targettouches was bound to a touches targets array, mark it 3335 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 3336 // (a) if no element could be found: mark the target touches and continue 3337 // --- in the following cases, 'init' means: 3338 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 3339 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 3340 // (b) if the element is a point, init 3341 // (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 3342 // (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 3343 // add both to the touches array and mark them. 3344 for (i = 0; i < evtTouches.length; i++) { 3345 evtTouches[i].jxg_isused = false; 3346 } 3347 3348 for (i = 0; i < this.touches.length; i++) { 3349 touchTargets = this.touches[i].targets; 3350 for (j = 0; j < touchTargets.length; j++) { 3351 touchTargets[j].num = -1; 3352 eps = this.options.precision.touch; 3353 3354 do { 3355 for (k = 0; k < evtTouches.length; k++) { 3356 // find the new targettouches 3357 if ( 3358 Math.abs( 3359 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3360 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3361 ) < 3362 eps * eps 3363 ) { 3364 touchTargets[j].num = k; 3365 touchTargets[j].X = evtTouches[k].screenX; 3366 touchTargets[j].Y = evtTouches[k].screenY; 3367 evtTouches[k].jxg_isused = true; 3368 break; 3369 } 3370 } 3371 3372 eps *= 2; 3373 } while ( 3374 touchTargets[j].num === -1 && 3375 eps < this.options.precision.touchMax 3376 ); 3377 3378 if (touchTargets[j].num === -1) { 3379 JXG.debug( 3380 "i couldn't find a targettouches for target no " + 3381 j + 3382 ' on ' + 3383 this.touches[i].obj.name + 3384 ' (' + 3385 this.touches[i].obj.id + 3386 '). Removed the target.' 3387 ); 3388 JXG.debug( 3389 'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax 3390 ); 3391 touchTargets.splice(i, 1); 3392 } 3393 } 3394 } 3395 3396 // we just re-mapped the targettouches to our existing touches list. 3397 // now we have to initialize some touches from additional targettouches 3398 for (i = 0; i < evtTouches.length; i++) { 3399 if (!evtTouches[i].jxg_isused) { 3400 pos = this.getMousePosition(evt, i); 3401 // selection 3402 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 3403 if (this.selectingMode) { 3404 this._startSelecting(pos); 3405 this.triggerEventHandlers( 3406 ['touchstartselecting', 'startselecting'], 3407 [evt] 3408 ); 3409 evt.preventDefault(); 3410 evt.stopPropagation(); 3411 this.options.precision.hasPoint = this.options.precision.mouse; 3412 return this.touches.length > 0; // don't continue as a normal click 3413 } 3414 3415 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 3416 if (elements.length !== 0) { 3417 obj = elements[elements.length - 1]; 3418 target = { 3419 num: i, 3420 X: evtTouches[i].screenX, 3421 Y: evtTouches[i].screenY, 3422 Xprev: NaN, 3423 Yprev: NaN, 3424 Xstart: [], 3425 Ystart: [], 3426 Zstart: [] 3427 }; 3428 3429 if ( 3430 Type.isPoint(obj) || 3431 obj.elementClass === Const.OBJECT_CLASS_TEXT || 3432 obj.type === Const.OBJECT_TYPE_TICKS || 3433 obj.type === Const.OBJECT_TYPE_IMAGE 3434 ) { 3435 // It's a point, so it's single touch, so we just push it to our touches 3436 targets = [target]; 3437 3438 // For the UNDO/REDO of object moves 3439 this.saveStartPos(obj, targets[0]); 3440 3441 this.touches.push({ obj: obj, targets: targets }); 3442 obj.highlight(true); 3443 } else if ( 3444 obj.elementClass === Const.OBJECT_CLASS_LINE || 3445 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 3446 obj.elementClass === Const.OBJECT_CLASS_CURVE || 3447 obj.type === Const.OBJECT_TYPE_POLYGON 3448 ) { 3449 found = false; 3450 3451 // first check if this geometric object is already captured in this.touches 3452 for (j = 0; j < this.touches.length; j++) { 3453 if (obj.id === this.touches[j].obj.id) { 3454 found = true; 3455 // only add it, if we don't have two targets in there already 3456 if (this.touches[j].targets.length === 1) { 3457 // For the UNDO/REDO of object moves 3458 this.saveStartPos(obj, target); 3459 this.touches[j].targets.push(target); 3460 } 3461 3462 evtTouches[i].jxg_isused = true; 3463 } 3464 } 3465 3466 // we couldn't find it in touches, so we just init a new touches 3467 // IF there is a second touch targetting this line, we will find it later on, and then add it to 3468 // the touches control object. 3469 if (!found) { 3470 targets = [target]; 3471 3472 // For the UNDO/REDO of object moves 3473 this.saveStartPos(obj, targets[0]); 3474 this.touches.push({ obj: obj, targets: targets }); 3475 obj.highlight(true); 3476 } 3477 } 3478 } 3479 3480 evtTouches[i].jxg_isused = true; 3481 } 3482 } 3483 3484 if (this.touches.length > 0) { 3485 evt.preventDefault(); 3486 evt.stopPropagation(); 3487 } 3488 3489 // Touch events on empty areas of the board are handled here: 3490 // 1. case: one finger. If allowed, this triggers pan with one finger 3491 if ( 3492 evtTouches.length === 1 && 3493 this.mode === this.BOARD_MODE_NONE && 3494 this.touchStartMoveOriginOneFinger(evt) 3495 ) { 3496 } else if ( 3497 evtTouches.length === 2 && 3498 (this.mode === this.BOARD_MODE_NONE || 3499 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 3500 ) { 3501 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 3502 // This happens when the second finger hits the device. First, the 3503 // 'one finger pan mode' has to be cancelled. 3504 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 3505 this.originMoveEnd(); 3506 } 3507 this.gestureStartListener(evt); 3508 } 3509 3510 this.options.precision.hasPoint = this.options.precision.mouse; 3511 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 3512 3513 return false; 3514 //return this.touches.length > 0; 3515 }, 3516 3517 /** 3518 * Called periodically by the browser while the user moves his fingers across the device. 3519 * @param {Event} evt 3520 * @returns {Boolean} 3521 */ 3522 touchMoveListener: function (evt) { 3523 var i, 3524 pos1, 3525 pos2, 3526 touchTargets, 3527 evtTouches = evt['touches']; 3528 3529 if (!this.checkFrameRate(evt)) { 3530 return false; 3531 } 3532 3533 if (this.mode !== this.BOARD_MODE_NONE) { 3534 evt.preventDefault(); 3535 evt.stopPropagation(); 3536 } 3537 3538 if (this.mode !== this.BOARD_MODE_DRAG) { 3539 this.dehighlightAll(); 3540 this.displayInfobox(false); 3541 } 3542 3543 this._inputDevice = 'touch'; 3544 this.options.precision.hasPoint = this.options.precision.touch; 3545 this.updateQuality = this.BOARD_QUALITY_LOW; 3546 3547 // selection 3548 if (this.selectingMode) { 3549 for (i = 0; i < evtTouches.length; i++) { 3550 if (!evtTouches[i].jxg_isused) { 3551 pos1 = this.getMousePosition(evt, i); 3552 this._moveSelecting(pos1); 3553 this.triggerEventHandlers( 3554 ['touchmoves', 'moveselecting'], 3555 [evt, this.mode] 3556 ); 3557 break; 3558 } 3559 } 3560 } else { 3561 if (!this.touchOriginMove(evt)) { 3562 if (this.mode === this.BOARD_MODE_DRAG) { 3563 // Runs over through all elements which are touched 3564 // by at least one finger. 3565 for (i = 0; i < this.touches.length; i++) { 3566 touchTargets = this.touches[i].targets; 3567 if (touchTargets.length === 1) { 3568 // Touch by one finger: this is possible for all elements that can be dragged 3569 if (evtTouches[touchTargets[0].num]) { 3570 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3571 if ( 3572 pos1[0] < 0 || 3573 pos1[0] > this.canvasWidth || 3574 pos1[1] < 0 || 3575 pos1[1] > this.canvasHeight 3576 ) { 3577 return; 3578 } 3579 touchTargets[0].X = pos1[0]; 3580 touchTargets[0].Y = pos1[1]; 3581 this.moveObject( 3582 pos1[0], 3583 pos1[1], 3584 this.touches[i], 3585 evt, 3586 'touch' 3587 ); 3588 } 3589 } else if ( 3590 touchTargets.length === 2 && 3591 touchTargets[0].num > -1 && 3592 touchTargets[1].num > -1 3593 ) { 3594 // Touch by two fingers: moving lines, ... 3595 if ( 3596 evtTouches[touchTargets[0].num] && 3597 evtTouches[touchTargets[1].num] 3598 ) { 3599 // Get coordinates of the two touches 3600 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3601 pos2 = this.getMousePosition(evt, touchTargets[1].num); 3602 if ( 3603 pos1[0] < 0 || 3604 pos1[0] > this.canvasWidth || 3605 pos1[1] < 0 || 3606 pos1[1] > this.canvasHeight || 3607 pos2[0] < 0 || 3608 pos2[0] > this.canvasWidth || 3609 pos2[1] < 0 || 3610 pos2[1] > this.canvasHeight 3611 ) { 3612 return; 3613 } 3614 3615 touchTargets[0].X = pos1[0]; 3616 touchTargets[0].Y = pos1[1]; 3617 touchTargets[1].X = pos2[0]; 3618 touchTargets[1].Y = pos2[1]; 3619 3620 this.twoFingerMove( 3621 this.touches[i], 3622 touchTargets[0].num, 3623 evt 3624 ); 3625 3626 touchTargets[0].Xprev = pos1[0]; 3627 touchTargets[0].Yprev = pos1[1]; 3628 touchTargets[1].Xprev = pos2[0]; 3629 touchTargets[1].Yprev = pos2[1]; 3630 } 3631 } 3632 } 3633 } else { 3634 if (evtTouches.length === 2) { 3635 this.gestureChangeListener(evt); 3636 } 3637 // Move event without dragging an element 3638 pos1 = this.getMousePosition(evt, 0); 3639 this.highlightElements(pos1[0], pos1[1], evt, -1); 3640 } 3641 } 3642 } 3643 3644 if (this.mode !== this.BOARD_MODE_DRAG) { 3645 this.displayInfobox(false); 3646 } 3647 3648 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 3649 this.options.precision.hasPoint = this.options.precision.mouse; 3650 this.updateQuality = this.BOARD_QUALITY_HIGH; 3651 3652 return this.mode === this.BOARD_MODE_NONE; 3653 }, 3654 3655 /** 3656 * Triggered as soon as the user stops touching the device with at least one finger. 3657 * @param {Event} evt 3658 * @returns {Boolean} 3659 */ 3660 touchEndListener: function (evt) { 3661 var i, 3662 j, 3663 k, 3664 eps = this.options.precision.touch, 3665 tmpTouches = [], 3666 found, 3667 foundNumber, 3668 evtTouches = evt && evt['touches'], 3669 touchTargets, 3670 updateNeeded = false; 3671 3672 this.triggerEventHandlers(['touchend', 'up'], [evt]); 3673 this.displayInfobox(false); 3674 3675 // selection 3676 if (this.selectingMode) { 3677 this._stopSelecting(evt); 3678 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 3679 this.stopSelectionMode(); 3680 } else if (evtTouches && evtTouches.length > 0) { 3681 for (i = 0; i < this.touches.length; i++) { 3682 tmpTouches[i] = this.touches[i]; 3683 } 3684 this.touches.length = 0; 3685 3686 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 3687 // convert the operation to a simple one-finger-translation. 3688 // ADDENDUM 11/10/11: 3689 // see addendum to touchStartListener from 11/10/11 3690 // (1) run through the tmptouches 3691 // (2) check the touches.obj, if it is a 3692 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 3693 // (b) line with 3694 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 3695 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 3696 // (c) circle with [proceed like in line] 3697 3698 // init the targettouches marker 3699 for (i = 0; i < evtTouches.length; i++) { 3700 evtTouches[i].jxg_isused = false; 3701 } 3702 3703 for (i = 0; i < tmpTouches.length; i++) { 3704 // could all targets of the current this.touches.obj be assigned to targettouches? 3705 found = false; 3706 foundNumber = 0; 3707 touchTargets = tmpTouches[i].targets; 3708 3709 for (j = 0; j < touchTargets.length; j++) { 3710 touchTargets[j].found = false; 3711 for (k = 0; k < evtTouches.length; k++) { 3712 if ( 3713 Math.abs( 3714 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3715 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3716 ) < 3717 eps * eps 3718 ) { 3719 touchTargets[j].found = true; 3720 touchTargets[j].num = k; 3721 touchTargets[j].X = evtTouches[k].screenX; 3722 touchTargets[j].Y = evtTouches[k].screenY; 3723 foundNumber += 1; 3724 break; 3725 } 3726 } 3727 } 3728 3729 if (Type.isPoint(tmpTouches[i].obj)) { 3730 found = touchTargets[0] && touchTargets[0].found; 3731 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 3732 found = 3733 (touchTargets[0] && touchTargets[0].found) || 3734 (touchTargets[1] && touchTargets[1].found); 3735 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 3736 found = foundNumber === 1 || foundNumber === 3; 3737 } 3738 3739 // if we found this object to be still dragged by the user, add it back to this.touches 3740 if (found) { 3741 this.touches.push({ 3742 obj: tmpTouches[i].obj, 3743 targets: [] 3744 }); 3745 3746 for (j = 0; j < touchTargets.length; j++) { 3747 if (touchTargets[j].found) { 3748 this.touches[this.touches.length - 1].targets.push({ 3749 num: touchTargets[j].num, 3750 X: touchTargets[j].screenX, 3751 Y: touchTargets[j].screenY, 3752 Xprev: NaN, 3753 Yprev: NaN, 3754 Xstart: touchTargets[j].Xstart, 3755 Ystart: touchTargets[j].Ystart, 3756 Zstart: touchTargets[j].Zstart 3757 }); 3758 } 3759 } 3760 } else { 3761 tmpTouches[i].obj.noHighlight(); 3762 } 3763 } 3764 } else { 3765 this.touches.length = 0; 3766 } 3767 3768 for (i = this.downObjects.length - 1; i > -1; i--) { 3769 found = false; 3770 for (j = 0; j < this.touches.length; j++) { 3771 if (this.touches[j].obj.id === this.downObjects[i].id) { 3772 found = true; 3773 } 3774 } 3775 if (!found) { 3776 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 3777 if (!Type.exists(this.downObjects[i].coords)) { 3778 // snapTo methods have to be called e.g. for line elements here. 3779 // For coordsElements there might be a conflict with 3780 // attractors, see commit from 2022.04.08, 11:12:18. 3781 this.downObjects[i].snapToGrid(); 3782 this.downObjects[i].snapToPoints(); 3783 updateNeeded = true; 3784 } 3785 this.downObjects.splice(i, 1); 3786 } 3787 } 3788 3789 if (!evtTouches || evtTouches.length === 0) { 3790 if (this.hasTouchEnd) { 3791 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 3792 this.hasTouchEnd = false; 3793 } 3794 3795 this.dehighlightAll(); 3796 this.updateQuality = this.BOARD_QUALITY_HIGH; 3797 3798 this.originMoveEnd(); 3799 if (updateNeeded) { 3800 this.update(); 3801 } 3802 } 3803 3804 return true; 3805 }, 3806 3807 /** 3808 * This method is called by the browser when the mouse button is clicked. 3809 * @param {Event} evt The browsers event object. 3810 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 3811 */ 3812 mouseDownListener: function (evt) { 3813 var pos, elements, result; 3814 3815 // prevent accidental selection of text 3816 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3817 this.document.selection.empty(); 3818 } else if (window.getSelection) { 3819 window.getSelection().removeAllRanges(); 3820 } 3821 3822 if (!this.hasMouseUp) { 3823 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 3824 this.hasMouseUp = true; 3825 } else { 3826 // In case this.hasMouseUp==true, it may be that there was a 3827 // mousedown event before which was not followed by an mouseup event. 3828 // This seems to happen with interactive whiteboard pens sometimes. 3829 return; 3830 } 3831 3832 this._inputDevice = 'mouse'; 3833 this.options.precision.hasPoint = this.options.precision.mouse; 3834 pos = this.getMousePosition(evt); 3835 3836 // selection 3837 this._testForSelection(evt); 3838 if (this.selectingMode) { 3839 this._startSelecting(pos); 3840 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 3841 return; // don't continue as a normal click 3842 } 3843 3844 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 3845 3846 // if no draggable object can be found, get out here immediately 3847 if (elements.length === 0) { 3848 this.mode = this.BOARD_MODE_NONE; 3849 result = true; 3850 } else { 3851 this.mouse = { 3852 obj: null, 3853 targets: [ 3854 { 3855 X: pos[0], 3856 Y: pos[1], 3857 Xprev: NaN, 3858 Yprev: NaN 3859 } 3860 ] 3861 }; 3862 this.mouse.obj = elements[elements.length - 1]; 3863 3864 this.dehighlightAll(); 3865 this.mouse.obj.highlight(true); 3866 3867 this.mouse.targets[0].Xstart = []; 3868 this.mouse.targets[0].Ystart = []; 3869 this.mouse.targets[0].Zstart = []; 3870 3871 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 3872 3873 // prevent accidental text selection 3874 // this could get us new trouble: input fields, links and drop down boxes placed as text 3875 // on the board don't work anymore. 3876 if (evt && evt.preventDefault) { 3877 evt.preventDefault(); 3878 } else if (window.event) { 3879 window.event.returnValue = false; 3880 } 3881 } 3882 3883 if (this.mode === this.BOARD_MODE_NONE) { 3884 result = this.mouseOriginMoveStart(evt); 3885 } 3886 3887 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 3888 3889 return result; 3890 }, 3891 3892 /** 3893 * This method is called by the browser when the mouse is moved. 3894 * @param {Event} evt The browsers event object. 3895 */ 3896 mouseMoveListener: function (evt) { 3897 var pos; 3898 3899 if (!this.checkFrameRate(evt)) { 3900 return false; 3901 } 3902 3903 pos = this.getMousePosition(evt); 3904 3905 this.updateQuality = this.BOARD_QUALITY_LOW; 3906 3907 if (this.mode !== this.BOARD_MODE_DRAG) { 3908 this.dehighlightAll(); 3909 this.displayInfobox(false); 3910 } 3911 3912 // we have to check for four cases: 3913 // * user moves origin 3914 // * user drags an object 3915 // * user just moves the mouse, here highlight all elements at 3916 // the current mouse position 3917 // * the user is selecting 3918 3919 // selection 3920 if (this.selectingMode) { 3921 this._moveSelecting(pos); 3922 this.triggerEventHandlers( 3923 ['mousemoveselecting', 'moveselecting'], 3924 [evt, this.mode] 3925 ); 3926 } else if (!this.mouseOriginMove(evt)) { 3927 if (this.mode === this.BOARD_MODE_DRAG) { 3928 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 3929 } else { 3930 // BOARD_MODE_NONE 3931 // Move event without dragging an element 3932 this.highlightElements(pos[0], pos[1], evt, -1); 3933 } 3934 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 3935 } 3936 this.updateQuality = this.BOARD_QUALITY_HIGH; 3937 }, 3938 3939 /** 3940 * This method is called by the browser when the mouse button is released. 3941 * @param {Event} evt 3942 */ 3943 mouseUpListener: function (evt) { 3944 var i; 3945 3946 if (this.selectingMode === false) { 3947 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 3948 } 3949 3950 // redraw with high precision 3951 this.updateQuality = this.BOARD_QUALITY_HIGH; 3952 3953 if (this.mouse && this.mouse.obj) { 3954 if (!Type.exists(this.mouse.obj.coords)) { 3955 // snapTo methods have to be called e.g. for line elements here. 3956 // For coordsElements there might be a conflict with 3957 // attractors, see commit from 2022.04.08, 11:12:18. 3958 // The parameter is needed for lines with snapToGrid enabled 3959 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 3960 this.mouse.obj.snapToPoints(); 3961 } 3962 } 3963 3964 this.originMoveEnd(); 3965 this.dehighlightAll(); 3966 this.update(); 3967 3968 // selection 3969 if (this.selectingMode) { 3970 this._stopSelecting(evt); 3971 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 3972 this.stopSelectionMode(); 3973 } else { 3974 for (i = 0; i < this.downObjects.length; i++) { 3975 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 3976 } 3977 } 3978 3979 this.downObjects.length = 0; 3980 3981 if (this.hasMouseUp) { 3982 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 3983 this.hasMouseUp = false; 3984 } 3985 3986 // release dragged mouse object 3987 this.mouse = null; 3988 }, 3989 3990 /** 3991 * Handler for mouse wheel events. Used to zoom in and out of the board. 3992 * @param {Event} evt 3993 * @returns {Boolean} 3994 */ 3995 mouseWheelListener: function (evt) { 3996 var wd, zoomCenter, pos; 3997 3998 if (!this.attr.zoom.enabled || 3999 !this.attr.zoom.wheel || 4000 !this._isRequiredKeyPressed(evt, 'zoom')) { 4001 4002 return true; 4003 } 4004 4005 evt = evt || window.event; 4006 wd = evt.detail ? -evt.detail : evt.wheelDelta / 40; 4007 zoomCenter = this.attr.zoom.center; 4008 4009 if (zoomCenter === 'board') { 4010 pos = []; 4011 } else { // including zoomCenter === 'auto' 4012 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords; 4013 } 4014 4015 // pos == [] does not throw an error 4016 if (wd > 0) { 4017 this.zoomIn(pos[1], pos[2]); 4018 } else { 4019 this.zoomOut(pos[1], pos[2]); 4020 } 4021 4022 this.triggerEventHandlers(['mousewheel'], [evt]); 4023 4024 evt.preventDefault(); 4025 return false; 4026 }, 4027 4028 /** 4029 * Allow moving of JSXGraph elements with arrow keys. 4030 * The selection of the element is done with the tab key. For this, 4031 * the attribute 'tabindex' of the element has to be set to some number (default=0). 4032 * tabindex corresponds to the HTML and SVG attribute of the same name. 4033 * <p> 4034 * Panning of the construction is done with arrow keys 4035 * if the pan key (shift or ctrl - depending on the board attributes) is pressed. 4036 * <p> 4037 * Zooming is triggered with the keys +, o, -, if 4038 * the pan key (shift or ctrl - depending on the board attributes) is pressed. 4039 * <p> 4040 * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus. 4041 * 4042 * @param {Event} evt The browser's event object 4043 * 4044 * @see JXG.Board#keyboard 4045 * @see JXG.Board#keyFocusInListener 4046 * @see JXG.Board#keyFocusOutListener 4047 * 4048 */ 4049 keyDownListener: function (evt) { 4050 var id_node = evt.target.id, 4051 id, el, res, doc, 4052 sX = 0, 4053 sY = 0, 4054 // dx, dy are provided in screen units and 4055 // are converted to user coordinates 4056 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 4057 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 4058 // u = 100, 4059 doZoom = false, 4060 done = true, 4061 dir, 4062 actPos; 4063 4064 if (!this.attr.keyboard.enabled || id_node === '') { 4065 return false; 4066 } 4067 4068 // dx = Math.round(dx * u) / u; 4069 // dy = Math.round(dy * u) / u; 4070 4071 // An element of type input or textarea has focus, get out of here. 4072 doc = this.containerObj.shadowRoot || document; 4073 if (doc.activeElement) { 4074 el = doc.activeElement; 4075 if (el.tagName === 'INPUT' || el.tagName === 'textarea') { 4076 return false; 4077 } 4078 } 4079 4080 // Get the JSXGraph id from the id of the SVG node. 4081 id = id_node.replace(this.containerObj.id + '_', ''); 4082 el = this.select(id); 4083 4084 if (Type.exists(el.coords)) { 4085 actPos = el.coords.usrCoords.slice(1); 4086 } 4087 4088 if ( 4089 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 4090 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) 4091 ) { 4092 // Pan key has been pressed 4093 4094 if (Type.evaluate(this.attr.zoom.enabled) === true) { 4095 doZoom = true; 4096 } 4097 4098 // Arrow keys 4099 if (evt.keyCode === 38) { 4100 // up 4101 this.clickUpArrow(); 4102 } else if (evt.keyCode === 40) { 4103 // down 4104 this.clickDownArrow(); 4105 } else if (evt.keyCode === 37) { 4106 // left 4107 this.clickLeftArrow(); 4108 } else if (evt.keyCode === 39) { 4109 // right 4110 this.clickRightArrow(); 4111 4112 // Zoom keys 4113 } else if (doZoom && evt.keyCode === 171) { 4114 // + 4115 this.zoomIn(); 4116 } else if (doZoom && evt.keyCode === 173) { 4117 // - 4118 this.zoomOut(); 4119 } else if (doZoom && evt.keyCode === 79) { 4120 // o 4121 this.zoom100(); 4122 } else { 4123 done = false; 4124 } 4125 } else if (!evt.shiftKey && !evt.ctrlKey) { // Move an element if neither shift or ctrl are pressed 4126 // Adapt dx, dy to snapToGrid and attractToGrid. 4127 // snapToGrid has priority. 4128 if (Type.exists(el.visProp)) { 4129 if ( 4130 Type.exists(el.visProp.snaptogrid) && 4131 el.visProp.snaptogrid && 4132 el.evalVisProp('snapsizex') && 4133 el.evalVisProp('snapsizey') 4134 ) { 4135 // Adapt dx, dy such that snapToGrid is possible 4136 res = el.getSnapSizes(); 4137 sX = res[0]; 4138 sY = res[1]; 4139 // If snaptogrid is true, 4140 // we can only jump from grid point to grid point. 4141 dx = sX; 4142 dy = sY; 4143 } else if ( 4144 Type.exists(el.visProp.attracttogrid) && 4145 el.visProp.attracttogrid && 4146 el.evalVisProp('attractordistance') && 4147 el.evalVisProp('attractorunit') 4148 ) { 4149 // Adapt dx, dy such that attractToGrid is possible 4150 sX = 1.1 * el.evalVisProp('attractordistance'); 4151 sY = sX; 4152 4153 if (el.evalVisProp('attractorunit') === 'screen') { 4154 sX /= this.unitX; 4155 sY /= this.unitX; 4156 } 4157 dx = Math.max(sX, dx); 4158 dy = Math.max(sY, dy); 4159 } 4160 } 4161 4162 if (evt.keyCode === 38) { 4163 // up 4164 dir = [0, dy]; 4165 } else if (evt.keyCode === 40) { 4166 // down 4167 dir = [0, -dy]; 4168 } else if (evt.keyCode === 37) { 4169 // left 4170 dir = [-dx, 0]; 4171 } else if (evt.keyCode === 39) { 4172 // right 4173 dir = [dx, 0]; 4174 } else { 4175 done = false; 4176 } 4177 4178 if (dir && el.isDraggable && 4179 el.visPropCalc.visible && 4180 ((this.geonextCompatibilityMode && 4181 (Type.isPoint(el) || 4182 el.elementClass === Const.OBJECT_CLASS_TEXT) 4183 ) || !this.geonextCompatibilityMode) && 4184 !el.evalVisProp('fixed') 4185 ) { 4186 this.mode = this.BOARD_MODE_DRAG; 4187 if (Type.exists(el.coords)) { 4188 dir[0] += actPos[0]; 4189 dir[1] += actPos[1]; 4190 } 4191 // For coordsElement setPosition has to call setPositionDirectly. 4192 // Otherwise the position is set by a translation. 4193 if (Type.exists(el.coords)) { 4194 el.setPosition(JXG.COORDS_BY_USER, dir); 4195 this.updateInfobox(el); 4196 } else { 4197 this.displayInfobox(false); 4198 el.setPositionDirectly( 4199 Const.COORDS_BY_USER, 4200 dir, 4201 [0, 0] 4202 ); 4203 } 4204 4205 this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); 4206 el.triggerEventHandlers(['keydrag', 'drag'], [evt]); 4207 this.mode = this.BOARD_MODE_NONE; 4208 } 4209 } 4210 4211 this.update(); 4212 4213 if (done && Type.exists(evt.preventDefault)) { 4214 evt.preventDefault(); 4215 } 4216 return done; 4217 }, 4218 4219 /** 4220 * Event listener for SVG elements getting focus. 4221 * This is needed for highlighting when using keyboard control. 4222 * Only elements having the attribute 'tabindex' can receive focus. 4223 * 4224 * @see JXG.Board#keyFocusOutListener 4225 * @see JXG.Board#keyDownListener 4226 * @see JXG.Board#keyboard 4227 * 4228 * @param {Event} evt The browser's event object 4229 */ 4230 keyFocusInListener: function (evt) { 4231 var id_node = evt.target.id, 4232 id, 4233 el; 4234 4235 if (!this.attr.keyboard.enabled || id_node === '') { 4236 return false; 4237 } 4238 4239 // Get JSXGraph id from node id 4240 id = id_node.replace(this.containerObj.id + '_', ''); 4241 el = this.select(id); 4242 if (Type.exists(el.highlight)) { 4243 el.highlight(true); 4244 this.focusObjects = [id]; 4245 el.triggerEventHandlers(['hit'], [evt]); 4246 } 4247 if (Type.exists(el.coords)) { 4248 this.updateInfobox(el); 4249 } 4250 }, 4251 4252 /** 4253 * Event listener for SVG elements losing focus. 4254 * This is needed for dehighlighting when using keyboard control. 4255 * Only elements having the attribute 'tabindex' can receive focus. 4256 * 4257 * @see JXG.Board#keyFocusInListener 4258 * @see JXG.Board#keyDownListener 4259 * @see JXG.Board#keyboard 4260 * 4261 * @param {Event} evt The browser's event object 4262 */ 4263 keyFocusOutListener: function (evt) { 4264 if (!this.attr.keyboard.enabled) { 4265 return false; 4266 } 4267 this.focusObjects = []; // This has to be before displayInfobox(false) 4268 this.dehighlightAll(); 4269 this.displayInfobox(false); 4270 }, 4271 4272 /** 4273 * Update the width and height of the JSXGraph container div element. 4274 * If width and height are not supplied, read actual values with offsetWidth/Height, 4275 * and call board.resizeContainer() with this values. 4276 * <p> 4277 * If necessary, also call setBoundingBox(). 4278 * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element 4279 * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element 4280 * @returns {JXG.Board} Reference to the board 4281 * 4282 * @see JXG.Board#startResizeObserver 4283 * @see JXG.Board#resizeListener 4284 * @see JXG.Board#resizeContainer 4285 * @see JXG.Board#setBoundingBox 4286 * 4287 */ 4288 updateContainerDims: function (width, height) { 4289 var w = width, 4290 h = height, 4291 // bb, 4292 css, 4293 width_adjustment, height_adjustment; 4294 4295 if (width === undefined) { 4296 // Get size of the board's container div 4297 // 4298 // offsetWidth/Height ignores CSS transforms, 4299 // getBoundingClientRect includes CSS transforms 4300 // 4301 // bb = this.containerObj.getBoundingClientRect(); 4302 // w = bb.width; 4303 // h = bb.height; 4304 w = this.containerObj.offsetWidth; 4305 h = this.containerObj.offsetHeight; 4306 } 4307 4308 if (width === undefined && window && window.getComputedStyle) { 4309 // Subtract the border size 4310 css = window.getComputedStyle(this.containerObj, null); 4311 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 4312 if (!isNaN(width_adjustment)) { 4313 w -= width_adjustment; 4314 } 4315 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 4316 if (!isNaN(height_adjustment)) { 4317 h -= height_adjustment; 4318 } 4319 } 4320 4321 // If div is invisible - do nothing 4322 if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { 4323 return this; 4324 } 4325 4326 // If bounding box is not yet initialized, do it now. 4327 if (isNaN(this.getBoundingBox()[0])) { 4328 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 4329 } 4330 4331 // Do nothing if the dimension did not change since being visible 4332 // the last time. Note that if the div had display:none in the mean time, 4333 // we did not store this._prevDim. 4334 if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) { 4335 return this; 4336 } 4337 // Set the size of the SVG or canvas element 4338 this.resizeContainer(w, h, true); 4339 this._prevDim = { 4340 w: w, 4341 h: h 4342 }; 4343 return this; 4344 }, 4345 4346 /** 4347 * Start observer which reacts to size changes of the JSXGraph 4348 * container div element. Calls updateContainerDims(). 4349 * If not available, an event listener for the window-resize event is started. 4350 * On mobile devices also scrolling might trigger resizes. 4351 * However, resize events triggered by scrolling events should be ignored. 4352 * Therefore, also a scrollListener is started. 4353 * Resize can be controlled with the board attribute resize. 4354 * 4355 * @see JXG.Board#updateContainerDims 4356 * @see JXG.Board#resizeListener 4357 * @see JXG.Board#scrollListener 4358 * @see JXG.Board#resize 4359 * 4360 */ 4361 startResizeObserver: function () { 4362 var that = this; 4363 4364 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4365 return; 4366 } 4367 4368 this.resizeObserver = new ResizeObserver(function (entries) { 4369 var bb; 4370 if (!that._isResizing) { 4371 that._isResizing = true; 4372 bb = entries[0].contentRect; 4373 window.setTimeout(function () { 4374 try { 4375 that.updateContainerDims(bb.width, bb.height); 4376 } catch (e) { 4377 JXG.debug(e); // Used to log errors during board.update() 4378 that.stopResizeObserver(); 4379 } finally { 4380 that._isResizing = false; 4381 } 4382 }, that.attr.resize.throttle); 4383 } 4384 }); 4385 this.resizeObserver.observe(this.containerObj); 4386 }, 4387 4388 /** 4389 * Stops the resize observer. 4390 * @see JXG.Board#startResizeObserver 4391 * 4392 */ 4393 stopResizeObserver: function () { 4394 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4395 return; 4396 } 4397 4398 if (Type.exists(this.resizeObserver)) { 4399 this.resizeObserver.unobserve(this.containerObj); 4400 } 4401 }, 4402 4403 /** 4404 * Fallback solutions if there is no resizeObserver available in the browser. 4405 * Reacts to resize events of the window (only). Otherwise similar to 4406 * startResizeObserver(). To handle changes of the visibility 4407 * of the JSXGraph container element, additionally an intersection observer is used. 4408 * which watches changes in the visibility of the JSXGraph container element. 4409 * This is necessary e.g. for register tabs or dia shows. 4410 * 4411 * @see JXG.Board#startResizeObserver 4412 * @see JXG.Board#startIntersectionObserver 4413 */ 4414 resizeListener: function () { 4415 var that = this; 4416 4417 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4418 return; 4419 } 4420 if (!this._isScrolling && !this._isResizing) { 4421 this._isResizing = true; 4422 window.setTimeout(function () { 4423 that.updateContainerDims(); 4424 that._isResizing = false; 4425 }, this.attr.resize.throttle); 4426 } 4427 }, 4428 4429 /** 4430 * Listener to watch for scroll events. Sets board._isScrolling = true 4431 * @param {Event} evt The browser's event object 4432 * 4433 * @see JXG.Board#startResizeObserver 4434 * @see JXG.Board#resizeListener 4435 * 4436 */ 4437 scrollListener: function (evt) { 4438 var that = this; 4439 4440 if (!Env.isBrowser) { 4441 return; 4442 } 4443 if (!this._isScrolling) { 4444 this._isScrolling = true; 4445 window.setTimeout(function () { 4446 that._isScrolling = false; 4447 }, 66); 4448 } 4449 }, 4450 4451 /** 4452 * Watch for changes of the visibility of the JSXGraph container element. 4453 * 4454 * @see JXG.Board#startResizeObserver 4455 * @see JXG.Board#resizeListener 4456 * 4457 */ 4458 startIntersectionObserver: function () { 4459 var that = this, 4460 options = { 4461 root: null, 4462 rootMargin: '0px', 4463 threshold: 0.8 4464 }; 4465 4466 try { 4467 this.intersectionObserver = new IntersectionObserver(function (entries) { 4468 // If bounding box is not yet initialized, do it now. 4469 if (isNaN(that.getBoundingBox()[0])) { 4470 that.updateContainerDims(); 4471 } 4472 }, options); 4473 this.intersectionObserver.observe(that.containerObj); 4474 } catch (err) { 4475 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.'); 4476 } 4477 }, 4478 4479 /** 4480 * Stop the intersection observer 4481 * 4482 * @see JXG.Board#startIntersectionObserver 4483 * 4484 */ 4485 stopIntersectionObserver: function () { 4486 if (Type.exists(this.intersectionObserver)) { 4487 this.intersectionObserver.unobserve(this.containerObj); 4488 } 4489 }, 4490 4491 /** 4492 * Update the container before and after printing. 4493 * @param {Event} [evt] 4494 */ 4495 printListener: function(evt) { 4496 this.updateContainerDims(); 4497 }, 4498 4499 /** 4500 * Wrapper for printListener to be used in mediaQuery matches. 4501 * @param {MediaQueryList} mql 4502 */ 4503 printListenerMatch: function (mql) { 4504 if (mql.matches) { 4505 this.printListener(); 4506 } 4507 }, 4508 4509 /********************************************************** 4510 * 4511 * End of Event Handlers 4512 * 4513 **********************************************************/ 4514 4515 /** 4516 * Initialize the info box object which is used to display 4517 * the coordinates of points near the mouse pointer, 4518 * @returns {JXG.Board} Reference to the board 4519 */ 4520 initInfobox: function (attributes) { 4521 var attr = Type.copyAttributes(attributes, this.options, 'infobox'); 4522 4523 attr.id = this.id + '_infobox'; 4524 4525 /** 4526 * Infobox close to points in which the points' coordinates are displayed. 4527 * This is simply a JXG.Text element. Access through board.infobox. 4528 * Uses CSS class .JXGinfobox. 4529 * 4530 * @namespace 4531 * @name JXG.Board.infobox 4532 * @type JXG.Text 4533 * 4534 * @example 4535 * const board = JXG.JSXGraph.initBoard(BOARDID, { 4536 * boundingbox: [-0.5, 0.5, 0.5, -0.5], 4537 * intl: { 4538 * enabled: false, 4539 * locale: 'de-DE' 4540 * }, 4541 * keepaspectratio: true, 4542 * axis: true, 4543 * infobox: { 4544 * distanceY: 40, 4545 * intl: { 4546 * enabled: true, 4547 * options: { 4548 * minimumFractionDigits: 1, 4549 * maximumFractionDigits: 2 4550 * } 4551 * } 4552 * } 4553 * }); 4554 * var p = board.create('point', [0.1, 0.1], {}); 4555 * 4556 * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div> 4557 * <script type="text/javascript"> 4558 * (function() { 4559 * const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', { 4560 * boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false, 4561 * intl: { 4562 * enabled: false, 4563 * locale: 'de-DE' 4564 * }, 4565 * keepaspectratio: true, 4566 * axis: true, 4567 * infobox: { 4568 * distanceY: 40, 4569 * intl: { 4570 * enabled: true, 4571 * options: { 4572 * minimumFractionDigits: 1, 4573 * maximumFractionDigits: 2 4574 * } 4575 * } 4576 * } 4577 * }); 4578 * var p = board.create('point', [0.1, 0.1], {}); 4579 * })(); 4580 * 4581 * </script><pre> 4582 * 4583 */ 4584 this.infobox = this.create('text', [0, 0, '0,0'], attr); 4585 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 4586 this.infobox.dump = false; 4587 4588 this.displayInfobox(false); 4589 return this; 4590 }, 4591 4592 /** 4593 * Updates and displays a little info box to show coordinates of current selected points. 4594 * @param {JXG.GeometryElement} el A GeometryElement 4595 * @returns {JXG.Board} Reference to the board 4596 * @see JXG.Board#displayInfobox 4597 * @see JXG.Board#showInfobox 4598 * @see Point#showInfobox 4599 * 4600 */ 4601 updateInfobox: function (el) { 4602 var x, y, xc, yc, 4603 vpinfoboxdigits, 4604 distX, distY, 4605 vpsi = el.evalVisProp('showinfobox'); 4606 4607 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) { 4608 return this; 4609 } 4610 4611 if (Type.isPoint(el)) { 4612 xc = el.coords.usrCoords[1]; 4613 yc = el.coords.usrCoords[2]; 4614 distX = this.infobox.evalVisProp('distancex'); 4615 distY = this.infobox.evalVisProp('distancey'); 4616 4617 this.infobox.setCoords( 4618 xc + distX / this.unitX, 4619 yc + distY / this.unitY 4620 ); 4621 4622 vpinfoboxdigits = el.evalVisProp('infoboxdigits'); 4623 if (typeof el.infoboxText !== 'string') { 4624 if (vpinfoboxdigits === 'auto') { 4625 if (this.infobox.useLocale()) { 4626 x = this.infobox.formatNumberLocale(xc); 4627 y = this.infobox.formatNumberLocale(yc); 4628 } else { 4629 x = Type.autoDigits(xc); 4630 y = Type.autoDigits(yc); 4631 } 4632 } else if (Type.isNumber(vpinfoboxdigits)) { 4633 if (this.infobox.useLocale()) { 4634 x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits); 4635 y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits); 4636 } else { 4637 x = Type.toFixed(xc, vpinfoboxdigits); 4638 y = Type.toFixed(yc, vpinfoboxdigits); 4639 } 4640 4641 } else { 4642 x = xc; 4643 y = yc; 4644 } 4645 4646 this.highlightInfobox(x, y, el); 4647 } else { 4648 this.highlightCustomInfobox(el.infoboxText, el); 4649 } 4650 4651 this.displayInfobox(true); 4652 } 4653 return this; 4654 }, 4655 4656 /** 4657 * Set infobox visible / invisible. 4658 * 4659 * It uses its property hiddenByParent to memorize its status. 4660 * In this way, many DOM access can be avoided. 4661 * 4662 * @param {Boolean} val true for visible, false for invisible 4663 * @returns {JXG.Board} Reference to the board. 4664 * @see JXG.Board#updateInfobox 4665 * 4666 */ 4667 displayInfobox: function (val) { 4668 if (!val && this.focusObjects.length > 0 && 4669 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) { 4670 // If an element has focus we do not hide its infobox 4671 return this; 4672 } 4673 if (this.infobox.hiddenByParent === val) { 4674 this.infobox.hiddenByParent = !val; 4675 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 4676 } 4677 return this; 4678 }, 4679 4680 // Alias for displayInfobox to be backwards compatible. 4681 // The method showInfobox clashes with the board attribute showInfobox 4682 showInfobox: function (val) { 4683 return this.displayInfobox(val); 4684 }, 4685 4686 /** 4687 * Changes the text of the info box to show the given coordinates. 4688 * @param {Number} x 4689 * @param {Number} y 4690 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 4691 * @returns {JXG.Board} Reference to the board. 4692 */ 4693 highlightInfobox: function (x, y, el) { 4694 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 4695 return this; 4696 }, 4697 4698 /** 4699 * Changes the text of the info box to what is provided via text. 4700 * @param {String} text 4701 * @param {JXG.GeometryElement} [el] 4702 * @returns {JXG.Board} Reference to the board. 4703 */ 4704 highlightCustomInfobox: function (text, el) { 4705 this.infobox.setText(text); 4706 return this; 4707 }, 4708 4709 /** 4710 * Remove highlighting of all elements. 4711 * @returns {JXG.Board} Reference to the board. 4712 */ 4713 dehighlightAll: function () { 4714 var el, 4715 pEl, 4716 stillHighlighted = {}, 4717 needsDeHighlight = false; 4718 4719 for (el in this.highlightedObjects) { 4720 if (this.highlightedObjects.hasOwnProperty(el)) { 4721 4722 pEl = this.highlightedObjects[el]; 4723 if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus 4724 if (this.hasMouseHandlers || this.hasPointerHandlers) { 4725 pEl.noHighlight(); 4726 } 4727 needsDeHighlight = true; 4728 } else { 4729 stillHighlighted[el] = pEl; 4730 } 4731 // In highlightedObjects should only be objects which fulfill all these conditions 4732 // And in case of complex elements, like a turtle based fractal, it should be faster to 4733 // just de-highlight the element instead of checking hasPoint... 4734 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 4735 } 4736 } 4737 4738 this.highlightedObjects = stillHighlighted; 4739 4740 // We do not need to redraw during dehighlighting in CanvasRenderer 4741 // because we are redrawing anyhow 4742 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 4743 // another object is highlighted. 4744 if (this.renderer.type === 'canvas' && needsDeHighlight) { 4745 this.prepareUpdate(); 4746 this.renderer.suspendRedraw(this); 4747 this.updateRenderer(); 4748 this.renderer.unsuspendRedraw(); 4749 } 4750 4751 return this; 4752 }, 4753 4754 /** 4755 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 4756 * once. 4757 * @private 4758 * @param {Number} x X coordinate in screen coordinates 4759 * @param {Number} y Y coordinate in screen coordinates 4760 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 4761 * @see JXG.Board#getUsrCoordsOfMouse 4762 */ 4763 getScrCoordsOfMouse: function (x, y) { 4764 return [x, y]; 4765 }, 4766 4767 /** 4768 * This method calculates the user coords of the current mouse coordinates. 4769 * @param {Event} evt Event object containing the mouse coordinates. 4770 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 4771 * @example 4772 * board.on('up', function (evt) { 4773 * var a = board.getUsrCoordsOfMouse(evt), 4774 * x = a[0], 4775 * y = a[1], 4776 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4777 * // Shorter version: 4778 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4779 * }); 4780 * 4781 * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div> 4782 * <script type='text/javascript'> 4783 * (function() { 4784 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 4785 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4786 * board.on('up', function (evt) { 4787 * var a = board.getUsrCoordsOfMouse(evt), 4788 * x = a[0], 4789 * y = a[1], 4790 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4791 * // Shorter version: 4792 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4793 * }); 4794 * 4795 * })(); 4796 * 4797 * </script><pre> 4798 * 4799 * @see JXG.Board#getScrCoordsOfMouse 4800 * @see JXG.Board#getAllUnderMouse 4801 */ 4802 getUsrCoordsOfMouse: function (evt) { 4803 var cPos = this.getCoordsTopLeftCorner(), 4804 absPos = Env.getPosition(evt, null, this.document), 4805 x = absPos[0] - cPos[0], 4806 y = absPos[1] - cPos[1], 4807 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 4808 4809 return newCoords.usrCoords.slice(1); 4810 }, 4811 4812 /** 4813 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 4814 * @param {Event} evt Event object containing the mouse coordinates. 4815 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 4816 * @see JXG.Board#getUsrCoordsOfMouse 4817 * @see JXG.Board#getAllObjectsUnderMouse 4818 */ 4819 getAllUnderMouse: function (evt) { 4820 var elList = this.getAllObjectsUnderMouse(evt); 4821 elList.push(this.getUsrCoordsOfMouse(evt)); 4822 4823 return elList; 4824 }, 4825 4826 /** 4827 * Collects all elements under current mouse position. 4828 * @param {Event} evt Event object containing the mouse coordinates. 4829 * @returns {Array} Array of elements at the current mouse position. 4830 * @see JXG.Board#getAllUnderMouse 4831 */ 4832 getAllObjectsUnderMouse: function (evt) { 4833 var cPos = this.getCoordsTopLeftCorner(), 4834 absPos = Env.getPosition(evt, null, this.document), 4835 dx = absPos[0] - cPos[0], 4836 dy = absPos[1] - cPos[1], 4837 elList = [], 4838 el, 4839 pEl, 4840 len = this.objectsList.length; 4841 4842 for (el = 0; el < len; el++) { 4843 pEl = this.objectsList[el]; 4844 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 4845 elList[elList.length] = pEl; 4846 } 4847 } 4848 4849 return elList; 4850 }, 4851 4852 /** 4853 * Update the coords object of all elements which possess this 4854 * property. This is necessary after changing the viewport. 4855 * @returns {JXG.Board} Reference to this board. 4856 **/ 4857 updateCoords: function () { 4858 var el, 4859 ob, 4860 len = this.objectsList.length; 4861 4862 for (ob = 0; ob < len; ob++) { 4863 el = this.objectsList[ob]; 4864 4865 if (Type.exists(el.coords)) { 4866 if (el.evalVisProp('frozen')) { 4867 if (el.is3D) { 4868 el.element2D.coords.screen2usr(); 4869 } else { 4870 el.coords.screen2usr(); 4871 } 4872 } else { 4873 if (el.is3D) { 4874 el.element2D.coords.usr2screen(); 4875 } else { 4876 el.coords.usr2screen(); 4877 if (Type.exists(el.actualCoords)) { 4878 el.actualCoords.usr2screen(); 4879 4880 } 4881 } 4882 } 4883 } 4884 } 4885 return this; 4886 }, 4887 4888 /** 4889 * Moves the origin and initializes an update of all elements. 4890 * @param {Number} x 4891 * @param {Number} y 4892 * @param {Boolean} [diff=false] 4893 * @returns {JXG.Board} Reference to this board. 4894 */ 4895 moveOrigin: function (x, y, diff) { 4896 var ox, oy, ul, lr; 4897 if (Type.exists(x) && Type.exists(y)) { 4898 ox = this.origin.scrCoords[1]; 4899 oy = this.origin.scrCoords[2]; 4900 4901 this.origin.scrCoords[1] = x; 4902 this.origin.scrCoords[2] = y; 4903 4904 if (diff) { 4905 this.origin.scrCoords[1] -= this.drag_dx; 4906 this.origin.scrCoords[2] -= this.drag_dy; 4907 } 4908 4909 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords; 4910 lr = new Coords( 4911 Const.COORDS_BY_SCREEN, 4912 [this.canvasWidth, this.canvasHeight], 4913 this 4914 ).usrCoords; 4915 if ( 4916 ul[1] < this.maxboundingbox[0] - Mat.eps || 4917 ul[2] > this.maxboundingbox[1] + Mat.eps || 4918 lr[1] > this.maxboundingbox[2] + Mat.eps || 4919 lr[2] < this.maxboundingbox[3] - Mat.eps 4920 ) { 4921 this.origin.scrCoords[1] = ox; 4922 this.origin.scrCoords[2] = oy; 4923 } 4924 } 4925 4926 this.updateCoords().clearTraces().fullUpdate(); 4927 this.triggerEventHandlers(['boundingbox']); 4928 4929 return this; 4930 }, 4931 4932 /** 4933 * Add conditional updates to the elements. 4934 * @param {String} str String containing conditional update in geonext syntax 4935 */ 4936 addConditions: function (str) { 4937 var term, 4938 m, 4939 left, 4940 right, 4941 name, 4942 el, 4943 property, 4944 functions = [], 4945 // plaintext = 'var el, x, y, c, rgbo;\n', 4946 i = str.indexOf('<data>'), 4947 j = str.indexOf('<' + '/data>'), 4948 xyFun = function (board, el, f, what) { 4949 return function () { 4950 var e, t; 4951 4952 e = board.select(el.id); 4953 t = e.coords.usrCoords[what]; 4954 4955 if (what === 2) { 4956 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 4957 } else { 4958 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 4959 } 4960 e.prepareUpdate().update(); 4961 }; 4962 }, 4963 visFun = function (board, el, f) { 4964 return function () { 4965 var e, v; 4966 4967 e = board.select(el.id); 4968 v = f(); 4969 4970 e.setAttribute({ visible: v }); 4971 }; 4972 }, 4973 colFun = function (board, el, f, what) { 4974 return function () { 4975 var e, v; 4976 4977 e = board.select(el.id); 4978 v = f(); 4979 4980 if (what === 'strokewidth') { 4981 e.visProp.strokewidth = v; 4982 } else { 4983 v = Color.rgba2rgbo(v); 4984 e.visProp[what + 'color'] = v[0]; 4985 e.visProp[what + 'opacity'] = v[1]; 4986 } 4987 }; 4988 }, 4989 posFun = function (board, el, f) { 4990 return function () { 4991 var e = board.select(el.id); 4992 4993 e.position = f(); 4994 }; 4995 }, 4996 styleFun = function (board, el, f) { 4997 return function () { 4998 var e = board.select(el.id); 4999 5000 e.setStyle(f()); 5001 }; 5002 }; 5003 5004 if (i < 0) { 5005 return; 5006 } 5007 5008 while (i >= 0) { 5009 term = str.slice(i + 6, j); // throw away <data> 5010 m = term.indexOf('='); 5011 left = term.slice(0, m); 5012 right = term.slice(m + 1); 5013 m = left.indexOf('.'); // Resulting variable names must not contain dots, e.g. ' Steuern akt.' 5014 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 5015 el = this.elementsByName[Type.unescapeHTML(name)]; 5016 5017 property = left 5018 .slice(m + 1) 5019 .replace(/\s+/g, '') 5020 .toLowerCase(); // remove whitespace in property 5021 right = Type.createFunction(right, this, '', true); 5022 5023 // Debug 5024 if (!Type.exists(this.elementsByName[name])) { 5025 JXG.debug('debug conditions: |' + name + '| undefined'); 5026 } else { 5027 // plaintext += 'el = this.objects[\'' + el.id + '\'];\n'; 5028 5029 switch (property) { 5030 case 'x': 5031 functions.push(xyFun(this, el, right, 2)); 5032 break; 5033 case 'y': 5034 functions.push(xyFun(this, el, right, 1)); 5035 break; 5036 case 'visible': 5037 functions.push(visFun(this, el, right)); 5038 break; 5039 case 'position': 5040 functions.push(posFun(this, el, right)); 5041 break; 5042 case 'stroke': 5043 functions.push(colFun(this, el, right, 'stroke')); 5044 break; 5045 case 'style': 5046 functions.push(styleFun(this, el, right)); 5047 break; 5048 case 'strokewidth': 5049 functions.push(colFun(this, el, right, 'strokewidth')); 5050 break; 5051 case 'fill': 5052 functions.push(colFun(this, el, right, 'fill')); 5053 break; 5054 case 'label': 5055 break; 5056 default: 5057 JXG.debug( 5058 'property "' + 5059 property + 5060 '" in conditions not yet implemented:' + 5061 right 5062 ); 5063 break; 5064 } 5065 } 5066 str = str.slice(j + 7); // cut off '</data>' 5067 i = str.indexOf('<data>'); 5068 j = str.indexOf('<' + '/data>'); 5069 } 5070 5071 this.updateConditions = function () { 5072 var i; 5073 5074 for (i = 0; i < functions.length; i++) { 5075 functions[i](); 5076 } 5077 5078 this.prepareUpdate().updateElements(); 5079 return true; 5080 }; 5081 this.updateConditions(); 5082 }, 5083 5084 /** 5085 * Computes the commands in the conditions-section of the gxt file. 5086 * It is evaluated after an update, before the unsuspendRedraw. 5087 * The function is generated in 5088 * @see JXG.Board#addConditions 5089 * @private 5090 */ 5091 updateConditions: function () { 5092 return false; 5093 }, 5094 5095 /** 5096 * Calculates adequate snap sizes. 5097 * @returns {JXG.Board} Reference to the board. 5098 */ 5099 calculateSnapSizes: function () { 5100 var p1, p2, 5101 bbox = this.getBoundingBox(), 5102 gridStep = Type.evaluate(this.options.grid.majorStep), 5103 gridX = Type.evaluate(this.options.grid.gridX), 5104 gridY = Type.evaluate(this.options.grid.gridY), 5105 x, y; 5106 5107 if (!Type.isArray(gridStep)) { 5108 gridStep = [gridStep, gridStep]; 5109 } 5110 if (gridStep.length < 2) { 5111 gridStep = [gridStep[0], gridStep[0]]; 5112 } 5113 if (Type.exists(gridX)) { 5114 gridStep[0] = gridX; 5115 } 5116 if (Type.exists(gridY)) { 5117 gridStep[1] = gridY; 5118 } 5119 5120 if (gridStep[0] === 'auto') { 5121 gridStep[0] = 1; 5122 } else { 5123 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX); 5124 } 5125 if (gridStep[1] === 'auto') { 5126 gridStep[1] = 1; 5127 } else { 5128 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY); 5129 } 5130 5131 p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this); 5132 p2 = new Coords( 5133 Const.COORDS_BY_USER, 5134 [gridStep[0], gridStep[1]], 5135 this 5136 ); 5137 x = p1.scrCoords[1] - p2.scrCoords[1]; 5138 y = p1.scrCoords[2] - p2.scrCoords[2]; 5139 5140 this.options.grid.snapSizeX = gridStep[0]; 5141 while (Math.abs(x) > 25) { 5142 this.options.grid.snapSizeX *= 2; 5143 x /= 2; 5144 } 5145 5146 this.options.grid.snapSizeY = gridStep[1]; 5147 while (Math.abs(y) > 25) { 5148 this.options.grid.snapSizeY *= 2; 5149 y /= 2; 5150 } 5151 5152 return this; 5153 }, 5154 5155 /** 5156 * Apply update on all objects with the new zoom-factors. Clears all traces. 5157 * @returns {JXG.Board} Reference to the board. 5158 */ 5159 applyZoom: function () { 5160 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 5161 5162 return this; 5163 }, 5164 5165 /** 5166 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5167 * The zoom operation is centered at x, y. 5168 * @param {Number} [x] 5169 * @param {Number} [y] 5170 * @returns {JXG.Board} Reference to the board 5171 */ 5172 zoomIn: function (x, y) { 5173 var bb = this.getBoundingBox(), 5174 zX = Type.evaluate(this.attr.zoom.factorx), 5175 zY = Type.evaluate(this.attr.zoom.factory), 5176 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 5177 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 5178 lr = 0.5, 5179 tr = 0.5, 5180 ma = Type.evaluate(this.attr.zoom.max), 5181 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5182 5183 if ( 5184 (this.zoomX > ma && zX > 1.0) || 5185 (this.zoomY > ma && zY > 1.0) || 5186 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 5187 (this.zoomY < mi && zY < 1.0) 5188 ) { 5189 return this; 5190 } 5191 5192 if (Type.isNumber(x) && Type.isNumber(y)) { 5193 lr = (x - bb[0]) / (bb[2] - bb[0]); 5194 tr = (bb[1] - y) / (bb[1] - bb[3]); 5195 } 5196 5197 this.setBoundingBox( 5198 [ 5199 bb[0] + dX * lr, 5200 bb[1] - dY * tr, 5201 bb[2] - dX * (1 - lr), 5202 bb[3] + dY * (1 - tr) 5203 ], 5204 this.keepaspectratio, 5205 'update' 5206 ); 5207 return this.applyZoom(); 5208 }, 5209 5210 /** 5211 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5212 * The zoom operation is centered at x, y. 5213 * 5214 * @param {Number} [x] 5215 * @param {Number} [y] 5216 * @returns {JXG.Board} Reference to the board 5217 */ 5218 zoomOut: function (x, y) { 5219 var bb = this.getBoundingBox(), 5220 zX = Type.evaluate(this.attr.zoom.factorx), 5221 zY = Type.evaluate(this.attr.zoom.factory), 5222 dX = (bb[2] - bb[0]) * (1.0 - zX), 5223 dY = (bb[1] - bb[3]) * (1.0 - zY), 5224 lr = 0.5, 5225 tr = 0.5, 5226 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5227 5228 if (this.zoomX < mi || this.zoomY < mi) { 5229 return this; 5230 } 5231 5232 if (Type.isNumber(x) && Type.isNumber(y)) { 5233 lr = (x - bb[0]) / (bb[2] - bb[0]); 5234 tr = (bb[1] - y) / (bb[1] - bb[3]); 5235 } 5236 5237 this.setBoundingBox( 5238 [ 5239 bb[0] + dX * lr, 5240 bb[1] - dY * tr, 5241 bb[2] - dX * (1 - lr), 5242 bb[3] + dY * (1 - tr) 5243 ], 5244 this.keepaspectratio, 5245 'update' 5246 ); 5247 5248 return this.applyZoom(); 5249 }, 5250 5251 /** 5252 * Reset the zoom level to the original zoom level from initBoard(); 5253 * Additionally, if the board as been initialized with a boundingBox (which is the default), 5254 * restore the viewport to the original viewport during initialization. Otherwise, 5255 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 5256 * just set the zoom level to 100%. 5257 * 5258 * @returns {JXG.Board} Reference to the board 5259 */ 5260 zoom100: function () { 5261 var bb, dX, dY; 5262 5263 if (Type.exists(this.attr.boundingbox)) { 5264 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 5265 } else { 5266 // Board has been set up with unitX/Y and originX/Y 5267 bb = this.getBoundingBox(); 5268 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 5269 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 5270 this.setBoundingBox( 5271 [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], 5272 this.keepaspectratio, 5273 'reset' 5274 ); 5275 } 5276 return this.applyZoom(); 5277 }, 5278 5279 /** 5280 * Zooms the board so every visible point is shown. Keeps aspect ratio. 5281 * @returns {JXG.Board} Reference to the board 5282 */ 5283 zoomAllPoints: function () { 5284 var el, 5285 border, 5286 borderX, 5287 borderY, 5288 pEl, 5289 minX = 0, 5290 maxX = 0, 5291 minY = 0, 5292 maxY = 0, 5293 len = this.objectsList.length; 5294 5295 for (el = 0; el < len; el++) { 5296 pEl = this.objectsList[el]; 5297 5298 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 5299 if (pEl.coords.usrCoords[1] < minX) { 5300 minX = pEl.coords.usrCoords[1]; 5301 } else if (pEl.coords.usrCoords[1] > maxX) { 5302 maxX = pEl.coords.usrCoords[1]; 5303 } 5304 if (pEl.coords.usrCoords[2] > maxY) { 5305 maxY = pEl.coords.usrCoords[2]; 5306 } else if (pEl.coords.usrCoords[2] < minY) { 5307 minY = pEl.coords.usrCoords[2]; 5308 } 5309 } 5310 } 5311 5312 border = 50; 5313 borderX = border / this.unitX; 5314 borderY = border / this.unitY; 5315 5316 this.setBoundingBox( 5317 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], 5318 this.keepaspectratio, 5319 'update' 5320 ); 5321 5322 return this.applyZoom(); 5323 }, 5324 5325 /** 5326 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 5327 * within the board's viewport. 5328 * @param {Array} elements A set of elements given by id, reference, or name. 5329 * @returns {JXG.Board} Reference to the board. 5330 */ 5331 zoomElements: function (elements) { 5332 var i, e, 5333 box, 5334 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 5335 cx, cy, 5336 dx, dy, 5337 d; 5338 5339 if (!Type.isArray(elements) || elements.length === 0) { 5340 return this; 5341 } 5342 5343 for (i = 0; i < elements.length; i++) { 5344 e = this.select(elements[i]); 5345 5346 box = e.bounds(); 5347 if (Type.isArray(box)) { 5348 if (box[0] < newBBox[0]) { 5349 newBBox[0] = box[0]; 5350 } 5351 if (box[1] > newBBox[1]) { 5352 newBBox[1] = box[1]; 5353 } 5354 if (box[2] > newBBox[2]) { 5355 newBBox[2] = box[2]; 5356 } 5357 if (box[3] < newBBox[3]) { 5358 newBBox[3] = box[3]; 5359 } 5360 } 5361 } 5362 5363 if (Type.isArray(newBBox)) { 5364 cx = 0.5 * (newBBox[0] + newBBox[2]); 5365 cy = 0.5 * (newBBox[1] + newBBox[3]); 5366 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5; 5367 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5; 5368 d = Math.max(dx, dy); 5369 this.setBoundingBox( 5370 [cx - d, cy + d, cx + d, cy - d], 5371 this.keepaspectratio, 5372 'update' 5373 ); 5374 } 5375 5376 return this; 5377 }, 5378 5379 /** 5380 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 5381 * @param {Number} fX 5382 * @param {Number} fY 5383 * @returns {JXG.Board} Reference to the board. 5384 */ 5385 setZoom: function (fX, fY) { 5386 var oX = this.attr.zoom.factorx, 5387 oY = this.attr.zoom.factory; 5388 5389 this.attr.zoom.factorx = fX / this.zoomX; 5390 this.attr.zoom.factory = fY / this.zoomY; 5391 5392 this.zoomIn(); 5393 5394 this.attr.zoom.factorx = oX; 5395 this.attr.zoom.factory = oY; 5396 5397 return this; 5398 }, 5399 5400 /** 5401 * Inner, recursive method of removeObject. 5402 * 5403 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5404 * The element(s) is/are given by name, id or a reference. 5405 * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements 5406 * and tests if the element to be deleted is a child element. If this is the case, it will be 5407 * removed from the list of child elements. If saveMethod=false (default), the element 5408 * is removed from the lists of child elements of all its ancestors. 5409 * The latter should be much faster. 5410 * @returns {JXG.Board} Reference to the board 5411 * @private 5412 */ 5413 _removeObj: function (object, saveMethod) { 5414 var el, i; 5415 5416 if (Type.isArray(object)) { 5417 for (i = 0; i < object.length; i++) { 5418 this._removeObj(object[i], saveMethod); 5419 } 5420 5421 return this; 5422 } 5423 5424 object = this.select(object); 5425 5426 // If the object which is about to be removed is unknown or a string, do nothing. 5427 // it is a string if a string was given and could not be resolved to an element. 5428 if (!Type.exists(object) || Type.isString(object)) { 5429 return this; 5430 } 5431 5432 try { 5433 // remove all children. 5434 for (el in object.childElements) { 5435 if (object.childElements.hasOwnProperty(el)) { 5436 object.childElements[el].board._removeObj(object.childElements[el]); 5437 } 5438 } 5439 5440 // Remove all children in elements like turtle 5441 for (el in object.objects) { 5442 if (object.objects.hasOwnProperty(el)) { 5443 object.objects[el].board._removeObj(object.objects[el]); 5444 } 5445 } 5446 5447 // Remove the element from the childElement list and the descendant list of all elements. 5448 if (saveMethod) { 5449 // Running through all objects has quadratic complexity if many objects are deleted. 5450 for (el in this.objects) { 5451 if (this.objects.hasOwnProperty(el)) { 5452 if ( 5453 Type.exists(this.objects[el].childElements) && 5454 Type.exists( 5455 this.objects[el].childElements.hasOwnProperty(object.id) 5456 ) 5457 ) { 5458 delete this.objects[el].childElements[object.id]; 5459 delete this.objects[el].descendants[object.id]; 5460 } 5461 } 5462 } 5463 } else if (Type.exists(object.ancestors)) { 5464 // Running through the ancestors should be much more efficient. 5465 for (el in object.ancestors) { 5466 if (object.ancestors.hasOwnProperty(el)) { 5467 if ( 5468 Type.exists(object.ancestors[el].childElements) && 5469 Type.exists( 5470 object.ancestors[el].childElements.hasOwnProperty(object.id) 5471 ) 5472 ) { 5473 delete object.ancestors[el].childElements[object.id]; 5474 delete object.ancestors[el].descendants[object.id]; 5475 } 5476 } 5477 } 5478 } 5479 5480 // remove the object itself from our control structures 5481 if (object._pos > -1) { 5482 this.objectsList.splice(object._pos, 1); 5483 for (i = object._pos; i < this.objectsList.length; i++) { 5484 this.objectsList[i]._pos--; 5485 } 5486 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 5487 JXG.debug( 5488 'Board.removeObject: object ' + object.id + ' not found in list.' 5489 ); 5490 } 5491 5492 delete this.objects[object.id]; 5493 delete this.elementsByName[object.name]; 5494 5495 if (object.visProp && object.evalVisProp('trace')) { 5496 object.clearTrace(); 5497 } 5498 5499 // the object deletion itself is handled by the object. 5500 if (Type.exists(object.remove)) { 5501 object.remove(); 5502 } 5503 } catch (e) { 5504 JXG.debug(object.id + ': Could not be removed: ' + e); 5505 } 5506 5507 return this; 5508 }, 5509 5510 /** 5511 * Removes object from board and renderer. 5512 * <p> 5513 * <b>Performance hints:</b> It is recommended to use the object's id. 5514 * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt> 5515 * before looping through the elements to be removed and call 5516 * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop 5517 * in reverse order, i.e. remove the object in reverse order of their creation time. 5518 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5519 * The element(s) is/are given by name, id or a reference. 5520 * @param {Boolean} saveMethod If true, the algorithm runs through all elements 5521 * and tests if the element to be deleted is a child element. If yes, it will be 5522 * removed from the list of child elements. If false (default), the element 5523 * is removed from the lists of child elements of all its ancestors. 5524 * This should be much faster. 5525 * @returns {JXG.Board} Reference to the board 5526 */ 5527 removeObject: function (object, saveMethod) { 5528 var i; 5529 5530 this.renderer.suspendRedraw(this); 5531 if (Type.isArray(object)) { 5532 for (i = 0; i < object.length; i++) { 5533 this._removeObj(object[i], saveMethod); 5534 } 5535 } else { 5536 this._removeObj(object, saveMethod); 5537 } 5538 this.renderer.unsuspendRedraw(); 5539 5540 this.update(); 5541 return this; 5542 }, 5543 5544 /** 5545 * Removes the ancestors of an object an the object itself from board and renderer. 5546 * @param {JXG.GeometryElement} object The object to remove. 5547 * @returns {JXG.Board} Reference to the board 5548 */ 5549 removeAncestors: function (object) { 5550 var anc; 5551 5552 for (anc in object.ancestors) { 5553 if (object.ancestors.hasOwnProperty(anc)) { 5554 this.removeAncestors(object.ancestors[anc]); 5555 } 5556 } 5557 5558 this.removeObject(object); 5559 5560 return this; 5561 }, 5562 5563 /** 5564 * Initialize some objects which are contained in every GEONExT construction by default, 5565 * but are not contained in the gxt files. 5566 * @returns {JXG.Board} Reference to the board 5567 */ 5568 initGeonextBoard: function () { 5569 var p1, p2, p3; 5570 5571 p1 = this.create('point', [0, 0], { 5572 id: this.id + 'g00e0', 5573 name: 'Ursprung', 5574 withLabel: false, 5575 visible: false, 5576 fixed: true 5577 }); 5578 5579 p2 = this.create('point', [1, 0], { 5580 id: this.id + 'gX0e0', 5581 name: 'Punkt_1_0', 5582 withLabel: false, 5583 visible: false, 5584 fixed: true 5585 }); 5586 5587 p3 = this.create('point', [0, 1], { 5588 id: this.id + 'gY0e0', 5589 name: 'Punkt_0_1', 5590 withLabel: false, 5591 visible: false, 5592 fixed: true 5593 }); 5594 5595 this.create('line', [p1, p2], { 5596 id: this.id + 'gXLe0', 5597 name: 'X-Achse', 5598 withLabel: false, 5599 visible: false 5600 }); 5601 5602 this.create('line', [p1, p3], { 5603 id: this.id + 'gYLe0', 5604 name: 'Y-Achse', 5605 withLabel: false, 5606 visible: false 5607 }); 5608 5609 return this; 5610 }, 5611 5612 /** 5613 * Change the height and width of the board's container. 5614 * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using 5615 * the actual size of the bounding box and the actual value of keepaspectratio. 5616 * If setBoundingbox() should not be called automatically, 5617 * call resizeContainer with dontSetBoundingBox == true. 5618 * @param {Number} canvasWidth New width of the container. 5619 * @param {Number} canvasHeight New height of the container. 5620 * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element. 5621 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center. 5622 * @returns {JXG.Board} Reference to the board 5623 */ 5624 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 5625 var box, 5626 oldWidth, oldHeight, 5627 oX, oY; 5628 5629 oldWidth = this.canvasWidth; 5630 oldHeight = this.canvasHeight; 5631 5632 if (!dontSetBoundingBox) { 5633 box = this.getBoundingBox(); // This is the actual bounding box. 5634 } 5635 5636 // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps); 5637 // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps); 5638 this.canvasWidth = parseFloat(canvasWidth); 5639 this.canvasHeight = parseFloat(canvasHeight); 5640 5641 if (!dontset) { 5642 this.containerObj.style.width = this.canvasWidth + 'px'; 5643 this.containerObj.style.height = this.canvasHeight + 'px'; 5644 } 5645 this.renderer.resize(this.canvasWidth, this.canvasHeight); 5646 5647 if (!dontSetBoundingBox) { 5648 this.setBoundingBox(box, this.keepaspectratio, 'keep'); 5649 } else { 5650 oX = (this.canvasWidth - oldWidth) * 0.5; 5651 oY = (this.canvasHeight - oldHeight) * 0.5; 5652 5653 this.moveOrigin( 5654 this.origin.scrCoords[1] + oX, 5655 this.origin.scrCoords[2] + oY 5656 ); 5657 } 5658 5659 return this; 5660 }, 5661 5662 /** 5663 * Lists the dependencies graph in a new HTML-window. 5664 * @returns {JXG.Board} Reference to the board 5665 */ 5666 showDependencies: function () { 5667 var el, t, c, f, i; 5668 5669 t = '<p>\n'; 5670 for (el in this.objects) { 5671 if (this.objects.hasOwnProperty(el)) { 5672 i = 0; 5673 for (c in this.objects[el].childElements) { 5674 if (this.objects[el].childElements.hasOwnProperty(c)) { 5675 i += 1; 5676 } 5677 } 5678 if (i >= 0) { 5679 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 5680 } 5681 5682 for (c in this.objects[el].childElements) { 5683 if (this.objects[el].childElements.hasOwnProperty(c)) { 5684 t += 5685 this.objects[el].childElements[c].id + 5686 '(' + 5687 this.objects[el].childElements[c].name + 5688 ')' + 5689 ', '; 5690 } 5691 } 5692 t += '<p>\n'; 5693 } 5694 } 5695 t += '<' + '/p>\n'; 5696 f = window.open(); 5697 f.document.open(); 5698 f.document.write(t); 5699 f.document.close(); 5700 return this; 5701 }, 5702 5703 /** 5704 * Lists the XML code of the construction in a new HTML-window. 5705 * @returns {JXG.Board} Reference to the board 5706 */ 5707 showXML: function () { 5708 var f = window.open(''); 5709 f.document.open(); 5710 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 5711 f.document.close(); 5712 return this; 5713 }, 5714 5715 /** 5716 * Sets for all objects the needsUpdate flag to 'true'. 5717 * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged. 5718 * @returns {JXG.Board} Reference to the board 5719 */ 5720 prepareUpdate: function (drag) { 5721 var el, i, 5722 pEl, 5723 len = this.objectsList.length; 5724 5725 /* 5726 if (this.attr.updatetype === 'hierarchical') { 5727 return this; 5728 } 5729 */ 5730 5731 for (el = 0; el < len; el++) { 5732 pEl = this.objectsList[el]; 5733 if (this._change3DView || 5734 (Type.exists(drag) && drag.elType === 'view3d_slider') 5735 ) { 5736 // The 3D view has changed. No elements are recomputed, 5737 // only 3D elements are projected to the new view. 5738 pEl.needsUpdate = 5739 pEl.visProp.element3d || 5740 pEl.elType === 'view3d' || 5741 pEl.elType === 'view3d_slider' || 5742 this.needsFullUpdate; 5743 5744 // Special case sphere3d in central projection: 5745 // We have to update the defining points of the ellipse 5746 if (pEl.visProp.element3d && 5747 pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D 5748 ) { 5749 for (i = 0; i < pEl.parents.length; i++) { 5750 this.objects[pEl.parents[i]].needsUpdate = true; 5751 } 5752 } 5753 } else { 5754 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5755 } 5756 } 5757 5758 for (el in this.groups) { 5759 if (this.groups.hasOwnProperty(el)) { 5760 pEl = this.groups[el]; 5761 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5762 } 5763 } 5764 5765 return this; 5766 }, 5767 5768 /** 5769 * Runs through all elements and calls their update() method. 5770 * @param {JXG.GeometryElement} drag Element that caused the update. 5771 * @returns {JXG.Board} Reference to the board 5772 */ 5773 updateElements: function (drag) { 5774 var el, pEl; 5775 //var childId, i = 0; 5776 5777 drag = this.select(drag); 5778 5779 /* 5780 if (Type.exists(drag)) { 5781 for (el = 0; el < this.objectsList.length; el++) { 5782 pEl = this.objectsList[el]; 5783 if (pEl.id === drag.id) { 5784 i = el; 5785 break; 5786 } 5787 } 5788 } 5789 */ 5790 for (el = 0; el < this.objectsList.length; el++) { 5791 pEl = this.objectsList[el]; 5792 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) { 5793 pEl.updateSize(); 5794 } 5795 5796 // For updates of an element we distinguish if the dragged element is updated or 5797 // other elements are updated. 5798 // The difference lies in the treatment of gliders and points based on transformations. 5799 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility(); 5800 } 5801 5802 // update groups last 5803 for (el in this.groups) { 5804 if (this.groups.hasOwnProperty(el)) { 5805 this.groups[el].update(drag); 5806 } 5807 } 5808 5809 return this; 5810 }, 5811 5812 /** 5813 * Runs through all elements and calls their update() method. 5814 * @returns {JXG.Board} Reference to the board 5815 */ 5816 updateRenderer: function () { 5817 var el, 5818 len = this.objectsList.length; 5819 5820 if (!this.renderer) { 5821 return; 5822 } 5823 5824 /* 5825 objs = this.objectsList.slice(0); 5826 objs.sort(function (a, b) { 5827 if (a.visProp.layer < b.visProp.layer) { 5828 return -1; 5829 } else if (a.visProp.layer === b.visProp.layer) { 5830 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 5831 } else { 5832 return 1; 5833 } 5834 }); 5835 */ 5836 5837 if (this.renderer.type === 'canvas') { 5838 this.updateRendererCanvas(); 5839 } else { 5840 for (el = 0; el < len; el++) { 5841 this.objectsList[el].updateRenderer(); 5842 } 5843 } 5844 return this; 5845 }, 5846 5847 /** 5848 * Runs through all elements and calls their update() method. 5849 * This is a special version for the CanvasRenderer. 5850 * Here, we have to do our own layer handling. 5851 * @returns {JXG.Board} Reference to the board 5852 */ 5853 updateRendererCanvas: function () { 5854 var el, pEl, 5855 olen = this.objectsList.length, 5856 // i, minim, lay, 5857 // layers = this.options.layer, 5858 // len = this.options.layer.numlayers, 5859 // last = Number.NEGATIVE_INFINITY.toExponential, 5860 depth_order_layers = [], 5861 objects_sorted, 5862 // Sort the elements for the canvas rendering according to 5863 // their layer, _pos, depthOrder (with this priority) 5864 // @private 5865 _compareFn = function(a, b) { 5866 if (a.visProp.layer !== b.visProp.layer) { 5867 return a.visProp.layer - b.visProp.layer; 5868 } 5869 5870 // The objects are in the same layer, but the layer is not depth ordered 5871 if (depth_order_layers.indexOf(a.visProp.layer) === -1) { 5872 return a._pos - b._pos; 5873 } 5874 5875 // The objects are in the same layer and the layer is depth ordered 5876 // We have to sort 2D elements according to the zIndices of 5877 // their 3D parents. 5878 if (!a.visProp.element3d && !b.visProp.element3d) { 5879 return a._pos - b._pos; 5880 } 5881 5882 if (a.visProp.element3d && !b.visProp.element3d) { 5883 return -1; 5884 } 5885 5886 if (b.visProp.element3d && !a.visProp.element3d) { 5887 return 1; 5888 } 5889 5890 return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex; 5891 }; 5892 5893 // Only one view3d element is supported. Get the depth orderer layers and 5894 // update the zIndices of the 3D elements. 5895 for (el = 0; el < olen; el++) { 5896 pEl = this.objectsList[el]; 5897 if (pEl.elType === 'view3d' && pEl.evalVisProp('depthorder.enabled')) { 5898 depth_order_layers = pEl.evalVisProp('depthorder.layers'); 5899 pEl.updateRenderer(); 5900 break; 5901 } 5902 } 5903 5904 objects_sorted = this.objectsList.toSorted(_compareFn); 5905 olen = objects_sorted.length; 5906 for (el = 0; el < olen; el++) { 5907 objects_sorted[el].prepareUpdate().updateRenderer(); 5908 } 5909 5910 // for (i = 0; i < len; i++) { 5911 // minim = Number.POSITIVE_INFINITY; 5912 5913 // for (lay in layers) { 5914 // if (layers.hasOwnProperty(lay)) { 5915 // if (layers[lay] > last && layers[lay] < minim) { 5916 // minim = layers[lay]; 5917 // } 5918 // } 5919 // } 5920 5921 // for (el = 0; el < olen; el++) { 5922 // pEl = this.objectsList[el]; 5923 // if (pEl.visProp.layer === minim) { 5924 // pEl.prepareUpdate().updateRenderer(); 5925 // } 5926 // } 5927 // last = minim; 5928 // } 5929 5930 return this; 5931 }, 5932 5933 /** 5934 * Please use {@link JXG.Board.on} instead. 5935 * @param {Function} hook A function to be called by the board after an update occurred. 5936 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 5937 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 5938 * board object the hook is attached to. 5939 * @returns {Number} Id of the hook, required to remove the hook from the board. 5940 * @deprecated 5941 */ 5942 addHook: function (hook, m, context) { 5943 JXG.deprecated('Board.addHook()', 'Board.on()'); 5944 m = Type.def(m, 'update'); 5945 5946 context = Type.def(context, this); 5947 5948 this.hooks.push([m, hook]); 5949 this.on(m, hook, context); 5950 5951 return this.hooks.length - 1; 5952 }, 5953 5954 /** 5955 * Alias of {@link JXG.Board.on}. 5956 */ 5957 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 5958 5959 /** 5960 * Please use {@link JXG.Board.off} instead. 5961 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 5962 * @returns {JXG.Board} Reference to the board 5963 * @deprecated 5964 */ 5965 removeHook: function (id) { 5966 JXG.deprecated('Board.removeHook()', 'Board.off()'); 5967 if (this.hooks[id]) { 5968 this.off(this.hooks[id][0], this.hooks[id][1]); 5969 this.hooks[id] = null; 5970 } 5971 5972 return this; 5973 }, 5974 5975 /** 5976 * Alias of {@link JXG.Board.off}. 5977 */ 5978 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 5979 5980 /** 5981 * Runs through all hooked functions and calls them. 5982 * @returns {JXG.Board} Reference to the board 5983 * @deprecated 5984 */ 5985 updateHooks: function (m) { 5986 var arg = Array.prototype.slice.call(arguments, 0); 5987 5988 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 5989 5990 arg[0] = Type.def(arg[0], 'update'); 5991 this.triggerEventHandlers([arg[0]], arguments); 5992 5993 return this; 5994 }, 5995 5996 /** 5997 * Adds a dependent board to this board. 5998 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 5999 * @returns {JXG.Board} Reference to the board 6000 */ 6001 addChild: function (board) { 6002 if (Type.exists(board) && Type.exists(board.containerObj)) { 6003 this.dependentBoards.push(board); 6004 this.update(); 6005 } 6006 return this; 6007 }, 6008 6009 /** 6010 * Deletes a board from the list of dependent boards. 6011 * @param {JXG.Board} board Reference to the board which will be removed. 6012 * @returns {JXG.Board} Reference to the board 6013 */ 6014 removeChild: function (board) { 6015 var i; 6016 6017 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 6018 if (this.dependentBoards[i] === board) { 6019 this.dependentBoards.splice(i, 1); 6020 } 6021 } 6022 return this; 6023 }, 6024 6025 /** 6026 * Runs through most elements and calls their update() method and update the conditions. 6027 * @param {JXG.GeometryElement} [drag] Element that caused the update. 6028 * @returns {JXG.Board} Reference to the board 6029 */ 6030 update: function (drag) { 6031 var i, len, b, insert, storeActiveEl; 6032 6033 if (this.inUpdate || this.isSuspendedUpdate) { 6034 return this; 6035 } 6036 this.inUpdate = true; 6037 6038 if ( 6039 this.attr.minimizereflow === 'all' && 6040 this.containerObj && 6041 this.renderer.type !== 'vml' 6042 ) { 6043 storeActiveEl = this.document.activeElement; // Store focus element 6044 insert = this.renderer.removeToInsertLater(this.containerObj); 6045 } 6046 6047 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 6048 storeActiveEl = this.document.activeElement; 6049 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 6050 } 6051 6052 this.prepareUpdate(drag).updateElements(drag).updateConditions(); 6053 6054 this.renderer.suspendRedraw(this); 6055 this.updateRenderer(); 6056 this.renderer.unsuspendRedraw(); 6057 this.triggerEventHandlers(['update'], []); 6058 6059 if (insert) { 6060 insert(); 6061 storeActiveEl.focus(); // Restore focus element 6062 } 6063 6064 // To resolve dependencies between boards 6065 // for (var board in JXG.boards) { 6066 len = this.dependentBoards.length; 6067 for (i = 0; i < len; i++) { 6068 b = this.dependentBoards[i]; 6069 if (Type.exists(b) && b !== this) { 6070 b.updateQuality = this.updateQuality; 6071 b.prepareUpdate().updateElements().updateConditions(); 6072 b.renderer.suspendRedraw(this); 6073 b.updateRenderer(); 6074 b.renderer.unsuspendRedraw(); 6075 b.triggerEventHandlers(['update'], []); 6076 } 6077 } 6078 6079 this.inUpdate = false; 6080 return this; 6081 }, 6082 6083 /** 6084 * Runs through all elements and calls their update() method and update the conditions. 6085 * This is necessary after zooming and changing the bounding box. 6086 * @returns {JXG.Board} Reference to the board 6087 */ 6088 fullUpdate: function () { 6089 this.needsFullUpdate = true; 6090 this.update(); 6091 this.needsFullUpdate = false; 6092 return this; 6093 }, 6094 6095 /** 6096 * Adds a grid to the board according to the settings given in board.options. 6097 * @returns {JXG.Board} Reference to the board. 6098 */ 6099 addGrid: function () { 6100 this.create('grid', []); 6101 6102 return this; 6103 }, 6104 6105 /** 6106 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 6107 * more of the grids. 6108 * @returns {JXG.Board} Reference to the board object. 6109 */ 6110 removeGrids: function () { 6111 var i; 6112 6113 for (i = 0; i < this.grids.length; i++) { 6114 this.removeObject(this.grids[i]); 6115 } 6116 6117 this.grids.length = 0; 6118 this.update(); // required for canvas renderer 6119 6120 return this; 6121 }, 6122 6123 /** 6124 * Creates a new geometric element of type elementType. 6125 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 6126 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 6127 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 6128 * methods for a list of possible parameters. 6129 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 6130 * Common attributes are name, visible, strokeColor. 6131 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 6132 * two or more elements. 6133 */ 6134 create: function (elementType, parents, attributes) { 6135 var el, i; 6136 6137 elementType = elementType.toLowerCase(); 6138 6139 if (!Type.exists(parents)) { 6140 parents = []; 6141 } 6142 6143 if (!Type.exists(attributes)) { 6144 attributes = {}; 6145 } 6146 6147 for (i = 0; i < parents.length; i++) { 6148 if ( 6149 Type.isString(parents[i]) && 6150 !(elementType === 'text' && i === 2) && 6151 !(elementType === 'solidofrevolution3d' && i === 2) && 6152 !(elementType === 'text3d' && (i === 2 || i === 4)) && 6153 !( 6154 (elementType === 'input' || 6155 elementType === 'checkbox' || 6156 elementType === 'button') && 6157 (i === 2 || i === 3) 6158 ) && 6159 !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the 6160 // variable name 6161 !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y' 6162 !(elementType === 'implicitcurve') 6163 ) { 6164 if (i > 0 && parents[0].elType === 'view3d') { 6165 // 3D elements are based on 3D elements, only 6166 parents[i] = parents[0].select(parents[i]); 6167 } else { 6168 parents[i] = this.select(parents[i]); 6169 } 6170 } 6171 } 6172 6173 if (Type.isFunction(JXG.elements[elementType])) { 6174 el = JXG.elements[elementType](this, parents, attributes); 6175 } else { 6176 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType); 6177 } 6178 6179 if (!Type.exists(el)) { 6180 JXG.debug('JSXGraph: create: failure creating ' + elementType); 6181 return el; 6182 } 6183 6184 if (el.prepareUpdate && el.update && el.updateRenderer) { 6185 el.fullUpdate(); 6186 } 6187 return el; 6188 }, 6189 6190 /** 6191 * Deprecated name for {@link JXG.Board.create}. 6192 * @deprecated 6193 */ 6194 createElement: function () { 6195 JXG.deprecated('Board.createElement()', 'Board.create()'); 6196 return this.create.apply(this, arguments); 6197 }, 6198 6199 /** 6200 * Delete the elements drawn as part of a trace of an element. 6201 * @returns {JXG.Board} Reference to the board 6202 */ 6203 clearTraces: function () { 6204 var el; 6205 6206 for (el = 0; el < this.objectsList.length; el++) { 6207 this.objectsList[el].clearTrace(); 6208 } 6209 6210 this.numTraces = 0; 6211 return this; 6212 }, 6213 6214 /** 6215 * Stop updates of the board. 6216 * @returns {JXG.Board} Reference to the board 6217 */ 6218 suspendUpdate: function () { 6219 if (!this.inUpdate) { 6220 this.isSuspendedUpdate = true; 6221 } 6222 return this; 6223 }, 6224 6225 /** 6226 * Enable updates of the board. 6227 * @returns {JXG.Board} Reference to the board 6228 */ 6229 unsuspendUpdate: function () { 6230 if (this.isSuspendedUpdate) { 6231 this.isSuspendedUpdate = false; 6232 this.fullUpdate(); 6233 } 6234 return this; 6235 }, 6236 6237 /** 6238 * Set the bounding box of the board. 6239 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 6240 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 6241 * the resulting viewport may be larger. 6242 * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset' 6243 * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0). 6244 * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing. 6245 * @returns {JXG.Board} Reference to the board 6246 */ 6247 setBoundingBox: function (bbox, keepaspectratio, setZoom) { 6248 var h, w, ux, uy, 6249 offX = 0, 6250 offY = 0, 6251 zoom_ratio = 1, 6252 ratio, dx, dy, prev_w, prev_h, 6253 dim = Env.getDimensions(this.containerObj, this.document); 6254 6255 if (!Type.isArray(bbox)) { 6256 return this; 6257 } 6258 6259 if ( 6260 bbox[0] < this.maxboundingbox[0] - Mat.eps || 6261 bbox[1] > this.maxboundingbox[1] + Mat.eps || 6262 bbox[2] > this.maxboundingbox[2] + Mat.eps || 6263 bbox[3] < this.maxboundingbox[3] - Mat.eps 6264 ) { 6265 return this; 6266 } 6267 6268 if (!Type.exists(setZoom)) { 6269 setZoom = 'reset'; 6270 } 6271 6272 ux = this.unitX; 6273 uy = this.unitY; 6274 this.canvasWidth = parseFloat(dim.width); // parseInt(dim.width, 10); 6275 this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10); 6276 w = this.canvasWidth; 6277 h = this.canvasHeight; 6278 if (keepaspectratio) { 6279 if (this.keepaspectratio) { 6280 ratio = ux / uy; // Keep this ratio if keepaspectratio was true 6281 if (isNaN(ratio)) { 6282 ratio = 1.0; 6283 } 6284 } else { 6285 ratio = 1.0; 6286 } 6287 if (setZoom === 'keep') { 6288 zoom_ratio = this.zoomX / this.zoomY; 6289 } 6290 dx = bbox[2] - bbox[0]; 6291 dy = bbox[1] - bbox[3]; 6292 prev_w = ux * dx; 6293 prev_h = uy * dy; 6294 if (w >= h) { 6295 if (prev_w >= prev_h) { 6296 this.unitY = h / dy; 6297 this.unitX = this.unitY * ratio; 6298 } else { 6299 // Switch dominating interval 6300 this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio; 6301 this.unitX = this.unitY * ratio; 6302 } 6303 } else { 6304 if (prev_h > prev_w) { 6305 this.unitX = w / dx; 6306 this.unitY = this.unitX / ratio; 6307 } else { 6308 // Switch dominating interval 6309 this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio; 6310 this.unitY = this.unitX / ratio; 6311 } 6312 } 6313 // Add the additional units in equal portions left and right 6314 offX = (w / this.unitX - dx) * 0.5; 6315 // Add the additional units in equal portions above and below 6316 offY = (h / this.unitY - dy) * 0.5; 6317 this.keepaspectratio = true; 6318 } else { 6319 this.unitX = w / (bbox[2] - bbox[0]); 6320 this.unitY = h / (bbox[1] - bbox[3]); 6321 this.keepaspectratio = false; 6322 } 6323 6324 this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY)); 6325 6326 if (setZoom === 'update') { 6327 this.zoomX *= this.unitX / ux; 6328 this.zoomY *= this.unitY / uy; 6329 } else if (setZoom === 'reset') { 6330 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0; 6331 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0; 6332 } 6333 6334 return this; 6335 }, 6336 6337 /** 6338 * Get the bounding box of the board. 6339 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 6340 */ 6341 getBoundingBox: function () { 6342 var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords, 6343 lr = new Coords( 6344 Const.COORDS_BY_SCREEN, 6345 [this.canvasWidth, this.canvasHeight], 6346 this 6347 ).usrCoords; 6348 return [ul[1], ul[2], lr[1], lr[2]]; 6349 }, 6350 6351 /** 6352 * Sets the value of attribute <tt>key</tt> to <tt>value</tt>. 6353 * @param {String} key The attribute's name. 6354 * @param value The new value 6355 * @private 6356 */ 6357 _set: function (key, value) { 6358 key = key.toLocaleLowerCase(); 6359 6360 if ( 6361 value !== null && 6362 Type.isObject(value) && 6363 !Type.exists(value.id) && 6364 !Type.exists(value.name) 6365 ) { 6366 // value is of type {prop: val, prop: val,...} 6367 // Convert these attributes to lowercase, too 6368 // this.attr[key] = {}; 6369 // for (el in value) { 6370 // if (value.hasOwnProperty(el)) { 6371 // this.attr[key][el.toLocaleLowerCase()] = value[el]; 6372 // } 6373 // } 6374 Type.mergeAttr(this.attr[key], value); 6375 } else { 6376 this.attr[key] = value; 6377 } 6378 }, 6379 6380 /** 6381 * Sets an arbitrary number of attributes. This method has one or more 6382 * parameters of the following types: 6383 * <ul> 6384 * <li> object: {key1:value1,key2:value2,...} 6385 * <li> string: 'key:value' 6386 * <li> array: ['key', value] 6387 * </ul> 6388 * Some board attributes are immutable, like e.g. the renderer type. 6389 * 6390 * @param {Object} attributes An object with attributes. 6391 * @returns {JXG.Board} Reference to the board 6392 * 6393 * @example 6394 * const board = JXG.JSXGraph.initBoard('jxgbox', { 6395 * boundingbox: [-5, 5, 5, -5], 6396 * keepAspectRatio: false, 6397 * axis:true, 6398 * showFullscreen: true, 6399 * showScreenshot: true, 6400 * showCopyright: false 6401 * }); 6402 * 6403 * board.setAttribute({ 6404 * animationDelay: 10, 6405 * boundingbox: [-10, 5, 10, -5], 6406 * defaultAxes: { 6407 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6408 * }, 6409 * description: 'test', 6410 * fullscreen: { 6411 * scale: 0.5 6412 * }, 6413 * intl: { 6414 * enabled: true, 6415 * locale: 'de-DE' 6416 * } 6417 * }); 6418 * 6419 * board.setAttribute({ 6420 * selection: { 6421 * enabled: true, 6422 * fillColor: 'blue' 6423 * }, 6424 * showInfobox: false, 6425 * zoomX: 0.5, 6426 * zoomY: 2, 6427 * fullscreen: { symbol: 'x' }, 6428 * screenshot: { symbol: 'y' }, 6429 * showCopyright: true, 6430 * showFullscreen: false, 6431 * showScreenshot: false, 6432 * showZoom: false, 6433 * showNavigation: false 6434 * }); 6435 * board.setAttribute('showCopyright:false'); 6436 * 6437 * var p = board.create('point', [1, 1], {size: 10, 6438 * label: { 6439 * fontSize: 24, 6440 * highlightStrokeOpacity: 0.1, 6441 * offset: [5, 0] 6442 * } 6443 * }); 6444 * 6445 * 6446 * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div> 6447 * <script type="text/javascript"> 6448 * (function() { 6449 * const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', { 6450 * boundingbox: [-5, 5, 5, -5], 6451 * keepAspectRatio: false, 6452 * axis:true, 6453 * showFullscreen: true, 6454 * showScreenshot: true, 6455 * showCopyright: false 6456 * }); 6457 * 6458 * board.setAttribute({ 6459 * animationDelay: 10, 6460 * boundingbox: [-10, 5, 10, -5], 6461 * defaultAxes: { 6462 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6463 * }, 6464 * description: 'test', 6465 * fullscreen: { 6466 * scale: 0.5 6467 * }, 6468 * intl: { 6469 * enabled: true, 6470 * locale: 'de-DE' 6471 * } 6472 * }); 6473 * 6474 * board.setAttribute({ 6475 * selection: { 6476 * enabled: true, 6477 * fillColor: 'blue' 6478 * }, 6479 * showInfobox: false, 6480 * zoomX: 0.5, 6481 * zoomY: 2, 6482 * fullscreen: { symbol: 'x' }, 6483 * screenshot: { symbol: 'y' }, 6484 * showCopyright: true, 6485 * showFullscreen: false, 6486 * showScreenshot: false, 6487 * showZoom: false, 6488 * showNavigation: false 6489 * }); 6490 * 6491 * board.setAttribute('showCopyright:false'); 6492 * 6493 * var p = board.create('point', [1, 1], {size: 10, 6494 * label: { 6495 * fontSize: 24, 6496 * highlightStrokeOpacity: 0.1, 6497 * offset: [5, 0] 6498 * } 6499 * }); 6500 * 6501 * 6502 * })(); 6503 * 6504 * </script><pre> 6505 * 6506 * 6507 */ 6508 setAttribute: function (attr) { 6509 var i, arg, pair, 6510 key, value, oldvalue,// j, le, 6511 node, 6512 attributes = {}; 6513 6514 // Normalize the user input 6515 for (i = 0; i < arguments.length; i++) { 6516 arg = arguments[i]; 6517 if (Type.isString(arg)) { 6518 // pairRaw is string of the form 'key:value' 6519 pair = arg.split(":"); 6520 attributes[Type.trim(pair[0])] = Type.trim(pair[1]); 6521 } else if (!Type.isArray(arg)) { 6522 // pairRaw consists of objects of the form {key1:value1,key2:value2,...} 6523 JXG.extend(attributes, arg); 6524 } else { 6525 // pairRaw consists of array [key,value] 6526 attributes[arg[0]] = arg[1]; 6527 } 6528 } 6529 6530 for (i in attributes) { 6531 if (attributes.hasOwnProperty(i)) { 6532 key = i.replace(/\s+/g, "").toLowerCase(); 6533 value = attributes[i]; 6534 } 6535 value = (value.toLowerCase && value.toLowerCase() === 'false') 6536 ? false 6537 : value; 6538 6539 oldvalue = this.attr[key]; 6540 if (oldvalue === value) { 6541 continue; 6542 } 6543 switch (key) { 6544 case 'axis': 6545 if (value === false) { 6546 if (Type.exists(this.defaultAxes)) { 6547 this.defaultAxes.x.setAttribute({ visible: false }); 6548 this.defaultAxes.y.setAttribute({ visible: false }); 6549 } 6550 } else { 6551 // TODO 6552 } 6553 break; 6554 case 'boundingbox': 6555 this.setBoundingBox(value, this.keepaspectratio); 6556 this._set(key, value); 6557 break; 6558 case 'defaultaxes': 6559 if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) { 6560 this.defaultAxes.x.setAttribute(value.x); 6561 } 6562 if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) { 6563 this.defaultAxes.y.setAttribute(value.y); 6564 } 6565 break; 6566 case 'title': 6567 this.document.getElementById(this.container + '_ARIAlabel') 6568 .innerHTML = value; 6569 this._set(key, value); 6570 break; 6571 case 'keepaspectratio': 6572 this._set(key, value); 6573 this.setBoundingBox(this.getBoundingBox(), value, 'keep'); 6574 break; 6575 6576 /* eslint-disable no-fallthrough */ 6577 case 'document': 6578 case 'maxboundingbox': 6579 this[key] = value; 6580 this._set(key, value); 6581 break; 6582 6583 case 'zoomx': 6584 case 'zoomy': 6585 this[key] = value; 6586 this._set(key, value); 6587 this.setZoom(this.attr.zoomx, this.attr.zoomy); 6588 break; 6589 6590 case 'registerevents': 6591 case 'renderer': 6592 // immutable, i.e. ignored 6593 break; 6594 6595 case 'fullscreen': 6596 case 'screenshot': 6597 node = this.containerObj.ownerDocument.getElementById( 6598 this.container + '_navigation_' + key); 6599 if (node && Type.exists(value.symbol)) { 6600 node.innerHTML = Type.evaluate(value.symbol); 6601 } 6602 this._set(key, value); 6603 break; 6604 6605 case 'selection': 6606 value.visible = false; 6607 value.withLines = false; 6608 value.vertices = { visible: false }; 6609 this._set(key, value); 6610 break; 6611 6612 case 'showcopyright': 6613 if (this.renderer.type === 'svg') { 6614 node = this.containerObj.ownerDocument.getElementById( 6615 this.renderer.uniqName('licenseText') 6616 ); 6617 if (node) { 6618 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6619 } else if (Type.evaluate(value)) { 6620 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 6621 } 6622 } 6623 6624 default: 6625 if (Type.exists(this.attr[key])) { 6626 this._set(key, value); 6627 } 6628 break; 6629 /* eslint-enable no-fallthrough */ 6630 } 6631 } 6632 6633 // Redraw navbar to handle the remaining show* attributes 6634 this.containerObj.ownerDocument.getElementById( 6635 this.container + "_navigationbar" 6636 ).remove(); 6637 this.renderer.drawNavigationBar(this, this.attr.navbar); 6638 6639 this.triggerEventHandlers(["attribute"], [attributes, this]); 6640 this.fullUpdate(); 6641 6642 return this; 6643 }, 6644 6645 /** 6646 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 6647 * animated elements. This function tells the board about new elements to animate. 6648 * @param {JXG.GeometryElement} element The element which is to be animated. 6649 * @returns {JXG.Board} Reference to the board 6650 */ 6651 addAnimation: function (element) { 6652 var that = this; 6653 6654 this.animationObjects[element.id] = element; 6655 6656 if (!this.animationIntervalCode) { 6657 this.animationIntervalCode = window.setInterval(function () { 6658 that.animate(); 6659 }, element.board.attr.animationdelay); 6660 } 6661 6662 return this; 6663 }, 6664 6665 /** 6666 * Cancels all running animations. 6667 * @returns {JXG.Board} Reference to the board 6668 */ 6669 stopAllAnimation: function () { 6670 var el; 6671 6672 for (el in this.animationObjects) { 6673 if ( 6674 this.animationObjects.hasOwnProperty(el) && 6675 Type.exists(this.animationObjects[el]) 6676 ) { 6677 this.animationObjects[el] = null; 6678 delete this.animationObjects[el]; 6679 } 6680 } 6681 6682 window.clearInterval(this.animationIntervalCode); 6683 delete this.animationIntervalCode; 6684 6685 return this; 6686 }, 6687 6688 /** 6689 * General purpose animation function. This currently only supports moving points from one place to another. This 6690 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 6691 * @returns {JXG.Board} Reference to the board 6692 */ 6693 animate: function () { 6694 var props, 6695 el, 6696 o, 6697 newCoords, 6698 r, 6699 p, 6700 c, 6701 cbtmp, 6702 count = 0, 6703 obj = null; 6704 6705 for (el in this.animationObjects) { 6706 if ( 6707 this.animationObjects.hasOwnProperty(el) && 6708 Type.exists(this.animationObjects[el]) 6709 ) { 6710 count += 1; 6711 o = this.animationObjects[el]; 6712 6713 if (o.animationPath) { 6714 if (Type.isFunction(o.animationPath)) { 6715 newCoords = o.animationPath( 6716 new Date().getTime() - o.animationStart 6717 ); 6718 } else { 6719 newCoords = o.animationPath.pop(); 6720 } 6721 6722 if ( 6723 !Type.exists(newCoords) || 6724 (!Type.isArray(newCoords) && isNaN(newCoords)) 6725 ) { 6726 delete o.animationPath; 6727 } else { 6728 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 6729 o.fullUpdate(); 6730 obj = o; 6731 } 6732 } 6733 if (o.animationData) { 6734 c = 0; 6735 6736 for (r in o.animationData) { 6737 if (o.animationData.hasOwnProperty(r)) { 6738 p = o.animationData[r].pop(); 6739 6740 if (!Type.exists(p)) { 6741 delete o.animationData[p]; 6742 } else { 6743 c += 1; 6744 props = {}; 6745 props[r] = p; 6746 o.setAttribute(props); 6747 } 6748 } 6749 } 6750 6751 if (c === 0) { 6752 delete o.animationData; 6753 } 6754 } 6755 6756 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 6757 this.animationObjects[el] = null; 6758 delete this.animationObjects[el]; 6759 6760 if (Type.exists(o.animationCallback)) { 6761 cbtmp = o.animationCallback; 6762 o.animationCallback = null; 6763 cbtmp(); 6764 } 6765 } 6766 } 6767 } 6768 6769 if (count === 0) { 6770 window.clearInterval(this.animationIntervalCode); 6771 delete this.animationIntervalCode; 6772 } else { 6773 this.update(obj); 6774 } 6775 6776 return this; 6777 }, 6778 6779 /** 6780 * Migrate the dependency properties of the point src 6781 * to the point dest and delete the point src. 6782 * For example, a circle around the point src 6783 * receives the new center dest. The old center src 6784 * will be deleted. 6785 * @param {JXG.Point} src Original point which will be deleted 6786 * @param {JXG.Point} dest New point with the dependencies of src. 6787 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 6788 * dest element. 6789 * @returns {JXG.Board} Reference to the board 6790 */ 6791 migratePoint: function (src, dest, copyName) { 6792 var child, 6793 childId, 6794 prop, 6795 found, 6796 i, 6797 srcLabelId, 6798 srcHasLabel = false; 6799 6800 src = this.select(src); 6801 dest = this.select(dest); 6802 6803 if (Type.exists(src.label)) { 6804 srcLabelId = src.label.id; 6805 srcHasLabel = true; 6806 this.removeObject(src.label); 6807 } 6808 6809 for (childId in src.childElements) { 6810 if (src.childElements.hasOwnProperty(childId)) { 6811 child = src.childElements[childId]; 6812 found = false; 6813 6814 for (prop in child) { 6815 if (child.hasOwnProperty(prop)) { 6816 if (child[prop] === src) { 6817 child[prop] = dest; 6818 found = true; 6819 } 6820 } 6821 } 6822 6823 if (found) { 6824 delete src.childElements[childId]; 6825 } 6826 6827 for (i = 0; i < child.parents.length; i++) { 6828 if (child.parents[i] === src.id) { 6829 child.parents[i] = dest.id; 6830 } 6831 } 6832 6833 dest.addChild(child); 6834 } 6835 } 6836 6837 // The destination object should receive the name 6838 // and the label of the originating (src) object 6839 if (copyName) { 6840 if (srcHasLabel) { 6841 delete dest.childElements[srcLabelId]; 6842 delete dest.descendants[srcLabelId]; 6843 } 6844 6845 if (dest.label) { 6846 this.removeObject(dest.label); 6847 } 6848 6849 delete this.elementsByName[dest.name]; 6850 dest.name = src.name; 6851 if (srcHasLabel) { 6852 dest.createLabel(); 6853 } 6854 } 6855 6856 this.removeObject(src); 6857 6858 if (Type.exists(dest.name) && dest.name !== '') { 6859 this.elementsByName[dest.name] = dest; 6860 } 6861 6862 this.fullUpdate(); 6863 6864 return this; 6865 }, 6866 6867 /** 6868 * Initializes color blindness simulation. 6869 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 6870 * @returns {JXG.Board} Reference to the board 6871 */ 6872 emulateColorblindness: function (deficiency) { 6873 var e, o; 6874 6875 if (!Type.exists(deficiency)) { 6876 deficiency = 'none'; 6877 } 6878 6879 if (this.currentCBDef === deficiency) { 6880 return this; 6881 } 6882 6883 for (e in this.objects) { 6884 if (this.objects.hasOwnProperty(e)) { 6885 o = this.objects[e]; 6886 6887 if (deficiency !== 'none') { 6888 if (this.currentCBDef === 'none') { 6889 // this could be accomplished by JXG.extend, too. But do not use 6890 // JXG.deepCopy as this could result in an infinite loop because in 6891 // visProp there could be geometry elements which contain the board which 6892 // contains all objects which contain board etc. 6893 o.visPropOriginal = { 6894 strokecolor: o.visProp.strokecolor, 6895 fillcolor: o.visProp.fillcolor, 6896 highlightstrokecolor: o.visProp.highlightstrokecolor, 6897 highlightfillcolor: o.visProp.highlightfillcolor 6898 }; 6899 } 6900 o.setAttribute({ 6901 strokecolor: Color.rgb2cb( 6902 o.eval(o.visPropOriginal.strokecolor), 6903 deficiency 6904 ), 6905 fillcolor: Color.rgb2cb( 6906 o.eval(o.visPropOriginal.fillcolor), 6907 deficiency 6908 ), 6909 highlightstrokecolor: Color.rgb2cb( 6910 o.eval(o.visPropOriginal.highlightstrokecolor), 6911 deficiency 6912 ), 6913 highlightfillcolor: Color.rgb2cb( 6914 o.eval(o.visPropOriginal.highlightfillcolor), 6915 deficiency 6916 ) 6917 }); 6918 } else if (Type.exists(o.visPropOriginal)) { 6919 JXG.extend(o.visProp, o.visPropOriginal); 6920 } 6921 } 6922 } 6923 this.currentCBDef = deficiency; 6924 this.update(); 6925 6926 return this; 6927 }, 6928 6929 /** 6930 * Select a single or multiple elements at once. 6931 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 6932 * be used as a filter to return multiple elements at once filtered by the properties of the object. 6933 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 6934 * The advanced filters consisting of objects or functions are ignored. 6935 * @returns {JXG.GeometryElement|JXG.Composition} 6936 * @example 6937 * // select the element with name A 6938 * board.select('A'); 6939 * 6940 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 6941 * board.select({ 6942 * strokeColor: 'red' 6943 * }); 6944 * 6945 * // select all points on or below the x axis and make them black. 6946 * board.select({ 6947 * elementClass: JXG.OBJECT_CLASS_POINT, 6948 * Y: function (v) { 6949 * return v <= 0; 6950 * } 6951 * }).setAttribute({color: 'black'}); 6952 * 6953 * // select all elements 6954 * board.select(function (el) { 6955 * return true; 6956 * }); 6957 */ 6958 select: function (str, onlyByIdOrName) { 6959 var flist, 6960 olist, 6961 i, 6962 l, 6963 s = str; 6964 6965 if (s === null) { 6966 return s; 6967 } 6968 6969 // It's a string, most likely an id or a name. 6970 if (Type.isString(s) && s !== '') { 6971 // Search by ID 6972 if (Type.exists(this.objects[s])) { 6973 s = this.objects[s]; 6974 // Search by name 6975 } else if (Type.exists(this.elementsByName[s])) { 6976 s = this.elementsByName[s]; 6977 // Search by group ID 6978 } else if (Type.exists(this.groups[s])) { 6979 s = this.groups[s]; 6980 } 6981 6982 // It's a function or an object, but not an element 6983 } else if ( 6984 !onlyByIdOrName && 6985 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 6986 ) { 6987 flist = Type.filterElements(this.objectsList, s); 6988 6989 olist = {}; 6990 l = flist.length; 6991 for (i = 0; i < l; i++) { 6992 olist[flist[i].id] = flist[i]; 6993 } 6994 s = new Composition(olist); 6995 6996 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 6997 } else if ( 6998 Type.isObject(s) && 6999 Type.exists(s.id) && 7000 !Type.exists(this.objects[s.id]) 7001 ) { 7002 s = null; 7003 } 7004 7005 return s; 7006 }, 7007 7008 /** 7009 * Checks if the given point is inside the boundingbox. 7010 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 7011 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 7012 * @returns {Boolean} 7013 */ 7014 hasPoint: function (x, y) { 7015 var px = x, 7016 py = y, 7017 bbox = this.getBoundingBox(); 7018 7019 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 7020 px = x.usrCoords[1]; 7021 py = x.usrCoords[2]; 7022 } 7023 7024 return !!( 7025 Type.isNumber(px) && 7026 Type.isNumber(py) && 7027 bbox[0] < px && 7028 px < bbox[2] && 7029 bbox[1] > py && 7030 py > bbox[3] 7031 ); 7032 }, 7033 7034 /** 7035 * Update CSS transformations of type scaling. It is used to correct the mouse position 7036 * in {@link JXG.Board.getMousePosition}. 7037 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 7038 * 7039 * It is up to the user to call this method after an update of the CSS transformation 7040 * in the DOM. 7041 */ 7042 updateCSSTransforms: function () { 7043 var obj = this.containerObj, 7044 o = obj, 7045 o2 = obj; 7046 7047 this.cssTransMat = Env.getCSSTransformMatrix(o); 7048 7049 // Newer variant of walking up the tree. 7050 // We walk up all parent nodes and collect possible CSS transforms. 7051 // Works also for ShadowDOM 7052 if (Type.exists(o.getRootNode)) { 7053 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7054 while (o) { 7055 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7056 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7057 } 7058 this.cssTransMat = Mat.inverse(this.cssTransMat); 7059 } else { 7060 /* 7061 * This is necessary for IE11 7062 */ 7063 o = o.offsetParent; 7064 while (o) { 7065 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7066 7067 o2 = o2.parentNode; 7068 while (o2 !== o) { 7069 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7070 o2 = o2.parentNode; 7071 } 7072 o = o.offsetParent; 7073 } 7074 this.cssTransMat = Mat.inverse(this.cssTransMat); 7075 } 7076 return this; 7077 }, 7078 7079 /** 7080 * Start selection mode. This function can either be triggered from outside or by 7081 * a down event together with correct key pressing. The default keys are 7082 * shift+ctrl. But this can be changed in the options. 7083 * 7084 * Starting from out side can be realized for example with a button like this: 7085 * <pre> 7086 * <button onclick='board.startSelectionMode()'>Start</button> 7087 * </pre> 7088 * @example 7089 * // 7090 * // Set a new bounding box from the selection rectangle 7091 * // 7092 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7093 * boundingBox:[-3,2,3,-2], 7094 * keepAspectRatio: false, 7095 * axis:true, 7096 * selection: { 7097 * enabled: true, 7098 * needShift: false, 7099 * needCtrl: true, 7100 * withLines: false, 7101 * vertices: { 7102 * visible: false 7103 * }, 7104 * fillColor: '#ffff00', 7105 * } 7106 * }); 7107 * 7108 * var f = function f(x) { return Math.cos(x); }, 7109 * curve = board.create('functiongraph', [f]); 7110 * 7111 * board.on('stopselecting', function(){ 7112 * var box = board.stopSelectionMode(), 7113 * 7114 * // bbox has the coordinates of the selection rectangle. 7115 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7116 * // are homogeneous coordinates. 7117 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7118 * 7119 * // Set a new bounding box 7120 * board.setBoundingBox(bbox, false); 7121 * }); 7122 * 7123 * 7124 * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div> 7125 * <script type='text/javascript'> 7126 * (function() { 7127 * // 7128 * // Set a new bounding box from the selection rectangle 7129 * // 7130 * var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 7131 * boundingBox:[-3,2,3,-2], 7132 * keepAspectRatio: false, 7133 * axis:true, 7134 * selection: { 7135 * enabled: true, 7136 * needShift: false, 7137 * needCtrl: true, 7138 * withLines: false, 7139 * vertices: { 7140 * visible: false 7141 * }, 7142 * fillColor: '#ffff00', 7143 * } 7144 * }); 7145 * 7146 * var f = function f(x) { return Math.cos(x); }, 7147 * curve = board.create('functiongraph', [f]); 7148 * 7149 * board.on('stopselecting', function(){ 7150 * var box = board.stopSelectionMode(), 7151 * 7152 * // bbox has the coordinates of the selection rectangle. 7153 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7154 * // are homogeneous coordinates. 7155 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7156 * 7157 * // Set a new bounding box 7158 * board.setBoundingBox(bbox, false); 7159 * }); 7160 * })(); 7161 * 7162 * </script><pre> 7163 * 7164 */ 7165 startSelectionMode: function () { 7166 this.selectingMode = true; 7167 this.selectionPolygon.setAttribute({ visible: true }); 7168 this.selectingBox = [ 7169 [0, 0], 7170 [0, 0] 7171 ]; 7172 this._setSelectionPolygonFromBox(); 7173 this.selectionPolygon.fullUpdate(); 7174 }, 7175 7176 /** 7177 * Finalize the selection: disable selection mode and return the coordinates 7178 * of the selection rectangle. 7179 * @returns {Array} Coordinates of the selection rectangle. The array 7180 * contains two {@link JXG.Coords} objects. One the upper left corner and 7181 * the second for the lower right corner. 7182 */ 7183 stopSelectionMode: function () { 7184 this.selectingMode = false; 7185 this.selectionPolygon.setAttribute({ visible: false }); 7186 return [ 7187 this.selectionPolygon.vertices[0].coords, 7188 this.selectionPolygon.vertices[2].coords 7189 ]; 7190 }, 7191 7192 /** 7193 * Start the selection of a region. 7194 * @private 7195 * @param {Array} pos Screen coordiates of the upper left corner of the 7196 * selection rectangle. 7197 */ 7198 _startSelecting: function (pos) { 7199 this.isSelecting = true; 7200 this.selectingBox = [ 7201 [pos[0], pos[1]], 7202 [pos[0], pos[1]] 7203 ]; 7204 this._setSelectionPolygonFromBox(); 7205 }, 7206 7207 /** 7208 * Update the selection rectangle during a move event. 7209 * @private 7210 * @param {Array} pos Screen coordiates of the move event 7211 */ 7212 _moveSelecting: function (pos) { 7213 if (this.isSelecting) { 7214 this.selectingBox[1] = [pos[0], pos[1]]; 7215 this._setSelectionPolygonFromBox(); 7216 this.selectionPolygon.fullUpdate(); 7217 } 7218 }, 7219 7220 /** 7221 * Update the selection rectangle during an up event. Stop selection. 7222 * @private 7223 * @param {Object} evt Event object 7224 */ 7225 _stopSelecting: function (evt) { 7226 var pos = this.getMousePosition(evt); 7227 7228 this.isSelecting = false; 7229 this.selectingBox[1] = [pos[0], pos[1]]; 7230 this._setSelectionPolygonFromBox(); 7231 }, 7232 7233 /** 7234 * Update the Selection rectangle. 7235 * @private 7236 */ 7237 _setSelectionPolygonFromBox: function () { 7238 var A = this.selectingBox[0], 7239 B = this.selectingBox[1]; 7240 7241 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7242 A[0], 7243 A[1] 7244 ]); 7245 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7246 A[0], 7247 B[1] 7248 ]); 7249 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7250 B[0], 7251 B[1] 7252 ]); 7253 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7254 B[0], 7255 A[1] 7256 ]); 7257 }, 7258 7259 /** 7260 * Test if a down event should start a selection. Test if the 7261 * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called. 7262 * @param {Object} evt Event object 7263 */ 7264 _testForSelection: function (evt) { 7265 if (this._isRequiredKeyPressed(evt, 'selection')) { 7266 if (!Type.exists(this.selectionPolygon)) { 7267 this._createSelectionPolygon(this.attr); 7268 } 7269 this.startSelectionMode(); 7270 } 7271 }, 7272 7273 /** 7274 * Create the internal selection polygon, which will be available as board.selectionPolygon. 7275 * @private 7276 * @param {Object} attr board attributes, e.g. the subobject board.attr. 7277 * @returns {Object} pointer to the board to enable chaining. 7278 */ 7279 _createSelectionPolygon: function (attr) { 7280 var selectionattr; 7281 7282 if (!Type.exists(this.selectionPolygon)) { 7283 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 7284 if (selectionattr.enabled === true) { 7285 this.selectionPolygon = this.create( 7286 'polygon', 7287 [ 7288 [0, 0], 7289 [0, 0], 7290 [0, 0], 7291 [0, 0] 7292 ], 7293 selectionattr 7294 ); 7295 } 7296 } 7297 7298 return this; 7299 }, 7300 7301 /* ************************** 7302 * EVENT DEFINITION 7303 * for documentation purposes 7304 * ************************** */ 7305 7306 //region Event handler documentation 7307 7308 /** 7309 * @event 7310 * @description Whenever the {@link JXG.Board#setAttribute} is called. 7311 * @name JXG.Board#attribute 7312 * @param {Event} e The browser's event object. 7313 */ 7314 __evt__attribute: function (e) { }, 7315 7316 /** 7317 * @event 7318 * @description Whenever the user starts to touch or click the board. 7319 * @name JXG.Board#down 7320 * @param {Event} e The browser's event object. 7321 */ 7322 __evt__down: function (e) { }, 7323 7324 /** 7325 * @event 7326 * @description Whenever the user starts to click on the board. 7327 * @name JXG.Board#mousedown 7328 * @param {Event} e The browser's event object. 7329 */ 7330 __evt__mousedown: function (e) { }, 7331 7332 /** 7333 * @event 7334 * @description Whenever the user taps the pen on the board. 7335 * @name JXG.Board#pendown 7336 * @param {Event} e The browser's event object. 7337 */ 7338 __evt__pendown: function (e) { }, 7339 7340 /** 7341 * @event 7342 * @description Whenever the user starts to click on the board with a 7343 * device sending pointer events. 7344 * @name JXG.Board#pointerdown 7345 * @param {Event} e The browser's event object. 7346 */ 7347 __evt__pointerdown: function (e) { }, 7348 7349 /** 7350 * @event 7351 * @description Whenever the user starts to touch the board. 7352 * @name JXG.Board#touchstart 7353 * @param {Event} e The browser's event object. 7354 */ 7355 __evt__touchstart: function (e) { }, 7356 7357 /** 7358 * @event 7359 * @description Whenever the user stops to touch or click the board. 7360 * @name JXG.Board#up 7361 * @param {Event} e The browser's event object. 7362 */ 7363 __evt__up: function (e) { }, 7364 7365 /** 7366 * @event 7367 * @description Whenever the user releases the mousebutton over the board. 7368 * @name JXG.Board#mouseup 7369 * @param {Event} e The browser's event object. 7370 */ 7371 __evt__mouseup: function (e) { }, 7372 7373 /** 7374 * @event 7375 * @description Whenever the user releases the mousebutton over the board with a 7376 * device sending pointer events. 7377 * @name JXG.Board#pointerup 7378 * @param {Event} e The browser's event object. 7379 */ 7380 __evt__pointerup: function (e) { }, 7381 7382 /** 7383 * @event 7384 * @description Whenever the user stops touching the board. 7385 * @name JXG.Board#touchend 7386 * @param {Event} e The browser's event object. 7387 */ 7388 __evt__touchend: function (e) { }, 7389 7390 /** 7391 * @event 7392 * @description Whenever the user clicks on the board. 7393 * @name JXG.Board#click 7394 * @see JXG.Board#clickDelay 7395 * @param {Event} e The browser's event object. 7396 */ 7397 __evt__click: function (e) { }, 7398 7399 /** 7400 * @event 7401 * @description Whenever the user double clicks on the board. 7402 * This event works on desktop browser, but is undefined 7403 * on mobile browsers. 7404 * @name JXG.Board#dblclick 7405 * @see JXG.Board#clickDelay 7406 * @see JXG.Board#dblClickSuppressClick 7407 * @param {Event} e The browser's event object. 7408 */ 7409 __evt__dblclick: function (e) { }, 7410 7411 /** 7412 * @event 7413 * @description Whenever the user clicks on the board with a mouse device. 7414 * @name JXG.Board#mouseclick 7415 * @param {Event} e The browser's event object. 7416 */ 7417 __evt__mouseclick: function (e) { }, 7418 7419 /** 7420 * @event 7421 * @description Whenever the user double clicks on the board with a mouse device. 7422 * @name JXG.Board#mousedblclick 7423 * @see JXG.Board#clickDelay 7424 * @param {Event} e The browser's event object. 7425 */ 7426 __evt__mousedblclick: function (e) { }, 7427 7428 /** 7429 * @event 7430 * @description Whenever the user clicks on the board with a pointer device. 7431 * @name JXG.Board#pointerclick 7432 * @param {Event} e The browser's event object. 7433 */ 7434 __evt__pointerclick: function (e) { }, 7435 7436 /** 7437 * @event 7438 * @description Whenever the user double clicks on the board with a pointer device. 7439 * This event works on desktop browser, but is undefined 7440 * on mobile browsers. 7441 * @name JXG.Board#pointerdblclick 7442 * @see JXG.Board#clickDelay 7443 * @param {Event} e The browser's event object. 7444 */ 7445 __evt__pointerdblclick: function (e) { }, 7446 7447 /** 7448 * @event 7449 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 7450 * @name JXG.Board#move 7451 * @param {Event} e The browser's event object. 7452 * @param {Number} mode The mode the board currently is in 7453 * @see JXG.Board#mode 7454 */ 7455 __evt__move: function (e, mode) { }, 7456 7457 /** 7458 * @event 7459 * @description This event is fired whenever the user is moving the mouse over the board. 7460 * @name JXG.Board#mousemove 7461 * @param {Event} e The browser's event object. 7462 * @param {Number} mode The mode the board currently is in 7463 * @see JXG.Board#mode 7464 */ 7465 __evt__mousemove: function (e, mode) { }, 7466 7467 /** 7468 * @event 7469 * @description This event is fired whenever the user is moving the pen over the board. 7470 * @name JXG.Board#penmove 7471 * @param {Event} e The browser's event object. 7472 * @param {Number} mode The mode the board currently is in 7473 * @see JXG.Board#mode 7474 */ 7475 __evt__penmove: function (e, mode) { }, 7476 7477 /** 7478 * @event 7479 * @description This event is fired whenever the user is moving the mouse over the board with a 7480 * device sending pointer events. 7481 * @name JXG.Board#pointermove 7482 * @param {Event} e The browser's event object. 7483 * @param {Number} mode The mode the board currently is in 7484 * @see JXG.Board#mode 7485 */ 7486 __evt__pointermove: function (e, mode) { }, 7487 7488 /** 7489 * @event 7490 * @description This event is fired whenever the user is moving the finger over the board. 7491 * @name JXG.Board#touchmove 7492 * @param {Event} e The browser's event object. 7493 * @param {Number} mode The mode the board currently is in 7494 * @see JXG.Board#mode 7495 */ 7496 __evt__touchmove: function (e, mode) { }, 7497 7498 /** 7499 * @event 7500 * @description This event is fired whenever the user is moving an element over the board by 7501 * pressing arrow keys on a keyboard. 7502 * @name JXG.Board#keymove 7503 * @param {Event} e The browser's event object. 7504 * @param {Number} mode The mode the board currently is in 7505 * @see JXG.Board#mode 7506 */ 7507 __evt__keymove: function (e, mode) { }, 7508 7509 /** 7510 * @event 7511 * @description Whenever an element is highlighted this event is fired. 7512 * @name JXG.Board#hit 7513 * @param {Event} e The browser's event object. 7514 * @param {JXG.GeometryElement} el The hit element. 7515 * @param target 7516 * 7517 * @example 7518 * var c = board.create('circle', [[1, 1], 2]); 7519 * board.on('hit', function(evt, el) { 7520 * console.log('Hit element', el); 7521 * }); 7522 * 7523 * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7524 * <script type='text/javascript'> 7525 * (function() { 7526 * var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723', 7527 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 7528 * var c = board.create('circle', [[1, 1], 2]); 7529 * board.on('hit', function(evt, el) { 7530 * console.log('Hit element', el); 7531 * }); 7532 * 7533 * })(); 7534 * 7535 * </script><pre> 7536 */ 7537 __evt__hit: function (e, el, target) { }, 7538 7539 /** 7540 * @event 7541 * @description Whenever an element is highlighted this event is fired. 7542 * @name JXG.Board#mousehit 7543 * @see JXG.Board#hit 7544 * @param {Event} e The browser's event object. 7545 * @param {JXG.GeometryElement} el The hit element. 7546 * @param target 7547 */ 7548 __evt__mousehit: function (e, el, target) { }, 7549 7550 /** 7551 * @event 7552 * @description This board is updated. 7553 * @name JXG.Board#update 7554 */ 7555 __evt__update: function () { }, 7556 7557 /** 7558 * @event 7559 * @description The bounding box of the board has changed. 7560 * @name JXG.Board#boundingbox 7561 */ 7562 __evt__boundingbox: function () { }, 7563 7564 /** 7565 * @event 7566 * @description Select a region is started during a down event or by calling 7567 * {@link JXG.Board.startSelectionMode} 7568 * @name JXG.Board#startselecting 7569 */ 7570 __evt__startselecting: function () { }, 7571 7572 /** 7573 * @event 7574 * @description Select a region is started during a down event 7575 * from a device sending mouse events or by calling 7576 * {@link JXG.Board.startSelectionMode}. 7577 * @name JXG.Board#mousestartselecting 7578 */ 7579 __evt__mousestartselecting: function () { }, 7580 7581 /** 7582 * @event 7583 * @description Select a region is started during a down event 7584 * from a device sending pointer events or by calling 7585 * {@link JXG.Board.startSelectionMode}. 7586 * @name JXG.Board#pointerstartselecting 7587 */ 7588 __evt__pointerstartselecting: function () { }, 7589 7590 /** 7591 * @event 7592 * @description Select a region is started during a down event 7593 * from a device sending touch events or by calling 7594 * {@link JXG.Board.startSelectionMode}. 7595 * @name JXG.Board#touchstartselecting 7596 */ 7597 __evt__touchstartselecting: function () { }, 7598 7599 /** 7600 * @event 7601 * @description Selection of a region is stopped during an up event. 7602 * @name JXG.Board#stopselecting 7603 */ 7604 __evt__stopselecting: function () { }, 7605 7606 /** 7607 * @event 7608 * @description Selection of a region is stopped during an up event 7609 * from a device sending mouse events. 7610 * @name JXG.Board#mousestopselecting 7611 */ 7612 __evt__mousestopselecting: function () { }, 7613 7614 /** 7615 * @event 7616 * @description Selection of a region is stopped during an up event 7617 * from a device sending pointer events. 7618 * @name JXG.Board#pointerstopselecting 7619 */ 7620 __evt__pointerstopselecting: function () { }, 7621 7622 /** 7623 * @event 7624 * @description Selection of a region is stopped during an up event 7625 * from a device sending touch events. 7626 * @name JXG.Board#touchstopselecting 7627 */ 7628 __evt__touchstopselecting: function () { }, 7629 7630 /** 7631 * @event 7632 * @description A move event while selecting of a region is active. 7633 * @name JXG.Board#moveselecting 7634 */ 7635 __evt__moveselecting: function () { }, 7636 7637 /** 7638 * @event 7639 * @description A move event while selecting of a region is active 7640 * from a device sending mouse events. 7641 * @name JXG.Board#mousemoveselecting 7642 */ 7643 __evt__mousemoveselecting: function () { }, 7644 7645 /** 7646 * @event 7647 * @description Select a region is started during a down event 7648 * from a device sending mouse events. 7649 * @name JXG.Board#pointermoveselecting 7650 */ 7651 __evt__pointermoveselecting: function () { }, 7652 7653 /** 7654 * @event 7655 * @description Select a region is started during a down event 7656 * from a device sending touch events. 7657 * @name JXG.Board#touchmoveselecting 7658 */ 7659 __evt__touchmoveselecting: function () { }, 7660 7661 /** 7662 * @ignore 7663 */ 7664 __evt: function () { }, 7665 7666 //endregion 7667 7668 /** 7669 * Expand the JSXGraph construction to fullscreen. 7670 * In order to preserve the proportions of the JSXGraph element, 7671 * a wrapper div is created which is set to fullscreen. 7672 * This function is called when fullscreen mode is triggered 7673 * <b>and</b> when it is closed. 7674 * <p> 7675 * The wrapping div has the CSS class 'jxgbox_wrap_private' which is 7676 * defined in the file 'jsxgraph.css' 7677 * <p> 7678 * This feature is not available on iPhones (as of December 2021). 7679 * 7680 * @param {String} id (Optional) id of the div element which is brought to fullscreen. 7681 * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick 7682 * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied. 7683 * 7684 * @return {JXG.Board} Reference to the board 7685 * 7686 * @example 7687 * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div> 7688 * <button onClick='board.toFullscreen()'>Fullscreen</button> 7689 * 7690 * <script language='Javascript' type='text/javascript'> 7691 * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]}); 7692 * var p = board.create('point', [0, 1]); 7693 * </script> 7694 * 7695 * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7696 * <script type='text/javascript'> 7697 * var board_d5bab8b6; 7698 * (function() { 7699 * var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723', 7700 * {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false}); 7701 * var p = board.create('point', [0, 1]); 7702 * board_d5bab8b6 = board; 7703 * })(); 7704 * </script> 7705 * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button> 7706 * <pre> 7707 * 7708 * @example 7709 * <div id='outer' style='max-width: 500px; margin: 0 auto;'> 7710 * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div> 7711 * </div> 7712 * <button onClick='board.toFullscreen('outer')'>Fullscreen</button> 7713 * 7714 * <script language='Javascript' type='text/javascript'> 7715 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7716 * axis:true, 7717 * boundingbox:[-5,5,5,-5], 7718 * fullscreen: { id: 'outer' }, 7719 * showFullscreen: true 7720 * }); 7721 * var p = board.create('point', [-2, 3], {}); 7722 * </script> 7723 * 7724 * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'> 7725 * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div> 7726 * </div> 7727 * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button> 7728 * <script type='text/javascript'> 7729 * var board_JXG7103f6be; 7730 * (function() { 7731 * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac', 7732 * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true, 7733 * showcopyright: false, shownavigation: false}); 7734 * var p = board.create('point', [-2, 3], {}); 7735 * board_JXG7103f6be = board; 7736 * })(); 7737 * 7738 * </script><pre> 7739 * 7740 * 7741 */ 7742 toFullscreen: function (id) { 7743 var wrap_id, 7744 wrap_node, 7745 inner_node, 7746 dim, 7747 doc = this.document, 7748 fullscreenElement; 7749 7750 id = id || this.container; 7751 this._fullscreen_inner_id = id; 7752 inner_node = doc.getElementById(id); 7753 wrap_id = 'fullscreenwrap_' + id; 7754 7755 if (!Type.exists(inner_node._cssFullscreenStore)) { 7756 // Store the actual, absolute size of the div 7757 // This is used in scaleJSXGraphDiv 7758 dim = this.containerObj.getBoundingClientRect(); 7759 inner_node._cssFullscreenStore = { 7760 w: dim.width, 7761 h: dim.height 7762 }; 7763 } 7764 7765 // Wrap a div around the JSXGraph div. 7766 // It is removed when fullscreen mode is closed. 7767 if (doc.getElementById(wrap_id)) { 7768 wrap_node = doc.getElementById(wrap_id); 7769 } else { 7770 wrap_node = document.createElement('div'); 7771 wrap_node.classList.add('JXG_wrap_private'); 7772 wrap_node.setAttribute('id', wrap_id); 7773 inner_node.parentNode.insertBefore(wrap_node, inner_node); 7774 wrap_node.appendChild(inner_node); 7775 } 7776 7777 // Trigger fullscreen mode 7778 wrap_node.requestFullscreen = 7779 wrap_node.requestFullscreen || 7780 wrap_node.webkitRequestFullscreen || 7781 wrap_node.mozRequestFullScreen || 7782 wrap_node.msRequestFullscreen; 7783 7784 if (doc.fullscreenElement !== undefined) { 7785 fullscreenElement = doc.fullscreenElement; 7786 } else if (doc.webkitFullscreenElement !== undefined) { 7787 fullscreenElement = doc.webkitFullscreenElement; 7788 } else { 7789 fullscreenElement = doc.msFullscreenElement; 7790 } 7791 7792 if (fullscreenElement === null) { 7793 // Start fullscreen mode 7794 if (wrap_node.requestFullscreen) { 7795 wrap_node.requestFullscreen(); 7796 this.startFullscreenResizeObserver(wrap_node); 7797 } 7798 } else { 7799 this.stopFullscreenResizeObserver(wrap_node); 7800 if (Type.exists(document.exitFullscreen)) { 7801 document.exitFullscreen(); 7802 } else if (Type.exists(document.webkitExitFullscreen)) { 7803 document.webkitExitFullscreen(); 7804 } 7805 } 7806 7807 return this; 7808 }, 7809 7810 /** 7811 * If fullscreen mode is toggled, the possible CSS transformations 7812 * which are applied to the JSXGraph canvas have to be reread. 7813 * Otherwise the position of upper left corner is wrongly interpreted. 7814 * 7815 * @param {Object} evt fullscreen event object (unused) 7816 */ 7817 fullscreenListener: function (evt) { 7818 var inner_id, 7819 inner_node, 7820 fullscreenElement, 7821 doc = this.document; 7822 7823 inner_id = this._fullscreen_inner_id; 7824 if (!Type.exists(inner_id)) { 7825 return; 7826 } 7827 7828 if (doc.fullscreenElement !== undefined) { 7829 fullscreenElement = doc.fullscreenElement; 7830 } else if (doc.webkitFullscreenElement !== undefined) { 7831 fullscreenElement = doc.webkitFullscreenElement; 7832 } else { 7833 fullscreenElement = doc.msFullscreenElement; 7834 } 7835 7836 inner_node = doc.getElementById(inner_id); 7837 // If full screen mode is started we have to remove CSS margin around the JSXGraph div. 7838 // Otherwise, the positioning of the fullscreen div will be false. 7839 // When leaving the fullscreen mode, the margin is put back in. 7840 if (fullscreenElement) { 7841 // Just entered fullscreen mode 7842 7843 // Store the original data. 7844 // Further, the CSS margin has to be removed when in fullscreen mode, 7845 // and must be restored later. 7846 // 7847 // Obsolete: 7848 // It is used in AbstractRenderer.updateText to restore the scaling matrix 7849 // which is removed by MathJax. 7850 inner_node._cssFullscreenStore.id = fullscreenElement.id; 7851 inner_node._cssFullscreenStore.isFullscreen = true; 7852 inner_node._cssFullscreenStore.margin = inner_node.style.margin; 7853 inner_node._cssFullscreenStore.width = inner_node.style.width; 7854 inner_node._cssFullscreenStore.height = inner_node.style.height; 7855 inner_node._cssFullscreenStore.transform = inner_node.style.transform; 7856 // Be sure to replace relative width / height units by absolute units 7857 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px'; 7858 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px'; 7859 inner_node.style.margin = ''; 7860 7861 // Do the shifting and scaling via CSS properties 7862 // We do this after fullscreen mode has been established to get the correct size 7863 // of the JSXGraph div. 7864 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 7865 Type.evaluate(this.attr.fullscreen.scale)); 7866 7867 // Clear this.doc.fullscreenElement, because Safari doesn't to it and 7868 // when leaving full screen mode it is still set. 7869 fullscreenElement = null; 7870 } else if (Type.exists(inner_node._cssFullscreenStore)) { 7871 // Just left the fullscreen mode 7872 7873 inner_node._cssFullscreenStore.isFullscreen = false; 7874 inner_node.style.margin = inner_node._cssFullscreenStore.margin; 7875 inner_node.style.width = inner_node._cssFullscreenStore.width; 7876 inner_node.style.height = inner_node._cssFullscreenStore.height; 7877 inner_node.style.transform = inner_node._cssFullscreenStore.transform; 7878 inner_node._cssFullscreenStore = null; 7879 7880 // Remove the wrapper div 7881 inner_node.parentElement.replaceWith(inner_node); 7882 } 7883 7884 this.updateCSSTransforms(); 7885 }, 7886 7887 /** 7888 * Start resize observer to handle 7889 * orientation changes in fullscreen mode. 7890 * 7891 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 7892 * around the JSXGraph div. 7893 * @returns {JXG.Board} Reference to the board 7894 * @private 7895 * @see JXG.Board#toFullscreen 7896 * 7897 */ 7898 startFullscreenResizeObserver: function(node) { 7899 var that = this; 7900 7901 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 7902 return this; 7903 } 7904 7905 this.resizeObserver = new ResizeObserver(function (entries) { 7906 var inner_id, 7907 fullscreenElement, 7908 doc = that.document; 7909 7910 if (!that._isResizing) { 7911 that._isResizing = true; 7912 window.setTimeout(function () { 7913 try { 7914 inner_id = that._fullscreen_inner_id; 7915 if (doc.fullscreenElement !== undefined) { 7916 fullscreenElement = doc.fullscreenElement; 7917 } else if (doc.webkitFullscreenElement !== undefined) { 7918 fullscreenElement = doc.webkitFullscreenElement; 7919 } else { 7920 fullscreenElement = doc.msFullscreenElement; 7921 } 7922 if (fullscreenElement !== null) { 7923 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 7924 Type.evaluate(that.attr.fullscreen.scale)); 7925 } 7926 } catch (err) { 7927 that.stopFullscreenResizeObserver(node); 7928 } finally { 7929 that._isResizing = false; 7930 } 7931 }, that.attr.resize.throttle); 7932 } 7933 }); 7934 this.resizeObserver.observe(node); 7935 return this; 7936 }, 7937 7938 /** 7939 * Remove resize observer to handle orientation changes in fullscreen mode. 7940 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 7941 * around the JSXGraph div. 7942 * @returns {JXG.Board} Reference to the board 7943 * @private 7944 * @see JXG.Board#toFullscreen 7945 */ 7946 stopFullscreenResizeObserver: function(node) { 7947 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 7948 return this; 7949 } 7950 7951 if (Type.exists(this.resizeObserver)) { 7952 this.resizeObserver.unobserve(node); 7953 } 7954 return this; 7955 }, 7956 7957 /** 7958 * Add user activity to the array 'board.userLog'. 7959 * 7960 * @param {String} type Event type, e.g. 'drag' 7961 * @param {Object} obj JSXGraph element object 7962 * 7963 * @see JXG.Board#userLog 7964 * @return {JXG.Board} Reference to the board 7965 */ 7966 addLogEntry: function (type, obj, pos) { 7967 var t, id, 7968 last = this.userLog.length - 1; 7969 7970 if (Type.exists(obj.elementClass)) { 7971 id = obj.id; 7972 } 7973 if (Type.evaluate(this.attr.logging.enabled)) { 7974 t = (new Date()).getTime(); 7975 if (last >= 0 && 7976 this.userLog[last].type === type && 7977 this.userLog[last].id === id && 7978 // Distinguish consecutive drag events of 7979 // the same element 7980 t - this.userLog[last].end < 500) { 7981 7982 this.userLog[last].end = t; 7983 this.userLog[last].endpos = pos; 7984 } else { 7985 this.userLog.push({ 7986 type: type, 7987 id: id, 7988 start: t, 7989 startpos: pos, 7990 end: t, 7991 endpos: pos, 7992 bbox: this.getBoundingBox(), 7993 canvas: [this.canvasWidth, this.canvasHeight], 7994 zoom: [this.zoomX, this.zoomY] 7995 }); 7996 } 7997 } 7998 return this; 7999 }, 8000 8001 /** 8002 * Function to animate a curve rolling on another curve. 8003 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 8004 * @param {Curve} c2 JSXGraph curve which rolls on c1. 8005 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 8006 * rolling process 8007 * @param {Number} stepsize Increase in t in each step for the curve c1 8008 * @param {Number} direction 8009 * @param {Number} time Delay time for setInterval() 8010 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 8011 * all points which define c2 and gliders on c2. 8012 * 8013 * @example 8014 * 8015 * // Line which will be the floor to roll upon. 8016 * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8017 * // Center of the rolling circle 8018 * var C = board.create('point',[0,2],{name:'C'}); 8019 * // Starting point of the rolling circle 8020 * var P = board.create('point',[0,1],{name:'P', trace:true}); 8021 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8022 * var circle = board.create('curve',[ 8023 * function (t){var d = P.Dist(C), 8024 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8025 * t += beta; 8026 * return C.X()+d*Math.cos(t); 8027 * }, 8028 * function (t){var d = P.Dist(C), 8029 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8030 * t += beta; 8031 * return C.Y()+d*Math.sin(t); 8032 * }, 8033 * 0,2*Math.PI], 8034 * {strokeWidth:6, strokeColor:'green'}); 8035 * 8036 * // Point on circle 8037 * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8038 * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8039 * roll.start() // Start the rolling, to be stopped by roll.stop() 8040 * 8041 * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div> 8042 * <script type='text/javascript'> 8043 * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 8044 * // Line which will be the floor to roll upon. 8045 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8046 * // Center of the rolling circle 8047 * var C = brd.create('point',[0,2],{name:'C'}); 8048 * // Starting point of the rolling circle 8049 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 8050 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8051 * var circle = brd.create('curve',[ 8052 * function (t){var d = P.Dist(C), 8053 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8054 * t += beta; 8055 * return C.X()+d*Math.cos(t); 8056 * }, 8057 * function (t){var d = P.Dist(C), 8058 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8059 * t += beta; 8060 * return C.Y()+d*Math.sin(t); 8061 * }, 8062 * 0,2*Math.PI], 8063 * {strokeWidth:6, strokeColor:'green'}); 8064 * 8065 * // Point on circle 8066 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8067 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8068 * roll.start() // Start the rolling, to be stopped by roll.stop() 8069 * </script><pre> 8070 */ 8071 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 8072 var brd = this, 8073 Roulette = function () { 8074 var alpha = 0, 8075 Tx = 0, 8076 Ty = 0, 8077 t1 = start_c1, 8078 t2 = Numerics.root( 8079 function (t) { 8080 var c1x = c1.X(t1), 8081 c1y = c1.Y(t1), 8082 c2x = c2.X(t), 8083 c2y = c2.Y(t); 8084 8085 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 8086 }, 8087 [0, Math.PI * 2] 8088 ), 8089 t1_new = 0.0, 8090 t2_new = 0.0, 8091 c1dist, 8092 rotation = brd.create( 8093 'transform', 8094 [ 8095 function () { 8096 return alpha; 8097 } 8098 ], 8099 { type: 'rotate' } 8100 ), 8101 rotationLocal = brd.create( 8102 'transform', 8103 [ 8104 function () { 8105 return alpha; 8106 }, 8107 function () { 8108 return c1.X(t1); 8109 }, 8110 function () { 8111 return c1.Y(t1); 8112 } 8113 ], 8114 { type: 'rotate' } 8115 ), 8116 translate = brd.create( 8117 'transform', 8118 [ 8119 function () { 8120 return Tx; 8121 }, 8122 function () { 8123 return Ty; 8124 } 8125 ], 8126 { type: 'translate' } 8127 ), 8128 // arc length via Simpson's rule. 8129 arclen = function (c, a, b) { 8130 var cpxa = Numerics.D(c.X)(a), 8131 cpya = Numerics.D(c.Y)(a), 8132 cpxb = Numerics.D(c.X)(b), 8133 cpyb = Numerics.D(c.Y)(b), 8134 cpxab = Numerics.D(c.X)((a + b) * 0.5), 8135 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 8136 fa = Mat.hypot(cpxa, cpya), 8137 fb = Mat.hypot(cpxb, cpyb), 8138 fab = Mat.hypot(cpxab, cpyab); 8139 8140 return ((fa + 4 * fab + fb) * (b - a)) / 6; 8141 }, 8142 exactDist = function (t) { 8143 return c1dist - arclen(c2, t2, t); 8144 }, 8145 beta = Math.PI / 18, 8146 beta9 = beta * 9, 8147 interval = null; 8148 8149 this.rolling = function () { 8150 var h, g, hp, gp, z; 8151 8152 t1_new = t1 + direction * stepsize; 8153 8154 // arc length between c1(t1) and c1(t1_new) 8155 c1dist = arclen(c1, t1, t1_new); 8156 8157 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 8158 t2_new = Numerics.root(exactDist, t2); 8159 8160 // c1(t) as complex number 8161 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 8162 8163 // c2(t) as complex number 8164 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 8165 8166 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 8167 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 8168 8169 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 8170 z = Complex.C.div(hp, gp); 8171 8172 alpha = Math.atan2(z.imaginary, z.real); 8173 // Normalizing the quotient 8174 z.div(Complex.C.abs(z)); 8175 z.mult(g); 8176 Tx = h.real - z.real; 8177 8178 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 8179 Ty = h.imaginary - z.imaginary; 8180 8181 // -(10-90) degrees: make corners roll smoothly 8182 if (alpha < -beta && alpha > -beta9) { 8183 alpha = -beta; 8184 rotationLocal.applyOnce(pointlist); 8185 } else if (alpha > beta && alpha < beta9) { 8186 alpha = beta; 8187 rotationLocal.applyOnce(pointlist); 8188 } else { 8189 rotation.applyOnce(pointlist); 8190 translate.applyOnce(pointlist); 8191 t1 = t1_new; 8192 t2 = t2_new; 8193 } 8194 brd.update(); 8195 }; 8196 8197 this.start = function () { 8198 if (time > 0) { 8199 interval = window.setInterval(this.rolling, time); 8200 } 8201 return this; 8202 }; 8203 8204 this.stop = function () { 8205 window.clearInterval(interval); 8206 return this; 8207 }; 8208 return this; 8209 }; 8210 return new Roulette(); 8211 } 8212 } 8213 ); 8214 8215 export default JXG.Board; 8216