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