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 /* depends: 37 jxg 38 utils/type 39 */ 40 41 /** 42 * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish 43 * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device 44 * the browser runs on is a tablet/cell or a desktop computer. 45 */ 46 47 define(['jxg', 'utils/type'], function (JXG, Type) { 48 49 'use strict'; 50 51 JXG.extendConstants(JXG, /** @lends JXG */{ 52 /** 53 * Determines the property that stores the relevant information in the event object. 54 * @type String 55 * @default 'touches' 56 * @private 57 */ 58 touchProperty: 'touches', 59 }); 60 61 JXG.extend(JXG, /** @lends JXG */ { 62 /** 63 * Determines whether evt is a touch event. 64 * @param evt {Event} 65 * @returns {Boolean} 66 */ 67 isTouchEvent: function (evt) { 68 return JXG.exists(evt[JXG.touchProperty]); 69 }, 70 71 /** 72 * Determines whether evt is a pointer event. 73 * @param evt {Event} 74 * @returns {Boolean} 75 */ 76 isPointerEvent: function (evt) { 77 return JXG.exists(evt.pointerId); 78 }, 79 80 /** 81 * Determines whether evt is neither a touch event nor a pointer event. 82 * @param evt {Event} 83 * @returns {Boolean} 84 */ 85 isMouseEvent: function (evt) { 86 return !JXG.isTouchEvent(evt)&&!JXG.isPointerEvent(evt); 87 }, 88 89 /** 90 * Determines the number of touch points in a touch event. 91 * For other events, -1 is returned. 92 * @param evt {Event} 93 * @returns {Number} 94 */ 95 getNumberOfTouchPoints: function (evt) { 96 var n = -1; 97 98 if (JXG.isTouchEvent(evt)) { 99 n = evt[JXG.touchProperty].length; 100 } 101 102 return n; 103 }, 104 105 /** 106 * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event. 107 * Attention: When two or more pointer device types are being used concurrently, 108 * it is only checked whether the passed event is the first one of its type! 109 * @param evt {Event} 110 * @returns {boolean} 111 */ 112 isFirstTouch: function (evt) { 113 var touchPoints = JXG.getNumberOfTouchPoints(evt); 114 115 if (JXG.isPointerEvent(evt)) { 116 return evt.isPrimary; 117 } 118 119 return (touchPoints === 1); 120 }, 121 122 /** 123 * A document/window environment is available. 124 * @type Boolean 125 * @default false 126 */ 127 isBrowser: 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 return this.isBrowser && document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1'); 162 }, 163 164 /** 165 * Detect browser support for Canvas. 166 * @returns {Boolean} True, if the browser supports HTML canvas. 167 */ 168 supportsCanvas: function () { 169 var c, hasCanvas = false; 170 171 if (this.isNode()) { 172 try { 173 c = (typeof module === 'object' ? module.require('canvas') : require('canvas')); 174 hasCanvas = !!c; 175 } catch (err) { 176 } 177 } 178 179 return hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext); 180 }, 181 182 /** 183 * True, if run inside a node.js environment. 184 * @returns {Boolean} 185 */ 186 isNode: function () { 187 // this is not a 100% sure but should be valid in most cases 188 189 // we are not inside a browser 190 return !this.isBrowser && ( 191 // there is a module object (plain node, no requirejs) 192 (typeof module === 'object' && !!module.exports) || 193 // there is a global object and requirejs is loaded 194 (typeof global === 'object' && global.requirejsVars && !global.requirejsVars.isBrowser) 195 ); 196 }, 197 198 /** 199 * True if run inside a webworker environment. 200 * @returns {Boolean} 201 */ 202 isWebWorker: function () { 203 return !this.isBrowser && (typeof self === 'object' && typeof self.postMessage === 'function'); 204 }, 205 206 /** 207 * Checks if the environments supports the W3C Pointer Events API {@link http://www.w3.org/Submission/pointer-events/} 208 * @returns {Boolean} 209 */ 210 supportsPointerEvents: function () { 211 return !!(this.isBrowser && window.navigator && 212 (window.PointerEvent || // Chrome/Edge/IE11+ 213 window.navigator.pointerEnabled || // IE11+ 214 window.navigator.msPointerEnabled // IE10- 215 ) 216 ); 217 }, 218 219 /** 220 * Determine if the current browser supports touch events 221 * @returns {Boolean} True, if the browser supports touch events. 222 */ 223 isTouchDevice: function () { 224 return this.isBrowser && window.ontouchstart !== undefined; 225 }, 226 227 /** 228 * Detects if the user is using an Android powered device. 229 * @returns {Boolean} 230 */ 231 isAndroid: function () { 232 return Type.exists(navigator) && navigator.userAgent.toLowerCase().indexOf('android') > -1; 233 }, 234 235 /** 236 * Detects if the user is using the default Webkit browser on an Android powered device. 237 * @returns {Boolean} 238 */ 239 isWebkitAndroid: function () { 240 return this.isAndroid() && navigator.userAgent.indexOf(' AppleWebKit/') > -1; 241 }, 242 243 /** 244 * Detects if the user is using a Apple iPad / iPhone. 245 * @returns {Boolean} 246 */ 247 isApple: function () { 248 return Type.exists(navigator) && (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPhone') > -1); 249 }, 250 251 /** 252 * Detects if the user is using Safari on an Apple device. 253 * @returns {Boolean} 254 */ 255 isWebkitApple: function () { 256 return this.isApple() && (navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1); 257 }, 258 259 /** 260 * Returns true if the run inside a Windows 8 "Metro" App. 261 * @returns {Boolean} 262 */ 263 isMetroApp: function () { 264 return typeof window === 'object' && window.clientInformation && window.clientInformation.appVersion && window.clientInformation.appVersion.indexOf('MSAppHost') > -1; 265 }, 266 267 /** 268 * Detects if the user is using a Mozilla browser 269 * @returns {Boolean} 270 */ 271 isMozilla: function () { 272 return Type.exists(navigator) && 273 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 && 274 navigator.userAgent.toLowerCase().indexOf('apple') === -1; 275 }, 276 277 /** 278 * Detects if the user is using a firefoxOS powered device. 279 * @returns {Boolean} 280 */ 281 isFirefoxOS: function () { 282 return Type.exists(navigator) && 283 navigator.userAgent.toLowerCase().indexOf('android') === -1 && 284 navigator.userAgent.toLowerCase().indexOf('apple') === -1 && 285 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 && 286 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1; 287 }, 288 289 /** 290 * Internet Explorer version. Works only for IE > 4. 291 * @type Number 292 */ 293 ieVersion: (function () { 294 var div, all, 295 v = 3; 296 297 if (typeof document !== 'object') { 298 return 0; 299 } 300 301 div = document.createElement('div'); 302 all = div.getElementsByTagName('i'); 303 304 do { 305 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><' + 'i><' + '/i><![endif]-->'; 306 } while (all[0]); 307 308 return v > 4 ? v : undefined; 309 310 }()), 311 312 /** 313 * Reads the width and height of an HTML element. 314 * @param {String} elementId The HTML id of an HTML DOM node. 315 * @returns {Object} An object with the two properties width and height. 316 */ 317 getDimensions: function (elementId, doc) { 318 var element, display, els, originalVisibility, originalPosition, 319 originalDisplay, originalWidth, originalHeight, style, 320 pixelDimRegExp = /\d+(\.\d*)?px/; 321 322 if (!this.isBrowser || elementId === null) { 323 return { 324 width: 500, 325 height: 500 326 }; 327 } 328 329 doc = doc || document; 330 // Borrowed from prototype.js 331 element = doc.getElementById(elementId); 332 if (!Type.exists(element)) { 333 throw new Error('\nJSXGraph: HTML container element \'' + elementId + '\' not found.'); 334 } 335 336 display = element.style.display; 337 338 // Work around a bug in Safari 339 if (display !== 'none' && display !== null) { 340 if (element.clientWidth > 0 && element.clientHeight > 0) { 341 return {width: element.clientWidth, height: element.clientHeight}; 342 } 343 344 // a parent might be set to display:none; try reading them from styles 345 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style; 346 return { 347 width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0, 348 height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0 349 }; 350 } 351 352 // All *Width and *Height properties give 0 on elements with display set to none, 353 // hence we show the element temporarily 354 els = element.style; 355 356 // save style 357 originalVisibility = els.visibility; 358 originalPosition = els.position; 359 originalDisplay = els.display; 360 361 // show element 362 els.visibility = 'hidden'; 363 els.position = 'absolute'; 364 els.display = 'block'; 365 366 // read the dimension 367 originalWidth = element.clientWidth; 368 originalHeight = element.clientHeight; 369 370 // restore original css values 371 els.display = originalDisplay; 372 els.position = originalPosition; 373 els.visibility = originalVisibility; 374 375 return { 376 width: originalWidth, 377 height: originalHeight 378 }; 379 }, 380 381 /** 382 * Adds an event listener to a DOM element. 383 * @param {Object} obj Reference to a DOM node. 384 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 385 * @param {Function} fn The function to call when the event is triggered. 386 * @param {Object} owner The scope in which the event trigger is called. 387 */ 388 addEvent: function (obj, type, fn, owner) { 389 var el = function () { 390 return fn.apply(owner, arguments); 391 }; 392 393 el.origin = fn; 394 owner['x_internal' + type] = owner['x_internal' + type] || []; 395 owner['x_internal' + type].push(el); 396 397 // Non-IE browser 398 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 399 obj.addEventListener(type, el, false); 400 } 401 402 // IE 403 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 404 obj.attachEvent('on' + type, el); 405 } 406 }, 407 408 /** 409 * Removes an event listener from a DOM element. 410 * @param {Object} obj Reference to a DOM node. 411 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 412 * @param {Function} fn The function to call when the event is triggered. 413 * @param {Object} owner The scope in which the event trigger is called. 414 */ 415 removeEvent: function (obj, type, fn, owner) { 416 var i; 417 418 if (!Type.exists(owner)) { 419 JXG.debug('no such owner'); 420 return; 421 } 422 423 if (!Type.exists(owner['x_internal' + type])) { 424 JXG.debug('no such type: ' + type); 425 return; 426 } 427 428 if (!Type.isArray(owner['x_internal' + type])) { 429 JXG.debug('owner[x_internal + ' + type + '] is not an array'); 430 return; 431 } 432 433 i = Type.indexOf(owner['x_internal' + type], fn, 'origin'); 434 435 if (i === -1) { 436 JXG.debug('removeEvent: no such event function in internal list: ' + fn); 437 return; 438 } 439 440 try { 441 // Non-IE browser 442 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 443 obj.removeEventListener(type, owner['x_internal' + type][i], false); 444 } 445 446 // IE 447 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 448 obj.detachEvent('on' + type, owner['x_internal' + type][i]); 449 } 450 } catch (e) { 451 JXG.debug('event not registered in browser: (' + type + ' -- ' + fn + ')'); 452 } 453 454 owner['x_internal' + type].splice(i, 1); 455 }, 456 457 /** 458 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 459 * of a {@link JXG.Board} because this might corrupt the event handling system. 460 * @param {Object} obj Reference to a DOM node. 461 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 462 * @param {Object} owner The scope in which the event trigger is called. 463 */ 464 removeAllEvents: function (obj, type, owner) { 465 var i, len; 466 if (owner['x_internal' + type]) { 467 len = owner['x_internal' + type].length; 468 469 for (i = len - 1; i >= 0; i--) { 470 JXG.removeEvent(obj, type, owner['x_internal' + type][i].origin, owner); 471 } 472 473 if (owner['x_internal' + type].length > 0) { 474 JXG.debug('removeAllEvents: Not all events could be removed.'); 475 } 476 } 477 }, 478 479 /** 480 * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner. 481 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 482 * @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. 483 * @param {Object} [doc] The document object. 484 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 485 */ 486 getPosition: function (e, index, doc) { 487 var i, len, evtTouches, 488 posx = 0, 489 posy = 0; 490 491 if (!e) { 492 e = window.event; 493 } 494 495 doc = doc || document; 496 evtTouches = e[JXG.touchProperty]; 497 498 // touchend events have their position in "changedTouches" 499 if (Type.exists(evtTouches) && evtTouches.length === 0) { 500 evtTouches = e.changedTouches; 501 } 502 503 if (Type.exists(index) && Type.exists(evtTouches)) { 504 if (index === -1) { 505 len = evtTouches.length; 506 507 for (i = 0; i < len; i++) { 508 if (evtTouches[i]) { 509 e = evtTouches[i]; 510 break; 511 } 512 } 513 514 } else { 515 e = evtTouches[index]; 516 } 517 } 518 519 // Scrolling is ignored. 520 // e.clientX is supported since IE6 521 if (e.clientX) { 522 posx = e.clientX; 523 posy = e.clientY; 524 } 525 526 return [posx, posy]; 527 }, 528 529 /** 530 * Calculates recursively the offset of the DOM element in which the board is stored. 531 * @param {Object} obj A DOM element 532 * @returns {Array} An array with the elements left and top offset. 533 */ 534 getOffset: function (obj) { 535 var cPos, 536 o = obj, 537 o2 = obj, 538 l = o.offsetLeft - o.scrollLeft, 539 t = o.offsetTop - o.scrollTop; 540 541 cPos = this.getCSSTransform([l, t], o); 542 l = cPos[0]; 543 t = cPos[1]; 544 545 /* 546 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 547 * if not to the body. In IE and if we are in an position:absolute environment 548 * offsetParent walks up the DOM hierarchy. 549 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 550 * we need the parentNode steps. 551 */ 552 o = o.offsetParent; 553 while (o) { 554 l += o.offsetLeft; 555 t += o.offsetTop; 556 557 if (o.offsetParent) { 558 l += o.clientLeft - o.scrollLeft; 559 t += o.clientTop - o.scrollTop; 560 } 561 562 cPos = this.getCSSTransform([l, t], o); 563 l = cPos[0]; 564 t = cPos[1]; 565 566 o2 = o2.parentNode; 567 568 while (o2 !== o) { 569 l += o2.clientLeft - o2.scrollLeft; 570 t += o2.clientTop - o2.scrollTop; 571 572 cPos = this.getCSSTransform([l, t], o2); 573 l = cPos[0]; 574 t = cPos[1]; 575 576 o2 = o2.parentNode; 577 } 578 o = o.offsetParent; 579 } 580 581 return [l, t]; 582 }, 583 584 /** 585 * Access CSS style sheets. 586 * @param {Object} obj A DOM element 587 * @param {String} stylename The CSS property to read. 588 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 589 */ 590 getStyle: function (obj, stylename) { 591 var r, 592 doc = obj.ownerDocument; 593 594 // Non-IE 595 if (doc.defaultView && doc.defaultView.getComputedStyle) { 596 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 597 // IE 598 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 599 r = obj.currentStyle[stylename]; 600 } else { 601 if (obj.style) { 602 // make stylename lower camelcase 603 stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) { 604 return letter.toUpperCase(); 605 }); 606 r = obj.style[stylename]; 607 } 608 } 609 610 return r; 611 }, 612 613 /** 614 * Reads css style sheets of a given element. This method is a getStyle wrapper and 615 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 616 * @param {DOMElement} el 617 * @param {string} css 618 * @returns {number} 619 */ 620 getProp: function (el, css) { 621 var n = parseInt(this.getStyle(el, css), 10); 622 return isNaN(n) ? 0 : n; 623 }, 624 625 /** 626 * Correct position of upper left corner in case of 627 * a CSS transformation. Here, only translations are 628 * extracted. All scaling transformations are corrected 629 * in {@link JXG.Board#getMousePosition}. 630 * @param {Array} cPos Previously determined position 631 * @param {Object} obj A DOM element 632 * @returns {Array} The corrected position. 633 */ 634 getCSSTransform: function (cPos, obj) { 635 var i, j, str, arrStr, start, len, len2, arr, 636 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform']; 637 638 // Take the first transformation matrix 639 len = t.length; 640 641 for (i = 0, str = ''; i < len; i++) { 642 if (Type.exists(obj.style[t[i]])) { 643 str = obj.style[t[i]]; 644 break; 645 } 646 } 647 648 /** 649 * Extract the coordinates and apply the transformation 650 * to cPos 651 */ 652 if (str !== '') { 653 start = str.indexOf('('); 654 655 if (start > 0) { 656 len = str.length; 657 arrStr = str.substring(start + 1, len - 1); 658 arr = arrStr.split(','); 659 660 for (j = 0, len2 = arr.length; j < len2; j++) { 661 arr[j] = parseFloat(arr[j]); 662 } 663 664 if (str.indexOf('matrix') === 0) { 665 cPos[0] += arr[4]; 666 cPos[1] += arr[5]; 667 } else if (str.indexOf('translateX') === 0) { 668 cPos[0] += arr[0]; 669 } else if (str.indexOf('translateY') === 0) { 670 cPos[1] += arr[0]; 671 } else if (str.indexOf('translate') === 0) { 672 cPos[0] += arr[0]; 673 cPos[1] += arr[1]; 674 } 675 } 676 } 677 678 // Zoom is used by reveal.js 679 if (Type.exists(obj.style.zoom)) { 680 str = obj.style.zoom; 681 if (str !== '') { 682 cPos[0] *= parseFloat(str); 683 cPos[1] *= parseFloat(str); 684 } 685 } 686 687 return cPos; 688 }, 689 690 /** 691 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 692 * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported. 693 * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}. 694 */ 695 getCSSTransformMatrix: function (obj) { 696 var i, j, str, arrstr, start, len, len2, arr, 697 st, 698 doc = obj.ownerDocument, 699 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'], 700 mat = [[1, 0, 0], 701 [0, 1, 0], 702 [0, 0, 1]]; 703 704 // This should work on all browsers except IE 6-8 705 if (doc.defaultView && doc.defaultView.getComputedStyle) { 706 st = doc.defaultView.getComputedStyle(obj, null); 707 str = st.getPropertyValue('-webkit-transform') || 708 st.getPropertyValue('-moz-transform') || 709 st.getPropertyValue('-ms-transform') || 710 st.getPropertyValue('-o-transform') || 711 st.getPropertyValue('transform'); 712 } else { 713 // Take the first transformation matrix 714 len = t.length; 715 for (i = 0, str = ''; i < len; i++) { 716 if (Type.exists(obj.style[t[i]])) { 717 str = obj.style[t[i]]; 718 break; 719 } 720 } 721 } 722 723 if (str !== '') { 724 start = str.indexOf('('); 725 726 if (start > 0) { 727 len = str.length; 728 arrstr = str.substring(start + 1, len - 1); 729 arr = arrstr.split(','); 730 731 for (j = 0, len2 = arr.length; j < len2; j++) { 732 arr[j] = parseFloat(arr[j]); 733 } 734 735 if (str.indexOf('matrix') === 0) { 736 mat = [[1, 0, 0], 737 [0, arr[0], arr[1]], 738 [0, arr[2], arr[3]]]; 739 } else if (str.indexOf('scaleX') === 0) { 740 mat[1][1] = arr[0]; 741 } else if (str.indexOf('scaleY') === 0) { 742 mat[2][2] = arr[0]; 743 } else if (str.indexOf('scale') === 0) { 744 mat[1][1] = arr[0]; 745 mat[2][2] = arr[1]; 746 } 747 } 748 } 749 750 // CSS style zoom is used by reveal.js 751 // Recursively search for zoom style entries. 752 // This is necessary for reveal.js on webkit. 753 // It fails if the user does zooming 754 if (Type.exists(obj.style.zoom)) { 755 str = obj.style.zoom; 756 if (str !== '') { 757 mat[1][1] *= parseFloat(str); 758 mat[2][2] *= parseFloat(str); 759 } 760 } 761 762 return mat; 763 }, 764 765 /** 766 * Process data in timed chunks. Data which takes long to process, either because it is such 767 * a huge amount of data or the processing takes some time, causes warnings in browsers about 768 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 769 * called chunks which will be processed in serial order. 770 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 771 * @param {Array} items to do 772 * @param {Function} process Function that is applied for every array item 773 * @param {Object} context The scope of function process 774 * @param {Function} callback This function is called after the last array element has been processed. 775 */ 776 timedChunk: function (items, process, context, callback) { 777 //create a clone of the original 778 var todo = items.concat(), 779 timerFun = function () { 780 var start = +new Date(); 781 782 do { 783 process.call(context, todo.shift()); 784 } while (todo.length > 0 && (+new Date() - start < 300)); 785 786 if (todo.length > 0) { 787 window.setTimeout(timerFun, 1); 788 } else { 789 callback(items); 790 } 791 }; 792 793 window.setTimeout(timerFun, 1); 794 }, 795 796 /** 797 * Scale and vertically shift a DOM element (usually a JSXGraph div) 798 * inside of a parent DOM 799 * element which is set to fullscreen. 800 * This is realized with a CSS transformation. 801 * 802 * @param {String} wrap_id id of the parent DOM element which is in fullscreen mode 803 * @param {String} inner_id id of the DOM element which is scaled and shifted 804 * @param {Object} doc document object or shadow root 805 * 806 * @private 807 * @see JXG.Board#toFullscreen 808 * @see JXG.Board#fullscreenListener 809 * 810 */ 811 scaleJSXGraphDiv: function (wrap_id, inner_id, doc) { 812 var len = doc.styleSheets.length, style, rule, w, h, b, wi, hi, bi, 813 scale_l, vshift_l, // scale_p, vshift_p, 814 f = 0.85, 815 rule_inner_l, // rule_inner_p, 816 817 pseudo_keys = [':fullscreen', ':-webkit-full-screen', ':-moz-full-screen', ':-ms-fullscreen'], 818 len_pseudo = pseudo_keys.length, i, 819 820 // A previously installed CSS rule to center the JSXGraph div has to 821 // be searched and removed again. 822 regex = new RegExp('.*#' + wrap_id + ':.*full.*screen.*#' + inner_id + '.*auto;.*transform:.*matrix'); 823 824 b = doc.getElementById(wrap_id).getBoundingClientRect(); 825 h = b.height; 826 w = b.width; 827 828 bi = doc.getElementById(inner_id).getBoundingClientRect(); 829 hi = bi.height; 830 wi = bi.width; 831 832 if (wi / hi >= w / h) { 833 scale_l = f * w / wi; 834 } else { 835 scale_l = f * h / hi; 836 } 837 vshift_l = (h - hi) * 0.5; 838 839 // CSS rules to center the inner div horizontally and vertically. 840 rule_inner_l = '{margin:0 auto;transform:matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ');}'; 841 842 if (len === 0) { 843 // In case there is not a single CSS rule defined at all. 844 style = document.createElement('style'); 845 // WebKit hack :( 846 style.appendChild(document.createTextNode('')); 847 // Add the <style> element to the page 848 doc.appendChild(style); 849 len = doc.styleSheets.length; 850 } 851 852 // Remove a previously installed CSS rule. 853 if (doc.styleSheets[len - 1].cssRules.length > 0 && 854 regex.test(doc.styleSheets[len - 1].cssRules[0].cssText) && 855 doc.styleSheets[len - 1].deleteRule) { 856 doc.styleSheets[len - 1].deleteRule(0); 857 } 858 859 // Install a CSS rule to center the JSXGraph div at the first position of the list. 860 for (i = 0; i < len_pseudo; i++) { 861 try { 862 rule = '#' + wrap_id + pseudo_keys[i] + ' #' + inner_id + rule_inner_l; 863 // rule = '@media all and (orientation:landscape) {' + rule + '}'; 864 doc.styleSheets[len - 1].insertRule(rule, 0); 865 866 break; 867 } catch (err) { 868 // console.log('JXG.scaleJSXGraphDiv: Could not add CSS rule "' + pseudo_keys[i] + '".'); 869 // console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.'); 870 } 871 } 872 if (i === len_pseudo) { 873 console.log('JXG.scaleJSXGraphDiv: Could not add any CSS rule.'); 874 console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.'); 875 } 876 } 877 878 }); 879 880 return JXG; 881 }); 882