      1 /*
      2     Copyright 2008-2024
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Andreas Walter,
      8         Alfred Wassermann,
      9         Peter Wilfahrt
     10
     11     This file is part of JSXGraph.
     12
     13     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     14
     15     You can redistribute it and/or modify it under the terms of the
     16
     17       * GNU Lesser General Public License as published by
     18         the Free Software Foundation, either version 3 of the License, or
     19         (at your option) any later version
     20       OR
     21       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     22
     23     JSXGraph is distributed in the hope that it will be useful,
     24     but WITHOUT ANY WARRANTY; without even the implied warranty of
     25     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     26     GNU Lesser General Public License for more details.
     27
     28     You should have received a copy of the GNU Lesser General Public License and
     29     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
     30     and <https://opensource.org/licenses/MIT/>.
     31  */
     32
     33 /*global JXG: true, define: true, window: true, document: true, navigator: true, module: true, global: true, self: true, require: true*/
     34 /*jslint nomen: true, plusplus: true*/
     35
     36 /**
     37  * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish
     38  * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device
     39  * the browser runs on is a tablet/cell or a desktop computer.
     40  */
     41
     42 import JXG from "../jxg.js";
     43 import Type from "./type.js";
     44
     45 JXG.extendConstants(
     46     JXG,
     47     /** @lends JXG */ {
     48         // /**
     49         //  * Determines the property that stores the relevant information in the event object.
     50         //  * @type String
     51         //  * @default 'touches'
     52         //  * @private
     53         //  */
     54         // touchProperty: "touches"
     55     }
     56 );
     57
     58 JXG.extend(
     59     JXG,
     60     /** @lends JXG */ {
     61         /**
     62          * Determines whether evt is a touch event.
     63          * @param evt {Event}
     64          * @returns {Boolean}
     65          */
     66         isTouchEvent: function (evt) {
     67             return JXG.exists(evt['touches']); // Old iOS touch events
     68         },
     69
     70         /**
     71          * Determines whether evt is a pointer event.
     72          * @param evt {Event}
     73          * @returns {Boolean}
     74          */
     75         isPointerEvent: function (evt) {
     76             return JXG.exists(evt.pointerId);
     77         },
     78
     79         /**
     80          * Determines whether evt is neither a touch event nor a pointer event.
     81          * @param evt {Event}
     82          * @returns {Boolean}
     83          */
     84         isMouseEvent: function (evt) {
     85             return !JXG.isTouchEvent(evt) && !JXG.isPointerEvent(evt);
     86         },
     87
     88         /**
     89          * Determines the number of touch points in a touch event.
     90          * For other events, -1 is returned.
     91          * @param evt {Event}
     92          * @returns {Number}
     93          */
     94         getNumberOfTouchPoints: function (evt) {
     95             var n = -1;
     96
     97             if (JXG.isTouchEvent(evt)) {
     98                 n = evt['touches'].length;
     99             }
    100
    101             return n;
    102         },
    103
    104         /**
    105          * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event.
    106          * Attention: When two or more pointer device types are being used concurrently,
    107          *            it is only checked whether the passed event is the first one of its type!
    108          * @param evt {Event}
    109          * @returns {boolean}
    110          */
    111         isFirstTouch: function (evt) {
    112             var touchPoints = JXG.getNumberOfTouchPoints(evt);
    113
    114             if (JXG.isPointerEvent(evt)) {
    115                 return evt.isPrimary;
    116             }
    117
    118             return touchPoints === 1;
    119         },
    120
    121         /**
    122          * A document/window environment is available.
    123          * @type Boolean
    124          * @default false
    125          */
    126         isBrowser: typeof window === "object" && typeof document === "object",
    127
    128         /**
    129          * Features of ECMAScript 6+ are available.
    130          * @type Boolean
    131          * @default false
    132          */
    133         supportsES6: function () {
    134             // var testMap;
    135             /* jshint ignore:start */
    136             try {
    137                 // This would kill the old uglifyjs: testMap = (a = 0) => a;
    138                 new Function("(a = 0) => a");
    139                 return true;
    140             } catch (err) {
    141                 return false;
    142             }
    143             /* jshint ignore:end */
    144         },
    145
    146         /**
    147          * Detect browser support for VML.
    148          * @returns {Boolean} True, if the browser supports VML.
    149          */
    150         supportsVML: function () {
    151             // From stackoverflow.com
    152             return this.isBrowser && !!document.namespaces;
    153         },
    154
    155         /**
    156          * Detect browser support for SVG.
    157          * @returns {Boolean} True, if the browser supports SVG.
    158          */
    159         supportsSVG: function () {
    160             var svgSupport;
    161             if (!this.isBrowser) {
    162                 return false;
    163             }
    164             svgSupport = !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect;
    165             return svgSupport;
    166         },
    167
    168         /**
    169          * Detect browser support for Canvas.
    170          * @returns {Boolean} True, if the browser supports HTML canvas.
    171          */
    172         supportsCanvas: function () {
    173             var hasCanvas = false;
    174
    175             // if (this.isNode()) {
    176             //     try {
    177             //         // c = typeof module === "object" ? module.require("canvas") : $__canvas;
    178             //         c = typeof module === "object" ? module.require("canvas") : import('canvas');
    179             //         hasCanvas = !!c;
    180             //     } catch (err) {}
    181             // }
    182
    183             if (this.isNode()) {
    184                 //try {
    185                 //    JXG.createCanvas(500, 500);
    186                     hasCanvas = true;
    187                 // } catch (err) {
    188                 //     throw new Error('JXG.createCanvas not available.\n' +
    189                 //         'Install the npm package `canvas`\n' +
    190                 //         'and call:\n' +
    191                 //         '    import { createCanvas } from "canvas.js";\n' +
    192                 //         '    JXG.createCanvas = createCanvas;\n');
    193                 // }
    194             }
    195
    196             return (
    197                 hasCanvas || (this.isBrowser && !!document.createElement("canvas").getContext)
    198             );
    199         },
    200
    201         /**
    202          * True, if run inside a node.js environment.
    203          * @returns {Boolean}
    204          */
    205         isNode: function () {
    206             // This is not a 100% sure but should be valid in most cases
    207             // We are not inside a browser
    208             /* eslint-disable no-undef */
    209             return (
    210                 !this.isBrowser &&
    211                 (typeof process !== 'undefined') &&
    212                 (process.release.name.search(/node|io.js/) !== -1)
    213             /* eslint-enable no-undef */
    214
    215                 // there is a module object (plain node, no requirejs)
    216                 // ((typeof module === "object" && !!module.exports) ||
    217                 //     // there is a global object and requirejs is loaded
    218                 //     (typeof global === "object" &&
    219                 //         global.requirejsVars &&
    220                 //         !global.requirejsVars.isBrowser)
    221                 // )
    222             );
    223         },
    224
    225         /**
    226          * True if run inside a webworker environment.
    227          * @returns {Boolean}
    228          */
    229         isWebWorker: function () {
    230             return (
    231                 !this.isBrowser &&
    232                 typeof self === "object" &&
    233                 typeof self.postMessage === "function"
    234             );
    235         },
    236
    237         /**
    238          * Checks if the environments supports the W3C Pointer Events API {@link https://www.w3.org/TR/pointerevents/}
    239          * @returns {Boolean}
    240          */
    241         supportsPointerEvents: function () {
    242             return !!(
    243                 (
    244                     this.isBrowser &&
    245                     window.navigator &&
    246                     (window.PointerEvent || // Chrome/Edge/IE11+
    247                         window.navigator.pointerEnabled || // IE11+
    248                         window.navigator.msPointerEnabled)
    249                 ) // IE10-
    250             );
    251         },
    252
    253         /**
    254          * Determine if the current browser supports touch events
    255          * @returns {Boolean} True, if the browser supports touch events.
    256          */
    257         isTouchDevice: function () {
    258             return this.isBrowser && window.ontouchstart !== undefined;
    259         },
    260
    261         /**
    262          * Detects if the user is using an Android powered device.
    263          * @returns {Boolean}
    264          * @deprecated
    265          */
    266         isAndroid: function () {
    267             return (
    268                 Type.exists(navigator) &&
    269                 navigator.userAgent.toLowerCase().indexOf("android") > -1
    270             );
    271         },
    272
    273         /**
    274          * Detects if the user is using the default Webkit browser on an Android powered device.
    275          * @returns {Boolean}
    276          * @deprecated
    277          */
    278         isWebkitAndroid: function () {
    279             return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1;
    280         },
    281
    282         /**
    283          * Detects if the user is using a Apple iPad / iPhone.
    284          * @returns {Boolean}
    285          * @deprecated
    286          */
    287         isApple: function () {
    288             return (
    289                 Type.exists(navigator) &&
    290                 (navigator.userAgent.indexOf("iPad") > -1 ||
    291                     navigator.userAgent.indexOf("iPhone") > -1)
    292             );
    293         },
    294
    295         /**
    296          * Detects if the user is using Safari on an Apple device.
    297          * @returns {Boolean}
    298          * @deprecated
    299          */
    300         isWebkitApple: function () {
    301             return (
    302                 this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1
    303             );
    304         },
    305
    306         /**
    307          * Returns true if the run inside a Windows 8 "Metro" App.
    308          * @returns {Boolean}
    309          * @deprecated
    310          */
    311         isMetroApp: function () {
    312             return (
    313                 typeof window === "object" &&
    314                 window.clientInformation &&
    315                 window.clientInformation.appVersion &&
    316                 window.clientInformation.appVersion.indexOf("MSAppHost") > -1
    317             );
    318         },
    319
    320         /**
    321          * Detects if the user is using a Mozilla browser
    322          * @returns {Boolean}
    323          * @deprecated
    324          */
    325         isMozilla: function () {
    326             return (
    327                 Type.exists(navigator) &&
    328                 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1 &&
    329                 navigator.userAgent.toLowerCase().indexOf("apple") === -1
    330             );
    331         },
    332
    333         /**
    334          * Detects if the user is using a firefoxOS powered device.
    335          * @returns {Boolean}
    336          * @deprecated
    337          */
    338         isFirefoxOS: function () {
    339             return (
    340                 Type.exists(navigator) &&
    341                 navigator.userAgent.toLowerCase().indexOf("android") === -1 &&
    342                 navigator.userAgent.toLowerCase().indexOf("apple") === -1 &&
    343                 navigator.userAgent.toLowerCase().indexOf("mobile") > -1 &&
    344                 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1
    345             );
    346         },
    347
    348         /**
    349          * Detects if the user is using a desktop device.
    350          * @returns {boolean}
    351          *
    352          * @see https://stackoverflow.com/a/61073480
    353          * @deprecated
    354          */
    355         isDesktop: function () {
    356             return true;
    357             // console.log("isDesktop", screen.orientation);
    358             // const navigatorAgent =
    359             //     navigator.userAgent || navigator.vendor || window.opera;
    360             // return !(
    361             //     /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
    362             //         navigatorAgent
    363             //     ) ||
    364             //     /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
    365             //         navigatorAgent.substr(0, 4)
    366             //     )
    367             // );
    368         },
    369
    370         /**
    371          * Detects if the user is using a mobile device.
    372          * @returns {boolean}
    373          *
    374          * @see https://stackoverflow.com/questions/25542814/html5-detecting-if-youre-on-mobile-or-pc-with-javascript
    375          * @deprecated
    376          *
    377          */
    378         isMobile: function () {
    379             return true;
    380             // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    381         },
    382
    383         /**
    384          * Internet Explorer version. Works only for IE > 4.
    385          * @type Number
    386          * @deprecated
    387          */
    388         ieVersion: (function () {
    389             var div,
    390                 all,
    391                 v = 3;
    392
    393             if (typeof document !== "object") {
    394                 return 0;
    395             }
    396
    397             div = document.createElement("div");
    398             all = div.getElementsByTagName("i");
    399
    400             do {
    401                 div.innerHTML = "<!--[if gt IE " + ++v + "]><" + "i><" + "/i><![endif]-->";
    402             } while (all[0]);
    403
    404             return v > 4 ? v : undefined;
    405         })(),
    406
    407         /**
    408          * Reads the width and height of an HTML element.
    409          * @param {String|Object} elementId id of or reference to an HTML DOM node.
    410          * @returns {Object} An object with the two properties width and height.
    411          */
    412         getDimensions: function (elementId, doc) {
    413             var element,
    414                 display,
    415                 els,
    416                 originalVisibility,
    417                 originalPosition,
    418                 originalDisplay,
    419                 originalWidth,
    420                 originalHeight,
    421                 style,
    422                 pixelDimRegExp = /\d+(\.\d*)?px/;
    423
    424             if (!this.isBrowser || elementId === null) {
    425                 return {
    426                     width: 500,
    427                     height: 500
    428                 };
    429             }
    430
    431             doc = doc || document;
    432             // Borrowed from prototype.js
    433             element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId;
    434             if (!Type.exists(element)) {
    435                 throw new Error(
    436                     "\nJSXGraph: HTML container element '" + elementId + "' not found."
    437                 );
    438             }
    439
    440             display = element.style.display;
    441
    442             // Work around a bug in Safari
    443             if (display !== "none" && display !== null) {
    444                 if (element.clientWidth > 0 && element.clientHeight > 0) {
    445                     return { width: element.clientWidth, height: element.clientHeight };
    446                 }
    447
    448                 // A parent might be set to display:none; try reading them from styles
    449                 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style;
    450                 return {
    451                     width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0,
    452                     height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0
    453                 };
    454             }
    455
    456             // All *Width and *Height properties give 0 on elements with display set to none,
    457             // hence we show the element temporarily
    458             els = element.style;
    459
    460             // save style
    461             originalVisibility = els.visibility;
    462             originalPosition = els.position;
    463             originalDisplay = els.display;
    464
    465             // show element
    466             els.visibility = "hidden";
    467             els.position = "absolute";
    468             els.display = "block";
    469
    470             // read the dimension
    471             originalWidth = element.clientWidth;
    472             originalHeight = element.clientHeight;
    473
    474             // restore original css values
    475             els.display = originalDisplay;
    476             els.position = originalPosition;
    477             els.visibility = originalVisibility;
    478
    479             return {
    480                 width: originalWidth,
    481                 height: originalHeight
    482             };
    483         },
    484
    485         /**
    486          * Adds an event listener to a DOM element.
    487          * @param {Object} obj Reference to a DOM node.
    488          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    489          * @param {Function} fn The function to call when the event is triggered.
    490          * @param {Object} owner The scope in which the event trigger is called.
    491          * @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
    492          * an options object or the useCapture Boolean.
    493          *
    494          */
    495         addEvent: function (obj, type, fn, owner, options) {
    496             var el = function () {
    497                 return fn.apply(owner, arguments);
    498             };
    499
    500             el.origin = fn;
    501             // Check if owner is a board
    502             if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) {
    503                 owner['x_internal' + type] = owner['x_internal' + type] || [];
    504                 owner['x_internal' + type].push(el);
    505             }
    506
    507             // Non-IE browser
    508             if (Type.exists(obj) && Type.exists(obj.addEventListener)) {
    509                 options = options || false;  // options or useCapture
    510                 obj.addEventListener(type, el, options);
    511             }
    512
    513             // IE
    514             if (Type.exists(obj) && Type.exists(obj.attachEvent)) {
    515                 obj.attachEvent("on" + type, el);
    516             }
    517         },
    518
    519         /**
    520          * Removes an event listener from a DOM element.
    521          * @param {Object} obj Reference to a DOM node.
    522          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    523          * @param {Function} fn The function to call when the event is triggered.
    524          * @param {Object} owner The scope in which the event trigger is called.
    525          */
    526         removeEvent: function (obj, type, fn, owner) {
    527             var i;
    528
    529             if (!Type.exists(owner)) {
    530                 JXG.debug("no such owner");
    531                 return;
    532             }
    533
    534             if (!Type.exists(owner["x_internal" + type])) {
    535                 JXG.debug("removeEvent: no such type: " + type);
    536                 return;
    537             }
    538
    539             if (!Type.isArray(owner["x_internal" + type])) {
    540                 JXG.debug("owner[x_internal + " + type + "] is not an array");
    541                 return;
    542             }
    543
    544             i = Type.indexOf(owner["x_internal" + type], fn, "origin");
    545
    546             if (i === -1) {
    547                 JXG.debug("removeEvent: no such event function in internal list: " + fn);
    548                 return;
    549             }
    550
    551             try {
    552                 // Non-IE browser
    553                 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) {
    554                     obj.removeEventListener(type, owner["x_internal" + type][i], false);
    555                 }
    556
    557                 // IE
    558                 if (Type.exists(obj) && Type.exists(obj.detachEvent)) {
    559                     obj.detachEvent("on" + type, owner["x_internal" + type][i]);
    560                 }
    561             } catch (e) {
    562                 JXG.debug("removeEvent: event not registered in browser: (" + type + " -- " + fn + ")");
    563             }
    564
    565             owner["x_internal" + type].splice(i, 1);
    566         },
    567
    568         /**
    569          * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div
    570          * of a {@link JXG.Board} because this might corrupt the event handling system.
    571          * @param {Object} obj Reference to a DOM node.
    572          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    573          * @param {Object} owner The scope in which the event trigger is called.
    574          */
    575         removeAllEvents: function (obj, type, owner) {
    576             var i, len;
    577             if (owner["x_internal" + type]) {
    578                 len = owner["x_internal" + type].length;
    579
    580                 for (i = len - 1; i >= 0; i--) {
    581                     JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner);
    582                 }
    583
    584                 if (owner["x_internal" + type].length > 0) {
    585                     JXG.debug("removeAllEvents: Not all events could be removed.");
    586                 }
    587             }
    588         },
    589
    590         /**
    591          * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner.
    592          * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent.
    593          *
    594          * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used.
    595          * @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.
    596          * @param {Object} [doc] The document object.
    597          * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component.
    598          */
    599         getPosition: function (e, index, doc) {
    600             var i,
    601                 len,
    602                 evtTouches,
    603                 posx = 0,
    604                 posy = 0;
    605
    606             if (!e) {
    607                 e = window.event;
    608             }
    609
    610             doc = doc || document;
    611             evtTouches = e['touches']; // iOS touch events
    612
    613             // touchend events have their position in "changedTouches"
    614             if (Type.exists(evtTouches) && evtTouches.length === 0) {
    615                 evtTouches = e.changedTouches;
    616             }
    617
    618             if (Type.exists(index) && Type.exists(evtTouches)) {
    619                 if (index === -1) {
    620                     len = evtTouches.length;
    621
    622                     for (i = 0; i < len; i++) {
    623                         if (evtTouches[i]) {
    624                             e = evtTouches[i];
    625                             break;
    626                         }
    627                     }
    628                 } else {
    629                     e = evtTouches[index];
    630                 }
    631             }
    632
    633             // Scrolling is ignored.
    634             // e.clientX is supported since IE6
    635             if (e.clientX) {
    636                 posx = e.clientX;
    637                 posy = e.clientY;
    638             }
    639
    640             return [posx, posy];
    641         },
    642
    643         /**
    644          * Calculates recursively the offset of the DOM element in which the board is stored.
    645          * @param {Object} obj A DOM element
    646          * @returns {Array} An array with the elements left and top offset.
    647          */
    648         getOffset: function (obj) {
    649             var cPos,
    650                 o = obj,
    651                 o2 = obj,
    652                 l = o.offsetLeft - o.scrollLeft,
    653                 t = o.offsetTop - o.scrollTop;
    654
    655             cPos = this.getCSSTransform([l, t], o);
    656             l = cPos[0];
    657             t = cPos[1];
    658
    659             /*
    660              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
    661              * if not to the body. In IE and if we are in an position:absolute environment
    662              * offsetParent walks up the DOM hierarchy.
    663              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
    664              * we need the parentNode steps.
    665              */
    666             o = o.offsetParent;
    667             while (o) {
    668                 l += o.offsetLeft;
    669                 t += o.offsetTop;
    670
    671                 if (o.offsetParent) {
    672                     l += o.clientLeft - o.scrollLeft;
    673                     t += o.clientTop - o.scrollTop;
    674                 }
    675
    676                 cPos = this.getCSSTransform([l, t], o);
    677                 l = cPos[0];
    678                 t = cPos[1];
    679
    680                 o2 = o2.parentNode;
    681
    682                 while (o2 !== o) {
    683                     l += o2.clientLeft - o2.scrollLeft;
    684                     t += o2.clientTop - o2.scrollTop;
    685
    686                     cPos = this.getCSSTransform([l, t], o2);
    687                     l = cPos[0];
    688                     t = cPos[1];
    689
    690                     o2 = o2.parentNode;
    691                 }
    692                 o = o.offsetParent;
    693             }
    694
    695             return [l, t];
    696         },
    697
    698         /**
    699          * Access CSS style sheets.
    700          * @param {Object} obj A DOM element
    701          * @param {String} stylename The CSS property to read.
    702          * @returns The value of the CSS property and <tt>undefined</tt> if it is not set.
    703          */
    704         getStyle: function (obj, stylename) {
    705             var r,
    706                 doc = obj.ownerDocument;
    707
    708             // Non-IE
    709             if (doc.defaultView && doc.defaultView.getComputedStyle) {
    710                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
    711                 // IE
    712             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
    713                 r = obj.currentStyle[stylename];
    714             } else {
    715                 if (obj.style) {
    716                     // make stylename lower camelcase
    717                     stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) {
    718                         return letter.toUpperCase();
    719                     });
    720                     r = obj.style[stylename];
    721                 }
    722             }
    723
    724             return r;
    725         },
    726
    727         /**
    728          * Reads css style sheets of a given element. This method is a getStyle wrapper and
    729          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
    730          * @param {DOMElement} el
    731          * @param {string} css
    732          * @returns {number}
    733          */
    734         getProp: function (el, css) {
    735             var n = parseInt(this.getStyle(el, css), 10);
    736             return isNaN(n) ? 0 : n;
    737         },
    738
    739         /**
    740          * Correct position of upper left corner in case of
    741          * a CSS transformation. Here, only translations are
    742          * extracted. All scaling transformations are corrected
    743          * in {@link JXG.Board#getMousePosition}.
    744          * @param {Array} cPos Previously determined position
    745          * @param {Object} obj A DOM element
    746          * @returns {Array} The corrected position.
    747          */
    748         getCSSTransform: function (cPos, obj) {
    749             var i,
    750                 j,
    751                 str,
    752                 arrStr,
    753                 start,
    754                 len,
    755                 len2,
    756                 arr,
    757                 t = [
    758                     "transform",
    759                     "webkitTransform",
    760                     "MozTransform",
    761                     "msTransform",
    762                     "oTransform"
    763                 ];
    764
    765             // Take the first transformation matrix
    766             len = t.length;
    767
    768             for (i = 0, str = ""; i < len; i++) {
    769                 if (Type.exists(obj.style[t[i]])) {
    770                     str = obj.style[t[i]];
    771                     break;
    772                 }
    773             }
    774
    775             /**
    776              * Extract the coordinates and apply the transformation
    777              * to cPos
    778              */
    779             if (str !== "") {
    780                 start = str.indexOf("(");
    781
    782                 if (start > 0) {
    783                     len = str.length;
    784                     arrStr = str.substring(start + 1, len - 1);
    785                     arr = arrStr.split(",");
    786
    787                     for (j = 0, len2 = arr.length; j < len2; j++) {
    788                         arr[j] = parseFloat(arr[j]);
    789                     }
    790
    791                     if (str.indexOf("matrix") === 0) {
    792                         cPos[0] += arr[4];
    793                         cPos[1] += arr[5];
    794                     } else if (str.indexOf("translateX") === 0) {
    795                         cPos[0] += arr[0];
    796                     } else if (str.indexOf("translateY") === 0) {
    797                         cPos[1] += arr[0];
    798                     } else if (str.indexOf("translate") === 0) {
    799                         cPos[0] += arr[0];
    800                         cPos[1] += arr[1];
    801                     }
    802                 }
    803             }
    804
    805             // Zoom is used by reveal.js
    806             if (Type.exists(obj.style.zoom)) {
    807                 str = obj.style.zoom;
    808                 if (str !== "") {
    809                     cPos[0] *= parseFloat(str);
    810                     cPos[1] *= parseFloat(str);
    811                 }
    812             }
    813
    814             return cPos;
    815         },
    816
    817         /**
    818          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
    819          * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported.
    820          * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}.
    821          */
    822         getCSSTransformMatrix: function (obj) {
    823             var i, j, str, arrstr, arr,
    824                 start, len, len2, st,
    825                 doc = obj.ownerDocument,
    826                 t = [
    827                     "transform",
    828                     "webkitTransform",
    829                     "MozTransform",
    830                     "msTransform",
    831                     "oTransform"
    832                 ],
    833                 mat = [
    834                     [1, 0, 0],
    835                     [0, 1, 0],
    836                     [0, 0, 1]
    837                 ];
    838
    839             // This should work on all browsers except IE 6-8
    840             if (doc.defaultView && doc.defaultView.getComputedStyle) {
    841                 st = doc.defaultView.getComputedStyle(obj, null);
    842                 str =
    843                     st.getPropertyValue("-webkit-transform") ||
    844                     st.getPropertyValue("-moz-transform") ||
    845                     st.getPropertyValue("-ms-transform") ||
    846                     st.getPropertyValue("-o-transform") ||
    847                     st.getPropertyValue("transform");
    848             } else {
    849                 // Take the first transformation matrix
    850                 len = t.length;
    851                 for (i = 0, str = ""; i < len; i++) {
    852                     if (Type.exists(obj.style[t[i]])) {
    853                         str = obj.style[t[i]];
    854                         break;
    855                     }
    856                 }
    857             }
    858
    859             // Convert and reorder the matrix for JSXGraph
    860             if (str !== "") {
    861                 start = str.indexOf("(");
    862
    863                 if (start > 0) {
    864                     len = str.length;
    865                     arrstr = str.substring(start + 1, len - 1);
    866                     arr = arrstr.split(",");
    867
    868                     for (j = 0, len2 = arr.length; j < len2; j++) {
    869                         arr[j] = parseFloat(arr[j]);
    870                     }
    871
    872                     if (str.indexOf("matrix") === 0) {
    873                         mat = [
    874                             [1, 0, 0],
    875                             [0, arr[0], arr[1]],
    876                             [0, arr[2], arr[3]]
    877                         ];
    878                     } else if (str.indexOf("scaleX") === 0) {
    879                         mat[1][1] = arr[0];
    880                     } else if (str.indexOf("scaleY") === 0) {
    881                         mat[2][2] = arr[0];
    882                     } else if (str.indexOf("scale") === 0) {
    883                         mat[1][1] = arr[0];
    884                         mat[2][2] = arr[1];
    885                     }
    886                 }
    887             }
    888
    889             // CSS style zoom is used by reveal.js
    890             // Recursively search for zoom style entries.
    891             // This is necessary for reveal.js on webkit.
    892             // It fails if the user does zooming
    893             if (Type.exists(obj.style.zoom)) {
    894                 str = obj.style.zoom;
    895                 if (str !== "") {
    896                     mat[1][1] *= parseFloat(str);
    897                     mat[2][2] *= parseFloat(str);
    898                 }
    899             }
    900
    901             return mat;
    902         },
    903
    904         /**
    905          * Process data in timed chunks. Data which takes long to process, either because it is such
    906          * a huge amount of data or the processing takes some time, causes warnings in browsers about
    907          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
    908          * called chunks which will be processed in serial order.
    909          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
    910          * @param {Array} items to do
    911          * @param {Function} process Function that is applied for every array item
    912          * @param {Object} context The scope of function process
    913          * @param {Function} callback This function is called after the last array element has been processed.
    914          */
    915         timedChunk: function (items, process, context, callback) {
    916             //create a clone of the original
    917             var todo = items.slice(),
    918                 timerFun = function () {
    919                     var start = +new Date();
    920
    921                     do {
    922                         process.call(context, todo.shift());
    923                     } while (todo.length > 0 && +new Date() - start < 300);
    924
    925                     if (todo.length > 0) {
    926                         window.setTimeout(timerFun, 1);
    927                     } else {
    928                         callback(items);
    929                     }
    930                 };
    931
    932             window.setTimeout(timerFun, 1);
    933         },
    934
    935         /**
    936          * Scale and vertically shift a DOM element (usually a JSXGraph div)
    937          * inside of a parent DOM
    938          * element which is set to fullscreen.
    939          * This is realized with a CSS transformation.
    940          *
    941          * @param  {String} wrap_id  id of the parent DOM element which is in fullscreen mode
    942          * @param  {String} inner_id id of the DOM element which is scaled and shifted
    943          * @param  {Object} doc      document object or shadow root
    944          * @param  {Number} scale    Relative size of the JSXGraph board in the fullscreen window.
    945          *
    946          * @private
    947          * @see JXG.Board#toFullscreen
    948          * @see JXG.Board#fullscreenListener
    949          *
    950          */
    951         scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) {
    952             var w, h, b,
    953                 wi, hi,
    954                 wo, ho, inner,
    955                 scale_l, vshift_l,
    956                 f = scale,
    957                 ratio,
    958                 pseudo_keys = [
    959                     ":fullscreen",
    960                     ":-webkit-full-screen",
    961                     ":-moz-full-screen",
    962                     ":-ms-fullscreen"
    963                 ],
    964                 len_pseudo = pseudo_keys.length,
    965                 i;
    966
    967             b = doc.getElementById(wrap_id).getBoundingClientRect();
    968             h = b.height;
    969             w = b.width;
    970
    971             inner = doc.getElementById(inner_id);
    972             wo = inner._cssFullscreenStore.w;
    973             ho = inner._cssFullscreenStore.h;
    974             ratio = ho / wo;
    975
    976             // Scale the div such that fits into the fullscreen.
    977             if (wo > w * f) {
    978                 wo = w * f;
    979                 ho = wo * ratio;
    980             }
    981             if (ho > h * f) {
    982                 ho = h * f;
    983                 wo = ho / ratio;
    984             }
    985
    986             wi = wo;
    987             hi = ho;
    988             // Compare the code in this.setBoundingBox()
    989             if (ratio > 1) {
    990                 // h > w
    991                 if (ratio < h / w) {
    992                     scale_l =  w * f / wo;
    993                 } else {
    994                     scale_l =  h * f / ho;
    995                 }
    996             } else {
    997                 // h <= w
    998                 if (ratio < h / w) {
    999                     scale_l = w * f / wo;
    1000                 } else {
    1001                     scale_l = h * f / ho;
    1002                 }
    1003             }
    1004             vshift_l = (h - hi) * 0.5;
    1005
    1006             // Set a CSS properties to center the JSXGraph div horizontally and vertically
    1007             // at the first position of the fullscreen pseudo classes.
    1008             for (i = 0; i < len_pseudo; i++) {
    1009                 try {
    1010                     inner.style.width = wi + 'px !important';
    1011                     inner.style.height = hi + 'px !important';
    1012                     inner.style.margin = '0 auto';
    1013                     // Add the transform to a possibly already existing transform
    1014                     inner.style.transform = inner._cssFullscreenStore.transform +
    1015                         ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')';
    1016                     break;
    1017                 } catch (err) {
    1018                     JXG.debug("JXG.scaleJSXGraphDiv:\n" + err);
    1019                 }
    1020             }
    1021             if (i === len_pseudo) {
    1022                 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property.");
    1023             }
    1024         }
    1025
    1026     }
    1027 );
    1028
    1029 export default JXG;
    1030