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