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