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