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 * Determines whether evt is a touch event. 63 * @param evt {Event} 64 * @returns {Boolean} 65 */ 66 isTouchEvent: function (evt) { 67 return JXG.exists(evt['touches']); // Old iOS touch events 68 }, 69 70 /** 71 * Determines whether evt is a pointer event. 72 * @param evt {Event} 73 * @returns {Boolean} 74 */ 75 isPointerEvent: function (evt) { 76 return JXG.exists(evt.pointerId); 77 }, 78 79 /** 80 * Determines whether evt is neither a touch event nor a pointer event. 81 * @param evt {Event} 82 * @returns {Boolean} 83 */ 84 isMouseEvent: function (evt) { 85 return !JXG.isTouchEvent(evt) && !JXG.isPointerEvent(evt); 86 }, 87 88 /** 89 * Determines the number of touch points in a touch event. 90 * For other events, -1 is returned. 91 * @param evt {Event} 92 * @returns {Number} 93 */ 94 getNumberOfTouchPoints: function (evt) { 95 var n = -1; 96 97 if (JXG.isTouchEvent(evt)) { 98 n = evt['touches'].length; 99 } 100 101 return n; 102 }, 103 104 /** 105 * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event. 106 * Attention: When two or more pointer device types are being used concurrently, 107 * it is only checked whether the passed event is the first one of its type! 108 * @param evt {Event} 109 * @returns {boolean} 110 */ 111 isFirstTouch: function (evt) { 112 var touchPoints = JXG.getNumberOfTouchPoints(evt); 113 114 if (JXG.isPointerEvent(evt)) { 115 return evt.isPrimary; 116 } 117 118 return touchPoints === 1; 119 }, 120 121 /** 122 * A document/window environment is available. 123 * @type Boolean 124 * @default false 125 */ 126 // isBrowser: Type.exists(window) && Type.exists(document) && 127 // typeof window === "object" && typeof document === "object", 128 isBrowser: typeof window !== 'undefined' && typeof document !== 'undefined' && 129 typeof window === "object" && typeof document === "object", 130 131 /** 132 * Features of ECMAScript 6+ are available. 133 * @type Boolean 134 * @default false 135 */ 136 supportsES6: function () { 137 // var testMap; 138 /* jshint ignore:start */ 139 try { 140 // This would kill the old uglifyjs: testMap = (a = 0) => a; 141 new Function("(a = 0) => a"); 142 return true; 143 } catch (err) { 144 return false; 145 } 146 /* jshint ignore:end */ 147 }, 148 149 /** 150 * Detect browser support for VML. 151 * @returns {Boolean} True, if the browser supports VML. 152 */ 153 supportsVML: function () { 154 // From stackoverflow.com 155 return this.isBrowser && !!document.namespaces; 156 }, 157 158 /** 159 * Detect browser support for SVG. 160 * @returns {Boolean} True, if the browser supports SVG. 161 */ 162 supportsSVG: function () { 163 var svgSupport; 164 if (!this.isBrowser) { 165 return false; 166 } 167 svgSupport = !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect; 168 return svgSupport; 169 }, 170 171 /** 172 * Detect browser support for Canvas. 173 * @returns {Boolean} True, if the browser supports HTML canvas. 174 */ 175 supportsCanvas: function () { 176 var hasCanvas = false; 177 178 // if (this.isNode()) { 179 // try { 180 // // c = typeof module === "object" ? module.require('canvas') : $__canvas; 181 // c = typeof module === "object" ? module.require('canvas') : import('canvas'); 182 // hasCanvas = !!c; 183 // } catch (err) {} 184 // } 185 186 if (this.isNode()) { 187 //try { 188 // JXG.createCanvas(500, 500); 189 hasCanvas = true; 190 // } catch (err) { 191 // throw new Error('JXG.createCanvas not available.\n' + 192 // 'Install the npm package `canvas`\n' + 193 // 'and call:\n' + 194 // ' import { createCanvas } from 'canvas.js'\n' + 195 // ' JXG.createCanvas = createCanvas;\n'); 196 // } 197 } 198 199 return ( 200 hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext) 201 ); 202 }, 203 204 /** 205 * True, if run inside a node.js environment. 206 * @returns {Boolean} 207 */ 208 isNode: function () { 209 // This is not a 100% sure but should be valid in most cases 210 // We are not inside a browser 211 /* eslint-disable no-undef */ 212 return ( 213 !this.isBrowser && 214 (typeof process !== 'undefined') && 215 (process.release.name.search(/node|io.js/) !== -1) 216 /* eslint-enable no-undef */ 217 218 // there is a module object (plain node, no requirejs) 219 // ((typeof module === "object" && !!module.exports) || 220 // // there is a global object and requirejs is loaded 221 // (typeof global === "object" && 222 // global.requirejsVars && 223 // !global.requirejsVars.isBrowser) 224 // ) 225 ); 226 }, 227 228 /** 229 * True if run inside a webworker environment. 230 * @returns {Boolean} 231 */ 232 isWebWorker: function () { 233 return ( 234 !this.isBrowser && 235 typeof self === "object" && 236 typeof self.postMessage === "function" 237 ); 238 }, 239 240 /** 241 * Checks if the environments supports the W3C Pointer Events API {@link https://www.w3.org/TR/pointerevents/} 242 * @returns {Boolean} 243 */ 244 supportsPointerEvents: function () { 245 return !!( 246 ( 247 this.isBrowser && 248 window.navigator && 249 (window.PointerEvent || // Chrome/Edge/IE11+ 250 window.navigator.pointerEnabled || // IE11+ 251 window.navigator.msPointerEnabled) 252 ) // IE10- 253 ); 254 }, 255 256 /** 257 * Determine if the current browser supports touch events 258 * @returns {Boolean} True, if the browser supports touch events. 259 */ 260 isTouchDevice: function () { 261 return this.isBrowser && window.ontouchstart !== undefined; 262 }, 263 264 /** 265 * Detects if the user is using an Android powered device. 266 * @returns {Boolean} 267 * @deprecated 268 */ 269 isAndroid: function () { 270 return ( 271 Type.exists(navigator) && 272 navigator.userAgent.toLowerCase().indexOf('android') > -1 273 ); 274 }, 275 276 /** 277 * Detects if the user is using the default Webkit browser on an Android powered device. 278 * @returns {Boolean} 279 * @deprecated 280 */ 281 isWebkitAndroid: function () { 282 return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1; 283 }, 284 285 /** 286 * Detects if the user is using a Apple iPad / iPhone. 287 * @returns {Boolean} 288 * @deprecated 289 */ 290 isApple: function () { 291 return ( 292 Type.exists(navigator) && 293 (navigator.userAgent.indexOf('iPad') > -1 || 294 navigator.userAgent.indexOf('iPhone') > -1) 295 ); 296 }, 297 298 /** 299 * Detects if the user is using Safari on an Apple device. 300 * See https://evilmartians.com/chronicles/how-to-detect-safari-and-ios-versions-with-ease (2025) 301 * @returns {Boolean} 302 * @deprecated 303 */ 304 isWebkitApple: function () { 305 var is = ('GestureEvent' in window) && // Desktop and mobile 306 ( 307 ('ongesturechange' in window) || // mobile webkit browsers and webview iOS 308 (window !== undefined && // Desktop Safari 309 'safari' in window && 310 'pushNotification' in window.safari) 311 ); 312 return is; 313 314 // return ( 315 // this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1 316 // ); 317 }, 318 319 /** 320 * Returns true if the run inside a Windows 8 "Metro" App. 321 * @returns {Boolean} 322 * @deprecated 323 */ 324 isMetroApp: function () { 325 return ( 326 typeof window === "object" && 327 window.clientInformation && 328 window.clientInformation.appVersion && 329 window.clientInformation.appVersion.indexOf('MSAppHost') > -1 330 ); 331 }, 332 333 /** 334 * Detects if the user is using a Mozilla browser 335 * @returns {Boolean} 336 * @deprecated 337 */ 338 isMozilla: function () { 339 return ( 340 Type.exists(navigator) && 341 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 && 342 navigator.userAgent.toLowerCase().indexOf('apple') === -1 343 ); 344 }, 345 346 /** 347 * Detects if the user is using a firefoxOS powered device. 348 * @returns {Boolean} 349 * @deprecated 350 */ 351 isFirefoxOS: function () { 352 return ( 353 Type.exists(navigator) && 354 navigator.userAgent.toLowerCase().indexOf('android') === -1 && 355 navigator.userAgent.toLowerCase().indexOf('apple') === -1 && 356 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 && 357 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 358 ); 359 }, 360 361 /** 362 * Detects if the user is using a desktop device, see <a href="https://stackoverflow.com/a/61073480">https://stackoverflow.com/a/61073480</a>. 363 * @returns {boolean} 364 * 365 * @deprecated 366 */ 367 isDesktop: function () { 368 return true; 369 // console.log("isDesktop", screen.orientation); 370 // const navigatorAgent = 371 // navigator.userAgent || navigator.vendor || window.opera; 372 // return !( 373 // /(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( 374 // navigatorAgent 375 // ) || 376 // /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( 377 // navigatorAgent.substr(0, 4) 378 // ) 379 // ); 380 }, 381 382 /** 383 * 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>. 384 * @returns {boolean} 385 * 386 * @deprecated 387 * 388 */ 389 isMobile: function () { 390 return true; 391 // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 392 }, 393 394 /** 395 * Internet Explorer version. Works only for IE > 4. 396 * @type Number 397 * @deprecated 398 */ 399 ieVersion: (function () { 400 var div, 401 all, 402 v = 3; 403 404 if (typeof document === 'undefined' || document === null || typeof document !== 'object') { 405 return 0; 406 } 407 408 div = document.createElement('div'); 409 all = div.getElementsByTagName('i'); 410 411 do { 412 div.innerHTML = "<!--[if gt IE " + (++v) + "]><" + "i><" + "/i><![endif]-->"; 413 } while (all[0]); 414 415 return v > 4 ? v : undefined; 416 })(), 417 418 /** 419 * Reads the width and height of an HTML element. 420 * @param {String|Object} elementId id of or reference to an HTML DOM node. 421 * @returns {Object} An object with the two properties width and height. 422 */ 423 getDimensions: function (elementId, doc) { 424 var element, 425 display, 426 els, 427 originalVisibility, 428 originalPosition, 429 originalDisplay, 430 originalWidth, 431 originalHeight, 432 style, 433 pixelDimRegExp = /\d+(\.\d*)?px/; 434 435 if (!this.isBrowser || elementId === null) { 436 return { 437 width: 500, 438 height: 500 439 }; 440 } 441 442 doc = doc || document; 443 // Borrowed from prototype.js 444 element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId; 445 if (!Type.exists(element)) { 446 throw new Error( 447 "\nJSXGraph: HTML container element '" + elementId + "' not found." 448 ); 449 } 450 451 display = element.style.display; 452 453 // Work around a bug in Safari 454 if (display !== "none" && display !== null) { 455 if (element.clientWidth > 0 && element.clientHeight > 0) { 456 return { width: element.clientWidth, height: element.clientHeight }; 457 } 458 459 // A parent might be set to display:none; try reading them from styles 460 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style; 461 return { 462 width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0, 463 height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0 464 }; 465 } 466 467 // All *Width and *Height properties give 0 on elements with display set to none, 468 // hence we show the element temporarily 469 els = element.style; 470 471 // save style 472 originalVisibility = els.visibility; 473 originalPosition = els.position; 474 originalDisplay = els.display; 475 476 // show element 477 els.visibility = 'hidden'; 478 els.position = 'absolute'; 479 els.display = 'block'; 480 481 // read the dimension 482 originalWidth = element.clientWidth; 483 originalHeight = element.clientHeight; 484 485 // restore original css values 486 els.display = originalDisplay; 487 els.position = originalPosition; 488 els.visibility = originalVisibility; 489 490 return { 491 width: originalWidth, 492 height: originalHeight 493 }; 494 }, 495 496 /** 497 * Adds an event listener to a DOM element. 498 * @param {Object} obj Reference to a DOM node. 499 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 500 * @param {Function} fn The function to call when the event is triggered. 501 * @param {Object} owner The scope in which the event trigger is called. 502 * @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 503 * an options object or the useCapture Boolean. 504 * 505 */ 506 addEvent: function (obj, type, fn, owner, options) { 507 var el = function () { 508 return fn.apply(owner, arguments); 509 }; 510 511 el.origin = fn; 512 // Check if owner is a board 513 if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) { 514 owner['x_internal' + type] = owner['x_internal' + type] || []; 515 owner['x_internal' + type].push(el); 516 } 517 518 // Non-IE browser 519 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 520 options = options || false; // options or useCapture 521 obj.addEventListener(type, el, options); 522 } 523 524 // IE 525 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 526 obj.attachEvent("on" + type, el); 527 } 528 }, 529 530 /** 531 * Removes an event listener from a DOM element. 532 * @param {Object} obj Reference to a DOM node. 533 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 534 * @param {Function} fn The function to call when the event is triggered. 535 * @param {Object} owner The scope in which the event trigger is called. 536 */ 537 removeEvent: function (obj, type, fn, owner) { 538 var i; 539 540 if (!Type.exists(owner)) { 541 JXG.debug("no such owner"); 542 return; 543 } 544 545 if (!Type.exists(owner["x_internal" + type])) { 546 JXG.debug("removeEvent: no such type: " + type); 547 return; 548 } 549 550 if (!Type.isArray(owner["x_internal" + type])) { 551 JXG.debug("owner[x_internal + " + type + "] is not an array"); 552 return; 553 } 554 555 i = Type.indexOf(owner["x_internal" + type], fn, 'origin'); 556 557 if (i === -1) { 558 JXG.debug("removeEvent: no such event function in internal list: " + fn); 559 return; 560 } 561 562 try { 563 // Non-IE browser 564 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 565 obj.removeEventListener(type, owner["x_internal" + type][i], false); 566 } 567 568 // IE 569 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 570 obj.detachEvent("on" + type, owner["x_internal" + type][i]); 571 } 572 } catch (e) { 573 JXG.debug("removeEvent: event not registered in browser: (" + type + " -- " + fn + ")"); 574 } 575 576 owner["x_internal" + type].splice(i, 1); 577 }, 578 579 /** 580 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 581 * of a {@link JXG.Board} because this might corrupt the event handling system. 582 * @param {Object} obj Reference to a DOM node. 583 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 584 * @param {Object} owner The scope in which the event trigger is called. 585 */ 586 removeAllEvents: function (obj, type, owner) { 587 var i, len; 588 if (owner["x_internal" + type]) { 589 len = owner["x_internal" + type].length; 590 591 for (i = len - 1; i >= 0; i--) { 592 JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner); 593 } 594 595 if (owner["x_internal" + type].length > 0) { 596 JXG.debug("removeAllEvents: Not all events could be removed."); 597 } 598 } 599 }, 600 601 /** 602 * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner. 603 * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent. 604 * 605 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 606 * @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. 607 * @param {Object} [doc] The document object. 608 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 609 */ 610 getPosition: function (e, index, doc) { 611 var i, 612 len, 613 evtTouches, 614 posx = 0, 615 posy = 0; 616 617 if (!e) { 618 e = window.event; 619 } 620 621 doc = doc || document; 622 evtTouches = e['touches']; // iOS touch events 623 624 // touchend events have their position in "changedTouches" 625 if (Type.exists(evtTouches) && evtTouches.length === 0) { 626 evtTouches = e.changedTouches; 627 } 628 629 if (Type.exists(index) && Type.exists(evtTouches)) { 630 if (index === -1) { 631 len = evtTouches.length; 632 633 for (i = 0; i < len; i++) { 634 if (evtTouches[i]) { 635 e = evtTouches[i]; 636 break; 637 } 638 } 639 } else { 640 e = evtTouches[index]; 641 } 642 } 643 644 // Scrolling is ignored. 645 // e.clientX is supported since IE6 646 if (e.clientX) { 647 posx = e.clientX; 648 posy = e.clientY; 649 } 650 651 return [posx, posy]; 652 }, 653 654 /** 655 * Calculates recursively the offset of the DOM element in which the board is stored. 656 * @param {Object} obj A DOM element 657 * @returns {Array} An array with the elements left and top offset. 658 */ 659 getOffset: function (obj) { 660 var cPos, 661 o = obj, 662 o2 = obj, 663 l = o.offsetLeft - o.scrollLeft, 664 t = o.offsetTop - o.scrollTop; 665 666 cPos = this.getCSSTransform([l, t], o); 667 l = cPos[0]; 668 t = cPos[1]; 669 670 /* 671 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 672 * if not to the body. In IE and if we are in an position:absolute environment 673 * offsetParent walks up the DOM hierarchy. 674 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 675 * we need the parentNode steps. 676 */ 677 o = o.offsetParent; 678 while (o) { 679 l += o.offsetLeft; 680 t += o.offsetTop; 681 682 if (o.offsetParent) { 683 l += o.clientLeft - o.scrollLeft; 684 t += o.clientTop - o.scrollTop; 685 } 686 687 cPos = this.getCSSTransform([l, t], o); 688 l = cPos[0]; 689 t = cPos[1]; 690 691 o2 = o2.parentNode; 692 693 while (o2 !== o) { 694 l += o2.clientLeft - o2.scrollLeft; 695 t += o2.clientTop - o2.scrollTop; 696 697 cPos = this.getCSSTransform([l, t], o2); 698 l = cPos[0]; 699 t = cPos[1]; 700 701 o2 = o2.parentNode; 702 } 703 o = o.offsetParent; 704 } 705 706 return [l, t]; 707 }, 708 709 /** 710 * Access CSS style sheets. 711 * @param {Object} obj A DOM element 712 * @param {String} stylename The CSS property to read. 713 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 714 */ 715 getStyle: function (obj, stylename) { 716 var r, 717 doc = obj.ownerDocument; 718 719 // Non-IE 720 if (doc.defaultView && doc.defaultView.getComputedStyle) { 721 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 722 // IE 723 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 724 r = obj.currentStyle[stylename]; 725 } else { 726 if (obj.style) { 727 // make stylename lower camelcase 728 stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) { 729 return letter.toUpperCase(); 730 }); 731 r = obj.style[stylename]; 732 } 733 } 734 735 return r; 736 }, 737 738 /** 739 * Reads css style sheets of a given element. This method is a getStyle wrapper and 740 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 741 * @param {DOMElement} el 742 * @param {string} css 743 * @returns {number} 744 */ 745 getProp: function (el, css) { 746 var n = parseInt(this.getStyle(el, css), 10); 747 return isNaN(n) ? 0 : n; 748 }, 749 750 /** 751 * Correct position of upper left corner in case of 752 * a CSS transformation. Here, only translations are 753 * extracted. All scaling transformations are corrected 754 * in {@link JXG.Board#getMousePosition}. 755 * @param {Array} cPos Previously determined position 756 * @param {Object} obj A DOM element 757 * @returns {Array} The corrected position. 758 */ 759 getCSSTransform: function (cPos, obj) { 760 var i, 761 j, 762 str, 763 arrStr, 764 start, 765 len, 766 len2, 767 arr, 768 t = [ 769 "transform", 770 "webkitTransform", 771 "MozTransform", 772 "msTransform", 773 "oTransform" 774 ]; 775 776 // Take the first transformation matrix 777 len = t.length; 778 779 for (i = 0, str = ""; i < len; i++) { 780 if (Type.exists(obj.style[t[i]])) { 781 str = obj.style[t[i]]; 782 break; 783 } 784 } 785 786 /** 787 * Extract the coordinates and apply the transformation 788 * to cPos 789 */ 790 if (str !== "") { 791 start = str.indexOf("("); 792 793 if (start > 0) { 794 len = str.length; 795 arrStr = str.substring(start + 1, len - 1); 796 arr = arrStr.split(","); 797 798 for (j = 0, len2 = arr.length; j < len2; j++) { 799 arr[j] = parseFloat(arr[j]); 800 } 801 802 if (str.indexOf('matrix') === 0) { 803 cPos[0] += arr[4]; 804 cPos[1] += arr[5]; 805 } else if (str.indexOf('translateX') === 0) { 806 cPos[0] += arr[0]; 807 } else if (str.indexOf('translateY') === 0) { 808 cPos[1] += arr[0]; 809 } else if (str.indexOf('translate') === 0) { 810 cPos[0] += arr[0]; 811 cPos[1] += arr[1]; 812 } 813 } 814 } 815 816 // Zoom is used by reveal.js 817 if (Type.exists(obj.style.zoom)) { 818 str = obj.style.zoom; 819 if (str !== "") { 820 cPos[0] *= parseFloat(str); 821 cPos[1] *= parseFloat(str); 822 } 823 } 824 825 return cPos; 826 }, 827 828 /** 829 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 830 * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported. 831 * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}. 832 */ 833 getCSSTransformMatrix: function (obj) { 834 var i, j, str, arrstr, arr, 835 start, len, len2, st, 836 doc = obj.ownerDocument, 837 t = [ 838 "transform", 839 "webkitTransform", 840 "MozTransform", 841 "msTransform", 842 "oTransform" 843 ], 844 mat = [ 845 [1, 0, 0], 846 [0, 1, 0], 847 [0, 0, 1] 848 ]; 849 850 // This should work on all browsers except IE 6-8 851 if (doc.defaultView && doc.defaultView.getComputedStyle) { 852 st = doc.defaultView.getComputedStyle(obj, null); 853 str = 854 st.getPropertyValue("-webkit-transform") || 855 st.getPropertyValue("-moz-transform") || 856 st.getPropertyValue("-ms-transform") || 857 st.getPropertyValue("-o-transform") || 858 st.getPropertyValue('transform'); 859 } else { 860 // Take the first transformation matrix 861 len = t.length; 862 for (i = 0, str = ""; i < len; i++) { 863 if (Type.exists(obj.style[t[i]])) { 864 str = obj.style[t[i]]; 865 break; 866 } 867 } 868 } 869 870 // Convert and reorder the matrix for JSXGraph 871 if (str !== "") { 872 start = str.indexOf("("); 873 874 if (start > 0) { 875 len = str.length; 876 arrstr = str.substring(start + 1, len - 1); 877 arr = arrstr.split(","); 878 879 for (j = 0, len2 = arr.length; j < len2; j++) { 880 arr[j] = parseFloat(arr[j]); 881 } 882 883 if (str.indexOf('matrix') === 0) { 884 mat = [ 885 [1, 0, 0], 886 [0, arr[0], arr[1]], 887 [0, arr[2], arr[3]] 888 ]; 889 } else if (str.indexOf('scaleX') === 0) { 890 mat[1][1] = arr[0]; 891 } else if (str.indexOf('scaleY') === 0) { 892 mat[2][2] = arr[0]; 893 } else if (str.indexOf('scale') === 0) { 894 mat[1][1] = arr[0]; 895 mat[2][2] = arr[1]; 896 } 897 } 898 } 899 900 // CSS style zoom is used by reveal.js 901 // Recursively search for zoom style entries. 902 // This is necessary for reveal.js on webkit. 903 // It fails if the user does zooming 904 if (Type.exists(obj.style.zoom)) { 905 str = obj.style.zoom; 906 if (str !== "") { 907 mat[1][1] *= parseFloat(str); 908 mat[2][2] *= parseFloat(str); 909 } 910 } 911 912 return mat; 913 }, 914 915 /** 916 * Process data in timed chunks. Data which takes long to process, either because it is such 917 * a huge amount of data or the processing takes some time, causes warnings in browsers about 918 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 919 * called chunks which will be processed in serial order. 920 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 921 * @param {Array} items to do 922 * @param {Function} process Function that is applied for every array item 923 * @param {Object} context The scope of function process 924 * @param {Function} callback This function is called after the last array element has been processed. 925 */ 926 timedChunk: function (items, process, context, callback) { 927 //create a clone of the original 928 var todo = items.slice(), 929 timerFun = function () { 930 var start = +new Date(); 931 932 do { 933 process.call(context, todo.shift()); 934 } while (todo.length > 0 && +new Date() - start < 300); 935 936 if (todo.length > 0) { 937 window.setTimeout(timerFun, 1); 938 } else { 939 callback(items); 940 } 941 }; 942 943 window.setTimeout(timerFun, 1); 944 }, 945 946 /** 947 * Scale and vertically shift a DOM element (usually a JSXGraph div) 948 * inside of a parent DOM 949 * element which is set to fullscreen. 950 * This is realized with a CSS transformation. 951 * 952 * @param {String} wrap_id id of the parent DOM element which is in fullscreen mode 953 * @param {String} inner_id id of the DOM element which is scaled and shifted 954 * @param {Object} doc document object or shadow root 955 * @param {Number} scale Relative size of the JSXGraph board in the fullscreen window. 956 * 957 * @private 958 * @see JXG.Board#toFullscreen 959 * @see JXG.Board#fullscreenListener 960 * 961 */ 962 scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) { 963 var w, h, b, 964 wi, hi, 965 wo, ho, inner, 966 scale_l, vshift_l, 967 f = scale, 968 ratio, 969 pseudo_keys = [ 970 ":fullscreen", 971 ":-webkit-full-screen", 972 ":-moz-full-screen", 973 ":-ms-fullscreen" 974 ], 975 len_pseudo = pseudo_keys.length, 976 i; 977 978 b = doc.getElementById(wrap_id).getBoundingClientRect(); 979 h = b.height; 980 w = b.width; 981 982 inner = doc.getElementById(inner_id); 983 wo = inner._cssFullscreenStore.w; 984 ho = inner._cssFullscreenStore.h; 985 ratio = ho / wo; 986 987 // Scale the div such that fits into the fullscreen. 988 if (wo > w * f) { 989 wo = w * f; 990 ho = wo * ratio; 991 } 992 if (ho > h * f) { 993 ho = h * f; 994 wo = ho / ratio; 995 } 996 997 wi = wo; 998 hi = ho; 999 // Compare the code in this.setBoundingBox() 1000 if (ratio > 1) { 1001 // h > w 1002 if (ratio < h / w) { 1003 scale_l = w * f / wo; 1004 } else { 1005 scale_l = h * f / ho; 1006 } 1007 } else { 1008 // h <= w 1009 if (ratio < h / w) { 1010 scale_l = w * f / wo; 1011 } else { 1012 scale_l = h * f / ho; 1013 } 1014 } 1015 vshift_l = (h - hi) * 0.5; 1016 1017 // Set a CSS properties to center the JSXGraph div horizontally and vertically 1018 // at the first position of the fullscreen pseudo classes. 1019 for (i = 0; i < len_pseudo; i++) { 1020 try { 1021 inner.style.width = wi + 'px !important'; 1022 inner.style.height = hi + 'px !important'; 1023 inner.style.margin = '0 auto'; 1024 // Add the transform to a possibly already existing transform 1025 inner.style.transform = inner._cssFullscreenStore.transform + 1026 ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')'; 1027 break; 1028 } catch (err) { 1029 JXG.debug("JXG.scaleJSXGraphDiv:\n" + err); 1030 } 1031 } 1032 if (i === len_pseudo) { 1033 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property."); 1034 } 1035 } 1036 1037 } 1038 ); 1039 1040 export default JXG; 1041