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