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