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