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