      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|Object} elementId id of or reference to 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 = (Type.isString(elementId)) ? doc.getElementById(elementId) : 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, j, str, arrstr, arr,
    781                 start, len, len2, st,
    782                 doc = obj.ownerDocument,
    783                 t = [
    784                     "transform",
    785                     "webkitTransform",
    786                     "MozTransform",
    787                     "msTransform",
    788                     "oTransform"
    789                 ],
    790                 mat = [
    791                     [1, 0, 0],
    792                     [0, 1, 0],
    793                     [0, 0, 1]
    794                 ];
    795
    796             // This should work on all browsers except IE 6-8
    797             if (doc.defaultView && doc.defaultView.getComputedStyle) {
    798                 st = doc.defaultView.getComputedStyle(obj, null);
    799                 str =
    800                     st.getPropertyValue("-webkit-transform") ||
    801                     st.getPropertyValue("-moz-transform") ||
    802                     st.getPropertyValue("-ms-transform") ||
    803                     st.getPropertyValue("-o-transform") ||
    804                     st.getPropertyValue("transform");
    805             } else {
    806                 // Take the first transformation matrix
    807                 len = t.length;
    808                 for (i = 0, str = ""; i < len; i++) {
    809                     if (Type.exists(obj.style[t[i]])) {
    810                         str = obj.style[t[i]];
    811                         break;
    812                     }
    813                 }
    814             }
    815
    816             // Convert and reorder the matrix for JSXGraph
    817             if (str !== "") {
    818                 start = str.indexOf("(");
    819
    820                 if (start > 0) {
    821                     len = str.length;
    822                     arrstr = str.substring(start + 1, len - 1);
    823                     arr = arrstr.split(",");
    824
    825                     for (j = 0, len2 = arr.length; j < len2; j++) {
    826                         arr[j] = parseFloat(arr[j]);
    827                     }
    828
    829                     if (str.indexOf("matrix") === 0) {
    830                         mat = [
    831                             [1, 0, 0],
    832                             [0, arr[0], arr[1]],
    833                             [0, arr[2], arr[3]]
    834                         ];
    835                     } else if (str.indexOf("scaleX") === 0) {
    836                         mat[1][1] = arr[0];
    837                     } else if (str.indexOf("scaleY") === 0) {
    838                         mat[2][2] = arr[0];
    839                     } else if (str.indexOf("scale") === 0) {
    840                         mat[1][1] = arr[0];
    841                         mat[2][2] = arr[1];
    842                     }
    843                 }
    844             }
    845
    846             // CSS style zoom is used by reveal.js
    847             // Recursively search for zoom style entries.
    848             // This is necessary for reveal.js on webkit.
    849             // It fails if the user does zooming
    850             if (Type.exists(obj.style.zoom)) {
    851                 str = obj.style.zoom;
    852                 if (str !== "") {
    853                     mat[1][1] *= parseFloat(str);
    854                     mat[2][2] *= parseFloat(str);
    855                 }
    856             }
    857
    858             return mat;
    859         },
    860
    861         /**
    862          * Process data in timed chunks. Data which takes long to process, either because it is such
    863          * a huge amount of data or the processing takes some time, causes warnings in browsers about
    864          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
    865          * called chunks which will be processed in serial order.
    866          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
    867          * @param {Array} items to do
    868          * @param {Function} process Function that is applied for every array item
    869          * @param {Object} context The scope of function process
    870          * @param {Function} callback This function is called after the last array element has been processed.
    871          */
    872         timedChunk: function (items, process, context, callback) {
    873             //create a clone of the original
    874             var todo = items.concat(),
    875                 timerFun = function () {
    876                     var start = +new Date();
    877
    878                     do {
    879                         process.call(context, todo.shift());
    880                     } while (todo.length > 0 && +new Date() - start < 300);
    881
    882                     if (todo.length > 0) {
    883                         window.setTimeout(timerFun, 1);
    884                     } else {
    885                         callback(items);
    886                     }
    887                 };
    888
    889             window.setTimeout(timerFun, 1);
    890         },
    891
    892         /**
    893          * Scale and vertically shift a DOM element (usually a JSXGraph div)
    894          * inside of a parent DOM
    895          * element which is set to fullscreen.
    896          * This is realized with a CSS transformation.
    897          *
    898          * @param  {String} wrap_id  id of the parent DOM element which is in fullscreen mode
    899          * @param  {String} inner_id id of the DOM element which is scaled and shifted
    900          * @param  {Object} doc      document object or shadow root
    901          * @param  {Number} scale    Relative size of the JSXGraph board in the fullscreen window.
    902          *
    903          * @private
    904          * @see JXG.Board#toFullscreen
    905          * @see JXG.Board#fullscreenListener
    906          *
    907          */
    908         scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) {
    909             var w, h, b,
    910                 wi, hi,
    911                 wo, ho, inner,
    912                 scale_l, vshift_l,
    913                 f = scale,
    914                 ratio,
    915                 pseudo_keys = [
    916                     ":fullscreen",
    917                     ":-webkit-full-screen",
    918                     ":-moz-full-screen",
    919                     ":-ms-fullscreen"
    920                 ],
    921                 len_pseudo = pseudo_keys.length,
    922                 i;
    923
    924             b = doc.getElementById(wrap_id).getBoundingClientRect();
    925             h = b.height;
    926             w = b.width;
    927
    928             inner = doc.getElementById(inner_id);
    929             wo = inner._cssFullscreenStore.w;
    930             ho = inner._cssFullscreenStore.h;
    931             ratio = ho / wo;
    932
    933             // Scale the div such that fits into the fullscreen.
    934             if (wo > w * f) {
    935                 wo = w * f;
    936                 ho = wo * ratio;
    937             }
    938             if (ho > h * f) {
    939                 ho = h * f;
    940                 wo = ho / ratio;
    941             }
    942
    943             wi = wo;
    944             hi = ho;
    945             // Compare the code in this.setBoundingBox()
    946             if (ratio > 1) {
    947                 // h > w
    948                 if (ratio < h / w) {
    949                     scale_l =  w * f / wo;
    950                 } else {
    951                     scale_l =  h * f / ho;
    952                 }
    953             } else {
    954                 // h <= w
    955                 if (ratio < h / w) {
    956                     scale_l = w * f / wo;
    957                 } else {
    958                     scale_l = h * f / ho;
    959                 }
    960             }
    961             vshift_l = (h - hi) * 0.5;
    962
    963             // Set a CSS properties to center the JSXGraph div horizontally and vertically
    964             // at the first position of the fullscreen pseudo classes.
    965             for (i = 0; i < len_pseudo; i++) {
    966                 try {
    967                     inner.style.width = wi + 'px !important';
    968                     inner.style.height = hi + 'px !important';
    969                     inner.style.margin = '0 auto';
    970                     // Add the transform to a possibly already existing transform
    971                     inner.style.transform = inner._cssFullscreenStore.transform +
    972                         ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')';
    973                     break;
    974                 } catch (err) {
    975                     JXG.debug("JXG.scaleJSXGraphDiv:\n" + err);
    976                 }
    977             }
    978             if (i === len_pseudo) {
    979                 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property.");
    980             }
    981         }
    982
    983     }
    984 );
    985
    986 export default JXG;
    987