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