1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Andreas Walter, 8 Alfred Wassermann, 9 Peter Wilfahrt 10 11 This file is part of JSXGraph. 12 13 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 14 15 You can redistribute it and/or modify it under the terms of the 16 17 * GNU Lesser General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version 20 OR 21 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 22 23 JSXGraph is distributed in the hope that it will be useful, 24 but WITHOUT ANY WARRANTY; without even the implied warranty of 25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 GNU Lesser General Public License for more details. 27 28 You should have received a copy of the GNU Lesser General Public License and 29 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 30 and <https://opensource.org/licenses/MIT/>. 31 */ 32 33 /*global JXG: true, define: true, window: true, document: true, navigator: true, module: true, global: true, self: true, require: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish 38 * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device 39 * the browser runs on is a tablet/cell or a desktop computer. 40 */ 41 42 import JXG from "../jxg.js"; 43 import Type from "./type.js"; 44 45 JXG.extendConstants( 46 JXG, 47 /** @lends JXG */ { 48 // /** 49 // * Determines the property that stores the relevant information in the event object. 50 // * @type String 51 // * @default 'touches' 52 // * @private 53 // */ 54 // touchProperty: "touches" 55 } 56 ); 57 58 JXG.extend( 59 JXG, 60 /** @lends JXG */ { 61 62 /** 63 * Upper bound on pixel coordinates. This is used in svg and canvas renderer to avoid limitations on numbers there. 64 * 2026: can be enlarged to 2**24-1, the largest integer valiue that browsers do support. 65 * Browser implementations support 32bit floating point values. 66 * <p> 67 * 68 * @private 69 */ 70 maxScreenCoord: 16777215, // = (2**24 - 1), 71 // Too large: 2147483647 = Math.pow(2, 31) - 1, 72 // 5000, 73 74 /** 75 * Determines whether evt is a touch event. 76 * @param evt {Event} 77 * @returns {Boolean} 78 */ 79 isTouchEvent: function (evt) { 80 return JXG.exists(evt['touches']); // Old iOS touch events 81 }, 82 83 /** 84 * Determines whether evt is a pointer event. 85 * @param evt {Event} 86 * @returns {Boolean} 87 */ 88 isPointerEvent: function (evt) { 89 return JXG.exists(evt.pointerId); 90 }, 91 92 /** 93 * Determines whether evt is neither a touch event nor a pointer event. 94 * @param evt {Event} 95 * @returns {Boolean} 96 */ 97 isMouseEvent: function (evt) { 98 return !JXG.isTouchEvent(evt) && !JXG.isPointerEvent(evt); 99 }, 100 101 /** 102 * Determines the number of touch points in a touch event. 103 * For other events, -1 is returned. 104 * @param evt {Event} 105 * @returns {Number} 106 */ 107 getNumberOfTouchPoints: function (evt) { 108 var n = -1; 109 110 if (JXG.isTouchEvent(evt)) { 111 n = evt['touches'].length; 112 } 113 114 return n; 115 }, 116 117 /** 118 * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event. 119 * Attention: When two or more pointer device types are being used concurrently, 120 * it is only checked whether the passed event is the first one of its type! 121 * @param evt {Event} 122 * @returns {boolean} 123 */ 124 isFirstTouch: function (evt) { 125 var touchPoints = JXG.getNumberOfTouchPoints(evt); 126 127 if (JXG.isPointerEvent(evt)) { 128 return evt.isPrimary; 129 } 130 131 return touchPoints === 1; 132 }, 133 134 /** 135 * A document/window environment is available. 136 * @type Boolean 137 * @default false 138 */ 139 // isBrowser: Type.exists(window) && Type.exists(document) && 140 // typeof window === "object" && typeof document === "object", 141 isBrowser: typeof window !== 'undefined' && typeof document !== 'undefined' && 142 typeof window === "object" && typeof document === "object", 143 144 /** 145 * Features of ECMAScript 6+ are available. 146 * @type Boolean 147 * @default false 148 */ 149 supportsES6: function () { 150 // var testMap; 151 /* jshint ignore:start */ 152 try { 153 // This would kill the old uglifyjs: testMap = (a = 0) => a; 154 new Function("(a = 0) => a"); 155 return true; 156 } catch (err) { 157 return false; 158 } 159 /* jshint ignore:end */ 160 }, 161 162 /** 163 * Detect browser support for VML. 164 * @returns {Boolean} True, if the browser supports VML. 165 */ 166 supportsVML: function () { 167 // From stackoverflow.com 168 return this.isBrowser && !!document.namespaces; 169 }, 170 171 /** 172 * Detect browser support for SVG. 173 * @returns {Boolean} True, if the browser supports SVG. 174 */ 175 supportsSVG: function () { 176 var svgSupport; 177 if (!this.isBrowser) { 178 return false; 179 } 180 svgSupport = !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect; 181 return svgSupport; 182 }, 183 184 /** 185 * Detect browser support for Canvas. 186 * @returns {Boolean} True, if the browser supports HTML canvas. 187 */ 188 supportsCanvas: function () { 189 var hasCanvas = false; 190 191 // if (this.isNode()) { 192 // try { 193 // // c = typeof module === "object" ? module.require('canvas') : $__canvas; 194 // c = typeof module === "object" ? module.require('canvas') : import('canvas'); 195 // hasCanvas = !!c; 196 // } catch (err) {} 197 // } 198 199 if (this.isNode()) { 200 //try { 201 // JXG.createCanvas(500, 500); 202 hasCanvas = true; 203 // } catch (err) { 204 // throw new Error('JXG.createCanvas not available.\n' + 205 // 'Install the npm package `canvas`\n' + 206 // 'and call:\n' + 207 // ' import { createCanvas } from 'canvas.js'\n' + 208 // ' JXG.createCanvas = createCanvas;\n'); 209 // } 210 } 211 212 return ( 213 hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext) 214 ); 215 }, 216 217 /** 218 * True, if run inside a node.js environment. 219 * @returns {Boolean} 220 */ 221 isNode: function () { 222 // This is not a 100% sure but should be valid in most cases 223 // We are not inside a browser 224 /* eslint-disable no-undef */ 225 return ( 226 !this.isBrowser && 227 (typeof process !== 'undefined') && 228 (process.release.name.search(/node|io.js/) !== -1) 229 /* eslint-enable no-undef */ 230 231 // there is a module object (plain node, no requirejs) 232 // ((typeof module === "object" && !!module.exports) || 233 // // there is a global object and requirejs is loaded 234 // (typeof global === "object" && 235 // global.requirejsVars && 236 // !global.requirejsVars.isBrowser) 237 // ) 238 ); 239 }, 240 241 /** 242 * True if run inside a webworker environment. 243 * @returns {Boolean} 244 */ 245 isWebWorker: function () { 246 return ( 247 !this.isBrowser && 248 typeof self === "object" && 249 typeof self.postMessage === "function" 250 ); 251 }, 252 253 /** 254 * Checks if the environments supports the W3C Pointer Events API {@link https://www.w3.org/TR/pointerevents/} 255 * @returns {Boolean} 256 */ 257 supportsPointerEvents: function () { 258 return !!( 259 ( 260 this.isBrowser && 261 window.navigator && 262 (window.PointerEvent || // Chrome/Edge/IE11+ 263 window.navigator.pointerEnabled || // IE11+ 264 window.navigator.msPointerEnabled) 265 ) // IE10- 266 ); 267 }, 268 269 /** 270 * Determine if the current browser supports touch events 271 * @returns {Boolean} True, if the browser supports touch events. 272 */ 273 isTouchDevice: function () { 274 return this.isBrowser && window.ontouchstart !== undefined; 275 }, 276 277 /** 278 * Detects if the user is using an Android powered device. 279 * @returns {Boolean} 280 * @deprecated 281 */ 282 isAndroid: function () { 283 return ( 284 Type.exists(navigator) && 285 navigator.userAgent.toLowerCase().indexOf('android') > -1 286 ); 287 }, 288 289 /** 290 * Detects if the user is using the default Webkit browser on an Android powered device. 291 * @returns {Boolean} 292 * @deprecated 293 */ 294 isWebkitAndroid: function () { 295 return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1; 296 }, 297 298 /** 299 * Detects if the user is using a Apple iPad / iPhone. 300 * @returns {Boolean} 301 * @deprecated 302 */ 303 isApple: function () { 304 return ( 305 Type.exists(navigator) && 306 (navigator.userAgent.indexOf('iPad') > -1 || 307 navigator.userAgent.indexOf('iPhone') > -1) 308 ); 309 }, 310 311 /** 312 * Detects if the user is using Safari on an Apple device. 313 * See https://evilmartians.com/chronicles/how-to-detect-safari-and-ios-versions-with-ease (2025) 314 * @returns {Boolean} 315 * @deprecated 316 */ 317 isWebkitApple: function () { 318 var is = ('GestureEvent' in window) && // Desktop and mobile 319 ( 320 ('ongesturechange' in window) || // mobile webkit browsers and webview iOS 321 (window !== undefined && // Desktop Safari 322 'safari' in window && 323 'pushNotification' in window.safari) 324 ); 325 return is; 326 327 // return ( 328 // this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1 329 // ); 330 }, 331 332 /** 333 * Returns true if the run inside a Windows 8 "Metro" App. 334 * @returns {Boolean} 335 * @deprecated 336 */ 337 isMetroApp: function () { 338 return ( 339 typeof window === "object" && 340 window.clientInformation && 341 window.clientInformation.appVersion && 342 window.clientInformation.appVersion.indexOf('MSAppHost') > -1 343 ); 344 }, 345 346 /** 347 * Detects if the user is using a Mozilla browser 348 * @returns {Boolean} 349 * @deprecated 350 */ 351 isMozilla: function () { 352 return ( 353 Type.exists(navigator) && 354 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 && 355 navigator.userAgent.toLowerCase().indexOf('apple') === -1 356 ); 357 }, 358 359 /** 360 * Detects if the user is using a firefoxOS powered device. 361 * @returns {Boolean} 362 * @deprecated 363 */ 364 isFirefoxOS: function () { 365 return ( 366 Type.exists(navigator) && 367 navigator.userAgent.toLowerCase().indexOf('android') === -1 && 368 navigator.userAgent.toLowerCase().indexOf('apple') === -1 && 369 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 && 370 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 371 ); 372 }, 373 374 /** 375 * Detects if the user is using a desktop device, see <a href="https://stackoverflow.com/a/61073480">https://stackoverflow.com/a/61073480</a>. 376 * @returns {boolean} 377 * 378 * @deprecated 379 */ 380 isDesktop: function () { 381 return true; 382 // console.log("isDesktop", screen.orientation); 383 // const navigatorAgent = 384 // navigator.userAgent || navigator.vendor || window.opera; 385 // return !( 386 // /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( 387 // navigatorAgent 388 // ) || 389 // /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( 390 // navigatorAgent.substr(0, 4) 391 // ) 392 // ); 393 }, 394 395 /** 396 * Detects if the user is using a mobile device, see <a href="https://stackoverflow.com/questions/25542814/html5-detecting-if-youre-on-mobile-or-pc-with-javascript">https://stackoverflow.com/questions/25542814/html5-detecting-if-youre-on-mobile-or-pc-with-javascript</a>. 397 * @returns {boolean} 398 * 399 * @deprecated 400 * 401 */ 402 isMobile: function () { 403 return true; 404 // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 405 }, 406 407 /** 408 * Internet Explorer version. Works only for IE > 4. 409 * @type Number 410 * @deprecated 411 */ 412 ieVersion: (function () { 413 var div, 414 all, 415 v = 3; 416 417 if (typeof document === 'undefined' || document === null || typeof document !== 'object') { 418 return 0; 419 } 420 421 div = document.createElement('div'); 422 all = div.getElementsByTagName('i'); 423 424 do { 425 div.innerHTML = "<!--[if gt IE " + (++v) + "]><" + "i><" + "/i><![endif]-->"; 426 } while (all[0]); 427 428 return v > 4 ? v : undefined; 429 })(), 430 431 /** 432 * Reads the width and height of an HTML element. 433 * @param {String|Object} elementId id of or reference to an HTML DOM node. 434 * @returns {Object} An object with the two properties width and height. 435 */ 436 getDimensions: function (elementId, doc) { 437 var element, 438 display, 439 els, 440 originalVisibility, 441 originalPosition, 442 originalDisplay, 443 originalWidth, 444 originalHeight, 445 style, 446 pixelDimRegExp = /\d+(\.\d*)?px/; 447 448 if (!this.isBrowser || elementId === null) { 449 return { 450 width: 500, 451 height: 500 452 }; 453 } 454 455 doc = doc || document; 456 // Borrowed from prototype.js 457 element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId; 458 if (!Type.exists(element)) { 459 throw new Error( 460 "\nJSXGraph: HTML container element '" + elementId + "' not found." 461 ); 462 } 463 464 display = element.style.display; 465 466 // Work around a bug in Safari 467 if (display !== "none" && display !== null) { 468 if (element.clientWidth > 0 && element.clientHeight > 0) { 469 return { width: element.clientWidth, height: element.clientHeight }; 470 } 471 472 // A parent might be set to display:none; try reading them from styles 473 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style; 474 return { 475 width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0, 476 height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0 477 }; 478 } 479 480 // All *Width and *Height properties give 0 on elements with display set to none, 481 // hence we show the element temporarily 482 els = element.style; 483 484 // store style 485 originalVisibility = els.visibility; 486 originalPosition = els.position; 487 originalDisplay = els.display; 488 489 // show element 490 els.visibility = 'hidden'; 491 els.position = 'absolute'; 492 els.display = 'block'; 493 494 // read the dimension 495 originalWidth = element.clientWidth; 496 originalHeight = element.clientHeight; 497 498 // restore original css values 499 els.display = originalDisplay; 500 els.position = originalPosition; 501 els.visibility = originalVisibility; 502 503 return { 504 width: originalWidth, 505 height: originalHeight 506 }; 507 }, 508 509 /** 510 * Adds an event listener to a DOM element. 511 * @param {Object} obj Reference to a DOM node. 512 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 513 * @param {Function} fn The function to call when the event is triggered. 514 * @param {Object} owner The scope in which the event trigger is called. 515 * @param {Object|Boolean} [options=false] This parameter is passed as the third parameter to the method addEventListener. Depending on the data type it is either 516 * an options object or the useCapture Boolean. 517 * 518 */ 519 addEvent: function (obj, type, fn, owner, options) { 520 var el = function () { 521 return fn.apply(owner, arguments); 522 }; 523 524 el.origin = fn; 525 // Check if owner is a board 526 if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) { 527 owner['x_internal' + type] = owner['x_internal' + type] || []; 528 owner['x_internal' + type].push(el); 529 } 530 531 // Non-IE browser 532 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 533 options = options || false; // options or useCapture 534 obj.addEventListener(type, el, options); 535 } 536 537 // IE 538 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 539 obj.attachEvent("on" + type, el); 540 } 541 }, 542 543 /** 544 * Removes an event listener from a DOM element. 545 * @param {Object} obj Reference to a DOM node. 546 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 547 * @param {Function} fn The function to call when the event is triggered. 548 * @param {Object} owner The scope in which the event trigger is called. 549 */ 550 removeEvent: function (obj, type, fn, owner) { 551 var i; 552 553 if (!Type.exists(owner)) { 554 JXG.debug("no such owner"); 555 return; 556 } 557 558 if (!Type.exists(owner["x_internal" + type])) { 559 JXG.debug("removeEvent: no such type: " + type); 560 return; 561 } 562 563 if (!Type.isArray(owner["x_internal" + type])) { 564 JXG.debug("owner[x_internal + " + type + "] is not an array"); 565 return; 566 } 567 568 i = Type.indexOf(owner["x_internal" + type], fn, 'origin'); 569 570 if (i === -1) { 571 JXG.debug("removeEvent: no such event function in internal list: " + fn); 572 return; 573 } 574 575 try { 576 // Non-IE browser 577 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 578 obj.removeEventListener(type, owner["x_internal" + type][i], false); 579 } 580 581 // IE 582 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 583 obj.detachEvent("on" + type, owner["x_internal" + type][i]); 584 } 585 } catch (e) { 586 JXG.debug("removeEvent: event not registered in browser: (" + type + " -- " + fn + ")"); 587 } 588 589 owner["x_internal" + type].splice(i, 1); 590 }, 591 592 /** 593 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 594 * of a {@link JXG.Board} because this might corrupt the event handling system. 595 * @param {Object} obj Reference to a DOM node. 596 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 597 * @param {Object} owner The scope in which the event trigger is called. 598 */ 599 removeAllEvents: function (obj, type, owner) { 600 var i, len; 601 if (owner["x_internal" + type]) { 602 len = owner["x_internal" + type].length; 603 604 for (i = len - 1; i >= 0; i--) { 605 JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner); 606 } 607 608 if (owner["x_internal" + type].length > 0) { 609 JXG.debug("removeAllEvents: Not all events could be removed."); 610 } 611 } 612 }, 613 614 /** 615 * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner. 616 * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent. 617 * 618 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 619 * @param {Number} [index] If <tt>e</tt> is a touch event, this provides the index of the touch coordinates, i.e. it determines which finger. 620 * @param {Object} [doc] The document object. 621 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 622 */ 623 getPosition: function (e, index, doc) { 624 var i, 625 len, 626 evtTouches, 627 posx = 0, 628 posy = 0; 629 630 if (!e) { 631 e = window.event; 632 } 633 634 doc = doc || document; 635 evtTouches = e['touches']; // iOS touch events 636 637 // touchend events have their position in "changedTouches" 638 if (Type.exists(evtTouches) && evtTouches.length === 0) { 639 evtTouches = e.changedTouches; 640 } 641 642 if (Type.exists(index) && Type.exists(evtTouches)) { 643 if (index === -1) { 644 len = evtTouches.length; 645 646 for (i = 0; i < len; i++) { 647 if (evtTouches[i]) { 648 e = evtTouches[i]; 649 break; 650 } 651 } 652 } else { 653 e = evtTouches[index]; 654 } 655 } 656 657 // Scrolling is ignored. 658 // e.clientX is supported since IE6 659 if (e.clientX) { 660 posx = e.clientX; 661 posy = e.clientY; 662 } 663 664 return [posx, posy]; 665 }, 666 667 /** 668 * Calculates recursively the offset of the DOM element in which the board is stored. 669 * @param {Object} obj A DOM element 670 * @returns {Array} An array with the elements left and top offset. 671 */ 672 getOffset: function (obj) { 673 var cPos, 674 o = obj, 675 o2 = obj, 676 l = o.offsetLeft - o.scrollLeft, 677 t = o.offsetTop - o.scrollTop; 678 679 cPos = this.getCSSTransform([l, t], o); 680 l = cPos[0]; 681 t = cPos[1]; 682 683 /* 684 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 685 * if not to the body. In IE and if we are in an position:absolute environment 686 * offsetParent walks up the DOM hierarchy. 687 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 688 * we need the parentNode steps. 689 */ 690 o = o.offsetParent; 691 while (o) { 692 l += o.offsetLeft; 693 t += o.offsetTop; 694 695 if (o.offsetParent) { 696 l += o.clientLeft - o.scrollLeft; 697 t += o.clientTop - o.scrollTop; 698 } 699 700 cPos = this.getCSSTransform([l, t], o); 701 l = cPos[0]; 702 t = cPos[1]; 703 704 o2 = o2.parentNode; 705 706 while (o2 !== o) { 707 l += o2.clientLeft - o2.scrollLeft; 708 t += o2.clientTop - o2.scrollTop; 709 710 cPos = this.getCSSTransform([l, t], o2); 711 l = cPos[0]; 712 t = cPos[1]; 713 714 o2 = o2.parentNode; 715 } 716 o = o.offsetParent; 717 } 718 719 return [l, t]; 720 }, 721 722 /** 723 * Access CSS style sheets. 724 * @param {Object} obj A DOM element 725 * @param {String} stylename The CSS property to read. 726 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 727 */ 728 getStyle: function (obj, stylename) { 729 var r, 730 doc = obj.ownerDocument; 731 732 // Non-IE 733 if (doc.defaultView && doc.defaultView.getComputedStyle) { 734 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 735 // IE 736 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 737 r = obj.currentStyle[stylename]; 738 } else { 739 if (obj.style) { 740 // make stylename lower camelcase 741 stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) { 742 return letter.toUpperCase(); 743 }); 744 r = obj.style[stylename]; 745 } 746 } 747 748 return r; 749 }, 750 751 /** 752 * Reads css style sheets of a given element. This method is a getStyle wrapper and 753 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 754 * @param {DOMElement} el 755 * @param {string} css 756 * @returns {number} 757 */ 758 getProp: function (el, css) { 759 var n = parseInt(this.getStyle(el, css), 10); 760 return isNaN(n) ? 0 : n; 761 }, 762 763 /** 764 * Correct position of upper left corner in case of 765 * a CSS transformation. Here, only translations are 766 * extracted. All scaling transformations are corrected 767 * in {@link JXG.Board#getMousePosition}. 768 * @param {Array} cPos Previously determined position 769 * @param {Object} obj A DOM element 770 * @returns {Array} The corrected position. 771 */ 772 getCSSTransform: function (cPos, obj) { 773 var i, 774 j, 775 str, 776 arrStr, 777 start, 778 len, 779 len2, 780 arr, 781 t = [ 782 "transform", 783 "webkitTransform", 784 "MozTransform", 785 "msTransform", 786 "oTransform" 787 ]; 788 789 // Take the first transformation matrix 790 len = t.length; 791 792 for (i = 0, str = ""; i < len; i++) { 793 if (Type.exists(obj.style[t[i]])) { 794 str = obj.style[t[i]]; 795 break; 796 } 797 } 798 799 /** 800 * Extract the coordinates and apply the transformation 801 * to cPos 802 */ 803 if (str !== "") { 804 start = str.indexOf("("); 805 806 if (start > 0) { 807 len = str.length; 808 arrStr = str.substring(start + 1, len - 1); 809 arr = arrStr.split(","); 810 811 for (j = 0, len2 = arr.length; j < len2; j++) { 812 arr[j] = parseFloat(arr[j]); 813 } 814 815 if (str.indexOf('matrix') === 0) { 816 cPos[0] += arr[4]; 817 cPos[1] += arr[5]; 818 } else if (str.indexOf('translateX') === 0) { 819 cPos[0] += arr[0]; 820 } else if (str.indexOf('translateY') === 0) { 821 cPos[1] += arr[0]; 822 } else if (str.indexOf('translate') === 0) { 823 cPos[0] += arr[0]; 824 cPos[1] += arr[1]; 825 } 826 } 827 } 828 829 // Zoom is used by reveal.js 830 if (Type.exists(obj.style.zoom)) { 831 str = obj.style.zoom; 832 if (str !== "") { 833 cPos[0] *= parseFloat(str); 834 cPos[1] *= parseFloat(str); 835 } 836 } 837 838 return cPos; 839 }, 840 841 /** 842 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 843 * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported. 844 * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}. 845 */ 846 getCSSTransformMatrix: function (obj) { 847 var i, j, str, arrstr, arr, 848 start, len, len2, st, 849 doc = obj.ownerDocument, 850 t = [ 851 "transform", 852 "webkitTransform", 853 "MozTransform", 854 "msTransform", 855 "oTransform" 856 ], 857 mat = [ 858 [1, 0, 0], 859 [0, 1, 0], 860 [0, 0, 1] 861 ]; 862 863 // This should work on all browsers except IE 6-8 864 if (doc.defaultView && doc.defaultView.getComputedStyle) { 865 st = doc.defaultView.getComputedStyle(obj, null); 866 str = 867 st.getPropertyValue("-webkit-transform") || 868 st.getPropertyValue("-moz-transform") || 869 st.getPropertyValue("-ms-transform") || 870 st.getPropertyValue("-o-transform") || 871 st.getPropertyValue('transform'); 872 } else { 873 // Take the first transformation matrix 874 len = t.length; 875 for (i = 0, str = ""; i < len; i++) { 876 if (Type.exists(obj.style[t[i]])) { 877 str = obj.style[t[i]]; 878 break; 879 } 880 } 881 } 882 883 // Convert and reorder the matrix for JSXGraph 884 if (str !== "") { 885 start = str.indexOf("("); 886 887 if (start > 0) { 888 len = str.length; 889 arrstr = str.substring(start + 1, len - 1); 890 arr = arrstr.split(","); 891 892 for (j = 0, len2 = arr.length; j < len2; j++) { 893 arr[j] = parseFloat(arr[j]); 894 } 895 896 if (str.indexOf('matrix') === 0) { 897 mat = [ 898 [1, 0, 0], 899 [0, arr[0], arr[1]], 900 [0, arr[2], arr[3]] 901 ]; 902 } else if (str.indexOf('scaleX') === 0) { 903 mat[1][1] = arr[0]; 904 } else if (str.indexOf('scaleY') === 0) { 905 mat[2][2] = arr[0]; 906 } else if (str.indexOf('scale') === 0) { 907 mat[1][1] = arr[0]; 908 mat[2][2] = arr[1]; 909 } 910 } 911 } 912 913 // CSS style zoom is used by reveal.js 914 // Recursively search for zoom style entries. 915 // This is necessary for reveal.js on webkit. 916 // It fails if the user does zooming 917 if (Type.exists(obj.style.zoom)) { 918 str = obj.style.zoom; 919 if (str !== "") { 920 mat[1][1] *= parseFloat(str); 921 mat[2][2] *= parseFloat(str); 922 } 923 } 924 925 return mat; 926 }, 927 928 /** 929 * Process data in timed chunks. Data which takes long to process, either because it is such 930 * a huge amount of data or the processing takes some time, causes warnings in browsers about 931 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 932 * called chunks which will be processed in serial order. 933 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 934 * @param {Array} items to do 935 * @param {Function} process Function that is applied for every array item 936 * @param {Object} context The scope of function process 937 * @param {Function} callback This function is called after the last array element has been processed. 938 */ 939 timedChunk: function (items, process, context, callback) { 940 //create a clone of the original 941 var todo = items.slice(), 942 timerFun = function () { 943 var start = +new Date(); 944 945 do { 946 process.call(context, todo.shift()); 947 } while (todo.length > 0 && +new Date() - start < 300); 948 949 if (todo.length > 0) { 950 window.setTimeout(timerFun, 1); 951 } else { 952 callback(items); 953 } 954 }; 955 956 window.setTimeout(timerFun, 1); 957 }, 958 959 /** 960 * Scale and vertically shift a DOM element (usually a JSXGraph div) 961 * inside of a parent DOM 962 * element which is set to fullscreen. 963 * This is realized with a CSS transformation. 964 * 965 * @param {String} wrap_id id of the parent DOM element which is in fullscreen mode 966 * @param {String} inner_id id of the DOM element which is scaled and shifted 967 * @param {Object} doc document object or shadow root 968 * @param {Number} scale Relative size of the JSXGraph board in the fullscreen window. 969 * 970 * @private 971 * @see JXG.Board#toFullscreen 972 * @see JXG.Board#fullscreenListener 973 * 974 */ 975 scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) { 976 var w, h, b, 977 wi, hi, 978 wo, ho, inner, 979 scale_l, vshift_l, 980 f = scale, 981 ratio, 982 pseudo_keys = [ 983 ":fullscreen", 984 ":-webkit-full-screen", 985 ":-moz-full-screen", 986 ":-ms-fullscreen" 987 ], 988 len_pseudo = pseudo_keys.length, 989 i; 990 991 b = doc.getElementById(wrap_id).getBoundingClientRect(); 992 h = b.height; 993 w = b.width; 994 995 inner = doc.getElementById(inner_id); 996 wo = inner._cssFullscreenStore.w; 997 ho = inner._cssFullscreenStore.h; 998 ratio = ho / wo; 999 1000 // Scale the div such that fits into the fullscreen. 1001 if (wo > w * f) { 1002 wo = w * f; 1003 ho = wo * ratio; 1004 } 1005 if (ho > h * f) { 1006 ho = h * f; 1007 wo = ho / ratio; 1008 } 1009 1010 wi = wo; 1011 hi = ho; 1012 // Compare the code in this.setBoundingBox() 1013 if (ratio > 1) { 1014 // h > w 1015 if (ratio < h / w) { 1016 scale_l = w * f / wo; 1017 } else { 1018 scale_l = h * f / ho; 1019 } 1020 } else { 1021 // h <= w 1022 if (ratio < h / w) { 1023 scale_l = w * f / wo; 1024 } else { 1025 scale_l = h * f / ho; 1026 } 1027 } 1028 vshift_l = (h - hi) * 0.5; 1029 1030 // Set a CSS properties to center the JSXGraph div horizontally and vertically 1031 // at the first position of the fullscreen pseudo classes. 1032 for (i = 0; i < len_pseudo; i++) { 1033 try { 1034 inner.style.width = wi + 'px !important'; 1035 inner.style.height = hi + 'px !important'; 1036 inner.style.margin = '0 auto'; 1037 // Add the transform to a possibly already existing transform 1038 inner.style.transform = inner._cssFullscreenStore.transform + 1039 ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')'; 1040 break; 1041 } catch (err) { 1042 JXG.debug("JXG.scaleJSXGraphDiv:\n" + err); 1043 } 1044 } 1045 if (i === len_pseudo) { 1046 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property."); 1047 } 1048 } 1049 1050 } 1051 ); 1052 1053 export default JXG; 1054