1 /*
  2     Copyright 2008-2026
  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         /**
 63          * Upper bound on pixel coordinates. This is used in svg and canvas renderer to avoid limitations on numbers there.
 64          * 2026: can be enlarged to 2**24-1, the largest integer valiue that browsers do support.
 65          * Browser implementations support 32bit floating point values.
 66          * <p>
 67          *
 68          * @private
 69          */
 70         maxScreenCoord: 16777215, // = (2**24 - 1),
 71         // Too large: 2147483647 = Math.pow(2, 31) - 1,
 72         // 5000,
 73 
 74         /**
 75          * Determines whether evt is a touch event.
 76          * @param evt {Event}
 77          * @returns {Boolean}
 78          */
 79         isTouchEvent: function (evt) {
 80             return JXG.exists(evt['touches']); // Old iOS touch events
 81         },
 82 
 83         /**
 84          * Determines whether evt is a pointer event.
 85          * @param evt {Event}
 86          * @returns {Boolean}
 87          */
 88         isPointerEvent: function (evt) {
 89             return JXG.exists(evt.pointerId);
 90         },
 91 
 92         /**
 93          * Determines whether evt is neither a touch event nor a pointer event.
 94          * @param evt {Event}
 95          * @returns {Boolean}
 96          */
 97         isMouseEvent: function (evt) {
 98             return !JXG.isTouchEvent(evt) && !JXG.isPointerEvent(evt);
 99         },
100 
101         /**
102          * Determines the number of touch points in a touch event.
103          * For other events, -1 is returned.
104          * @param evt {Event}
105          * @returns {Number}
106          */
107         getNumberOfTouchPoints: function (evt) {
108             var n = -1;
109 
110             if (JXG.isTouchEvent(evt)) {
111                 n = evt['touches'].length;
112             }
113 
114             return n;
115         },
116 
117         /**
118          * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event.
119          * Attention: When two or more pointer device types are being used concurrently,
120          *            it is only checked whether the passed event is the first one of its type!
121          * @param evt {Event}
122          * @returns {boolean}
123          */
124         isFirstTouch: function (evt) {
125             var touchPoints = JXG.getNumberOfTouchPoints(evt);
126 
127             if (JXG.isPointerEvent(evt)) {
128                 return evt.isPrimary;
129             }
130 
131             return touchPoints === 1;
132         },
133 
134         /**
135          * A document/window environment is available.
136          * @type Boolean
137          * @default false
138          */
139         // isBrowser: Type.exists(window) && Type.exists(document) &&
140         //     typeof window === "object" && typeof document === "object",
141         isBrowser: typeof window !== 'undefined' && typeof document !== 'undefined' &&
142             typeof window === "object" && typeof document === "object",
143 
144         /**
145          * Features of ECMAScript 6+ are available.
146          * @type Boolean
147          * @default false
148          */
149         supportsES6: function () {
150             // var testMap;
151             /* jshint ignore:start */
152             try {
153                 // This would kill the old uglifyjs: testMap = (a = 0) => a;
154                 new Function("(a = 0) => a");
155                 return true;
156             } catch (err) {
157                 return false;
158             }
159             /* jshint ignore:end */
160         },
161 
162         /**
163          * Detect browser support for VML.
164          * @returns {Boolean} True, if the browser supports VML.
165          */
166         supportsVML: function () {
167             // From stackoverflow.com
168             return this.isBrowser && !!document.namespaces;
169         },
170 
171         /**
172          * Detect browser support for SVG.
173          * @returns {Boolean} True, if the browser supports SVG.
174          */
175         supportsSVG: function () {
176             var svgSupport;
177             if (!this.isBrowser) {
178                 return false;
179             }
180             svgSupport = !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect;
181             return svgSupport;
182         },
183 
184         /**
185          * Detect browser support for Canvas.
186          * @returns {Boolean} True, if the browser supports HTML canvas.
187          */
188         supportsCanvas: function () {
189             var hasCanvas = false;
190 
191             // if (this.isNode()) {
192             //     try {
193             //         // c = typeof module === "object" ? module.require('canvas') : $__canvas;
194             //         c = typeof module === "object" ? module.require('canvas') : import('canvas');
195             //         hasCanvas = !!c;
196             //     } catch (err) {}
197             // }
198 
199             if (this.isNode()) {
200                 //try {
201                 //    JXG.createCanvas(500, 500);
202                     hasCanvas = true;
203                 // } catch (err) {
204                 //     throw new Error('JXG.createCanvas not available.\n' +
205                 //         'Install the npm package `canvas`\n' +
206                 //         'and call:\n' +
207                 //         '    import { createCanvas } from 'canvas.js'\n' +
208                 //         '    JXG.createCanvas = createCanvas;\n');
209                 // }
210             }
211 
212             return (
213                 hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext)
214             );
215         },
216 
217         /**
218          * True, if run inside a node.js environment.
219          * @returns {Boolean}
220          */
221         isNode: function () {
222             // This is not a 100% sure but should be valid in most cases
223             // We are not inside a browser
224             /* eslint-disable no-undef */
225             return (
226                 !this.isBrowser &&
227                 (typeof process !== 'undefined') &&
228                 (process.release.name.search(/node|io.js/) !== -1)
229             /* eslint-enable no-undef */
230 
231                 // there is a module object (plain node, no requirejs)
232                 // ((typeof module === "object" && !!module.exports) ||
233                 //     // there is a global object and requirejs is loaded
234                 //     (typeof global === "object" &&
235                 //         global.requirejsVars &&
236                 //         !global.requirejsVars.isBrowser)
237                 // )
238             );
239         },
240 
241         /**
242          * True if run inside a webworker environment.
243          * @returns {Boolean}
244          */
245         isWebWorker: function () {
246             return (
247                 !this.isBrowser &&
248                 typeof self === "object" &&
249                 typeof self.postMessage === "function"
250             );
251         },
252 
253         /**
254          * Checks if the environments supports the W3C Pointer Events API {@link https://www.w3.org/TR/pointerevents/}
255          * @returns {Boolean}
256          */
257         supportsPointerEvents: function () {
258             return !!(
259                 (
260                     this.isBrowser &&
261                     window.navigator &&
262                     (window.PointerEvent || // Chrome/Edge/IE11+
263                         window.navigator.pointerEnabled || // IE11+
264                         window.navigator.msPointerEnabled)
265                 ) // IE10-
266             );
267         },
268 
269         /**
270          * Determine if the current browser supports touch events
271          * @returns {Boolean} True, if the browser supports touch events.
272          */
273         isTouchDevice: function () {
274             return this.isBrowser && window.ontouchstart !== undefined;
275         },
276 
277         /**
278          * Detects if the user is using an Android powered device.
279          * @returns {Boolean}
280          * @deprecated
281          */
282         isAndroid: function () {
283             return (
284                 Type.exists(navigator) &&
285                 navigator.userAgent.toLowerCase().indexOf('android') > -1
286             );
287         },
288 
289         /**
290          * Detects if the user is using the default Webkit browser on an Android powered device.
291          * @returns {Boolean}
292          * @deprecated
293          */
294         isWebkitAndroid: function () {
295             return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1;
296         },
297 
298         /**
299          * Detects if the user is using a Apple iPad / iPhone.
300          * @returns {Boolean}
301          * @deprecated
302          */
303         isApple: function () {
304             return (
305                 Type.exists(navigator) &&
306                 (navigator.userAgent.indexOf('iPad') > -1 ||
307                     navigator.userAgent.indexOf('iPhone') > -1)
308             );
309         },
310 
311         /**
312          * Detects if the user is using Safari on an Apple device.
313          * See https://evilmartians.com/chronicles/how-to-detect-safari-and-ios-versions-with-ease (2025)
314          * @returns {Boolean}
315          * @deprecated
316          */
317         isWebkitApple: function () {
318             var is = ('GestureEvent' in window) && // Desktop and mobile
319                     (
320                         ('ongesturechange' in window) || // mobile webkit browsers and webview iOS
321                         (window !== undefined && // Desktop Safari
322                          'safari' in window &&
323                          'pushNotification' in window.safari)
324                     );
325             return is;
326 
327             // return (
328             //     this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1
329             // );
330         },
331 
332         /**
333          * Returns true if the run inside a Windows 8 "Metro" App.
334          * @returns {Boolean}
335          * @deprecated
336          */
337         isMetroApp: function () {
338             return (
339                 typeof window === "object" &&
340                 window.clientInformation &&
341                 window.clientInformation.appVersion &&
342                 window.clientInformation.appVersion.indexOf('MSAppHost') > -1
343             );
344         },
345 
346         /**
347          * Detects if the user is using a Mozilla browser
348          * @returns {Boolean}
349          * @deprecated
350          */
351         isMozilla: function () {
352             return (
353                 Type.exists(navigator) &&
354                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 &&
355                 navigator.userAgent.toLowerCase().indexOf('apple') === -1
356             );
357         },
358 
359         /**
360          * Detects if the user is using a firefoxOS powered device.
361          * @returns {Boolean}
362          * @deprecated
363          */
364         isFirefoxOS: function () {
365             return (
366                 Type.exists(navigator) &&
367                 navigator.userAgent.toLowerCase().indexOf('android') === -1 &&
368                 navigator.userAgent.toLowerCase().indexOf('apple') === -1 &&
369                 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 &&
370                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1
371             );
372         },
373 
374         /**
375          * Detects if the user is using a desktop device, see <a href="https://stackoverflow.com/a/61073480">https://stackoverflow.com/a/61073480</a>.
376          * @returns {boolean}
377          *
378          * @deprecated
379          */
380         isDesktop: function () {
381             return true;
382             // console.log("isDesktop", screen.orientation);
383             // const navigatorAgent =
384             //     navigator.userAgent || navigator.vendor || window.opera;
385             // return !(
386             //     /(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(
387             //         navigatorAgent
388             //     ) ||
389             //     /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(
390             //         navigatorAgent.substr(0, 4)
391             //     )
392             // );
393         },
394 
395         /**
396          * 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>.
397          * @returns {boolean}
398          *
399          * @deprecated
400          *
401          */
402         isMobile: function () {
403             return true;
404             // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
405         },
406 
407         /**
408          * Internet Explorer version. Works only for IE > 4.
409          * @type Number
410          * @deprecated
411          */
412         ieVersion: (function () {
413             var div,
414                 all,
415                 v = 3;
416 
417             if (typeof document === 'undefined' || document === null || typeof document !== 'object') {
418                 return 0;
419             }
420 
421             div = document.createElement('div');
422             all = div.getElementsByTagName('i');
423 
424             do {
425                 div.innerHTML = "<!--[if gt IE " + (++v) + "]><" + "i><" + "/i><![endif]-->";
426             } while (all[0]);
427 
428             return v > 4 ? v : undefined;
429         })(),
430 
431         /**
432          * Reads the width and height of an HTML element.
433          * @param {String|Object} elementId id of or reference to an HTML DOM node.
434          * @returns {Object} An object with the two properties width and height.
435          */
436         getDimensions: function (elementId, doc) {
437             var element,
438                 display,
439                 els,
440                 originalVisibility,
441                 originalPosition,
442                 originalDisplay,
443                 originalWidth,
444                 originalHeight,
445                 style,
446                 pixelDimRegExp = /\d+(\.\d*)?px/;
447 
448             if (!this.isBrowser || elementId === null) {
449                 return {
450                     width: 500,
451                     height: 500
452                 };
453             }
454 
455             doc = doc || document;
456             // Borrowed from prototype.js
457             element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId;
458             if (!Type.exists(element)) {
459                 throw new Error(
460                     "\nJSXGraph: HTML container element '" + elementId + "' not found."
461                 );
462             }
463 
464             display = element.style.display;
465 
466             // Work around a bug in Safari
467             if (display !== "none" && display !== null) {
468                 if (element.clientWidth > 0 && element.clientHeight > 0) {
469                     return { width: element.clientWidth, height: element.clientHeight };
470                 }
471 
472                 // A parent might be set to display:none; try reading them from styles
473                 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style;
474                 return {
475                     width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0,
476                     height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0
477                 };
478             }
479 
480             // All *Width and *Height properties give 0 on elements with display set to none,
481             // hence we show the element temporarily
482             els = element.style;
483 
484             // store style
485             originalVisibility = els.visibility;
486             originalPosition = els.position;
487             originalDisplay = els.display;
488 
489             // show element
490             els.visibility = 'hidden';
491             els.position = 'absolute';
492             els.display = 'block';
493 
494             // read the dimension
495             originalWidth = element.clientWidth;
496             originalHeight = element.clientHeight;
497 
498             // restore original css values
499             els.display = originalDisplay;
500             els.position = originalPosition;
501             els.visibility = originalVisibility;
502 
503             return {
504                 width: originalWidth,
505                 height: originalHeight
506             };
507         },
508 
509         /**
510          * Adds an event listener to a DOM element.
511          * @param {Object} obj Reference to a DOM node.
512          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
513          * @param {Function} fn The function to call when the event is triggered.
514          * @param {Object} owner The scope in which the event trigger is called.
515          * @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
516          * an options object or the useCapture Boolean.
517          *
518          */
519         addEvent: function (obj, type, fn, owner, options) {
520             var el = function () {
521                 return fn.apply(owner, arguments);
522             };
523 
524             el.origin = fn;
525             // Check if owner is a board
526             if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) {
527                 owner['x_internal' + type] = owner['x_internal' + type] || [];
528                 owner['x_internal' + type].push(el);
529             }
530 
531             // Non-IE browser
532             if (Type.exists(obj) && Type.exists(obj.addEventListener)) {
533                 options = options || false;  // options or useCapture
534                 obj.addEventListener(type, el, options);
535             }
536 
537             // IE
538             if (Type.exists(obj) && Type.exists(obj.attachEvent)) {
539                 obj.attachEvent("on" + type, el);
540             }
541         },
542 
543         /**
544          * Removes an event listener from a DOM element.
545          * @param {Object} obj Reference to a DOM node.
546          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
547          * @param {Function} fn The function to call when the event is triggered.
548          * @param {Object} owner The scope in which the event trigger is called.
549          */
550         removeEvent: function (obj, type, fn, owner) {
551             var i;
552 
553             if (!Type.exists(owner)) {
554                 JXG.debug("no such owner");
555                 return;
556             }
557 
558             if (!Type.exists(owner["x_internal" + type])) {
559                 JXG.debug("removeEvent: no such type: " + type);
560                 return;
561             }
562 
563             if (!Type.isArray(owner["x_internal" + type])) {
564                 JXG.debug("owner[x_internal + " + type + "] is not an array");
565                 return;
566             }
567 
568             i = Type.indexOf(owner["x_internal" + type], fn, 'origin');
569 
570             if (i === -1) {
571                 JXG.debug("removeEvent: no such event function in internal list: " + fn);
572                 return;
573             }
574 
575             try {
576                 // Non-IE browser
577                 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) {
578                     obj.removeEventListener(type, owner["x_internal" + type][i], false);
579                 }
580 
581                 // IE
582                 if (Type.exists(obj) && Type.exists(obj.detachEvent)) {
583                     obj.detachEvent("on" + type, owner["x_internal" + type][i]);
584                 }
585             } catch (e) {
586                 JXG.debug("removeEvent: event not registered in browser: (" + type + " -- " + fn + ")");
587             }
588 
589             owner["x_internal" + type].splice(i, 1);
590         },
591 
592         /**
593          * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div
594          * of a {@link JXG.Board} because this might corrupt the event handling system.
595          * @param {Object} obj Reference to a DOM node.
596          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
597          * @param {Object} owner The scope in which the event trigger is called.
598          */
599         removeAllEvents: function (obj, type, owner) {
600             var i, len;
601             if (owner["x_internal" + type]) {
602                 len = owner["x_internal" + type].length;
603 
604                 for (i = len - 1; i >= 0; i--) {
605                     JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner);
606                 }
607 
608                 if (owner["x_internal" + type].length > 0) {
609                     JXG.debug("removeAllEvents: Not all events could be removed.");
610                 }
611             }
612         },
613 
614         /**
615          * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner.
616          * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent.
617          *
618          * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used.
619          * @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.
620          * @param {Object} [doc] The document object.
621          * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component.
622          */
623         getPosition: function (e, index, doc) {
624             var i,
625                 len,
626                 evtTouches,
627                 posx = 0,
628                 posy = 0;
629 
630             if (!e) {
631                 e = window.event;
632             }
633 
634             doc = doc || document;
635             evtTouches = e['touches']; // iOS touch events
636 
637             // touchend events have their position in "changedTouches"
638             if (Type.exists(evtTouches) && evtTouches.length === 0) {
639                 evtTouches = e.changedTouches;
640             }
641 
642             if (Type.exists(index) && Type.exists(evtTouches)) {
643                 if (index === -1) {
644                     len = evtTouches.length;
645 
646                     for (i = 0; i < len; i++) {
647                         if (evtTouches[i]) {
648                             e = evtTouches[i];
649                             break;
650                         }
651                     }
652                 } else {
653                     e = evtTouches[index];
654                 }
655             }
656 
657             // Scrolling is ignored.
658             // e.clientX is supported since IE6
659             if (e.clientX) {
660                 posx = e.clientX;
661                 posy = e.clientY;
662             }
663 
664             return [posx, posy];
665         },
666 
667         /**
668          * Calculates recursively the offset of the DOM element in which the board is stored.
669          * @param {Object} obj A DOM element
670          * @returns {Array} An array with the elements left and top offset.
671          */
672         getOffset: function (obj) {
673             var cPos,
674                 o = obj,
675                 o2 = obj,
676                 l = o.offsetLeft - o.scrollLeft,
677                 t = o.offsetTop - o.scrollTop;
678 
679             cPos = this.getCSSTransform([l, t], o);
680             l = cPos[0];
681             t = cPos[1];
682 
683             /*
684              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
685              * if not to the body. In IE and if we are in an position:absolute environment
686              * offsetParent walks up the DOM hierarchy.
687              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
688              * we need the parentNode steps.
689              */
690             o = o.offsetParent;
691             while (o) {
692                 l += o.offsetLeft;
693                 t += o.offsetTop;
694 
695                 if (o.offsetParent) {
696                     l += o.clientLeft - o.scrollLeft;
697                     t += o.clientTop - o.scrollTop;
698                 }
699 
700                 cPos = this.getCSSTransform([l, t], o);
701                 l = cPos[0];
702                 t = cPos[1];
703 
704                 o2 = o2.parentNode;
705 
706                 while (o2 !== o) {
707                     l += o2.clientLeft - o2.scrollLeft;
708                     t += o2.clientTop - o2.scrollTop;
709 
710                     cPos = this.getCSSTransform([l, t], o2);
711                     l = cPos[0];
712                     t = cPos[1];
713 
714                     o2 = o2.parentNode;
715                 }
716                 o = o.offsetParent;
717             }
718 
719             return [l, t];
720         },
721 
722         /**
723          * Access CSS style sheets.
724          * @param {Object} obj A DOM element
725          * @param {String} stylename The CSS property to read.
726          * @returns The value of the CSS property and <tt>undefined</tt> if it is not set.
727          */
728         getStyle: function (obj, stylename) {
729             var r,
730                 doc = obj.ownerDocument;
731 
732             // Non-IE
733             if (doc.defaultView && doc.defaultView.getComputedStyle) {
734                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
735                 // IE
736             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
737                 r = obj.currentStyle[stylename];
738             } else {
739                 if (obj.style) {
740                     // make stylename lower camelcase
741                     stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) {
742                         return letter.toUpperCase();
743                     });
744                     r = obj.style[stylename];
745                 }
746             }
747 
748             return r;
749         },
750 
751         /**
752          * Reads css style sheets of a given element. This method is a getStyle wrapper and
753          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
754          * @param {DOMElement} el
755          * @param {string} css
756          * @returns {number}
757          */
758         getProp: function (el, css) {
759             var n = parseInt(this.getStyle(el, css), 10);
760             return isNaN(n) ? 0 : n;
761         },
762 
763         /**
764          * Correct position of upper left corner in case of
765          * a CSS transformation. Here, only translations are
766          * extracted. All scaling transformations are corrected
767          * in {@link JXG.Board#getMousePosition}.
768          * @param {Array} cPos Previously determined position
769          * @param {Object} obj A DOM element
770          * @returns {Array} The corrected position.
771          */
772         getCSSTransform: function (cPos, obj) {
773             var i,
774                 j,
775                 str,
776                 arrStr,
777                 start,
778                 len,
779                 len2,
780                 arr,
781                 t = [
782                     "transform",
783                     "webkitTransform",
784                     "MozTransform",
785                     "msTransform",
786                     "oTransform"
787                 ];
788 
789             // Take the first transformation matrix
790             len = t.length;
791 
792             for (i = 0, str = ""; i < len; i++) {
793                 if (Type.exists(obj.style[t[i]])) {
794                     str = obj.style[t[i]];
795                     break;
796                 }
797             }
798 
799             /**
800              * Extract the coordinates and apply the transformation
801              * to cPos
802              */
803             if (str !== "") {
804                 start = str.indexOf("(");
805 
806                 if (start > 0) {
807                     len = str.length;
808                     arrStr = str.substring(start + 1, len - 1);
809                     arr = arrStr.split(",");
810 
811                     for (j = 0, len2 = arr.length; j < len2; j++) {
812                         arr[j] = parseFloat(arr[j]);
813                     }
814 
815                     if (str.indexOf('matrix') === 0) {
816                         cPos[0] += arr[4];
817                         cPos[1] += arr[5];
818                     } else if (str.indexOf('translateX') === 0) {
819                         cPos[0] += arr[0];
820                     } else if (str.indexOf('translateY') === 0) {
821                         cPos[1] += arr[0];
822                     } else if (str.indexOf('translate') === 0) {
823                         cPos[0] += arr[0];
824                         cPos[1] += arr[1];
825                     }
826                 }
827             }
828 
829             // Zoom is used by reveal.js
830             if (Type.exists(obj.style.zoom)) {
831                 str = obj.style.zoom;
832                 if (str !== "") {
833                     cPos[0] *= parseFloat(str);
834                     cPos[1] *= parseFloat(str);
835                 }
836             }
837 
838             return cPos;
839         },
840 
841         /**
842          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
843          * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported.
844          * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}.
845          */
846         getCSSTransformMatrix: function (obj) {
847             var i, j, str, arrstr, arr,
848                 start, len, len2, st,
849                 doc = obj.ownerDocument,
850                 t = [
851                     "transform",
852                     "webkitTransform",
853                     "MozTransform",
854                     "msTransform",
855                     "oTransform"
856                 ],
857                 mat = [
858                     [1, 0, 0],
859                     [0, 1, 0],
860                     [0, 0, 1]
861                 ];
862 
863             // This should work on all browsers except IE 6-8
864             if (doc.defaultView && doc.defaultView.getComputedStyle) {
865                 st = doc.defaultView.getComputedStyle(obj, null);
866                 str =
867                     st.getPropertyValue("-webkit-transform") ||
868                     st.getPropertyValue("-moz-transform") ||
869                     st.getPropertyValue("-ms-transform") ||
870                     st.getPropertyValue("-o-transform") ||
871                     st.getPropertyValue('transform');
872             } else {
873                 // Take the first transformation matrix
874                 len = t.length;
875                 for (i = 0, str = ""; i < len; i++) {
876                     if (Type.exists(obj.style[t[i]])) {
877                         str = obj.style[t[i]];
878                         break;
879                     }
880                 }
881             }
882 
883             // Convert and reorder the matrix for JSXGraph
884             if (str !== "") {
885                 start = str.indexOf("(");
886 
887                 if (start > 0) {
888                     len = str.length;
889                     arrstr = str.substring(start + 1, len - 1);
890                     arr = arrstr.split(",");
891 
892                     for (j = 0, len2 = arr.length; j < len2; j++) {
893                         arr[j] = parseFloat(arr[j]);
894                     }
895 
896                     if (str.indexOf('matrix') === 0) {
897                         mat = [
898                             [1, 0, 0],
899                             [0, arr[0], arr[1]],
900                             [0, arr[2], arr[3]]
901                         ];
902                     } else if (str.indexOf('scaleX') === 0) {
903                         mat[1][1] = arr[0];
904                     } else if (str.indexOf('scaleY') === 0) {
905                         mat[2][2] = arr[0];
906                     } else if (str.indexOf('scale') === 0) {
907                         mat[1][1] = arr[0];
908                         mat[2][2] = arr[1];
909                     }
910                 }
911             }
912 
913             // CSS style zoom is used by reveal.js
914             // Recursively search for zoom style entries.
915             // This is necessary for reveal.js on webkit.
916             // It fails if the user does zooming
917             if (Type.exists(obj.style.zoom)) {
918                 str = obj.style.zoom;
919                 if (str !== "") {
920                     mat[1][1] *= parseFloat(str);
921                     mat[2][2] *= parseFloat(str);
922                 }
923             }
924 
925             return mat;
926         },
927 
928         /**
929          * Process data in timed chunks. Data which takes long to process, either because it is such
930          * a huge amount of data or the processing takes some time, causes warnings in browsers about
931          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
932          * called chunks which will be processed in serial order.
933          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
934          * @param {Array} items to do
935          * @param {Function} process Function that is applied for every array item
936          * @param {Object} context The scope of function process
937          * @param {Function} callback This function is called after the last array element has been processed.
938          */
939         timedChunk: function (items, process, context, callback) {
940             //create a clone of the original
941             var todo = items.slice(),
942                 timerFun = function () {
943                     var start = +new Date();
944 
945                     do {
946                         process.call(context, todo.shift());
947                     } while (todo.length > 0 && +new Date() - start < 300);
948 
949                     if (todo.length > 0) {
950                         window.setTimeout(timerFun, 1);
951                     } else {
952                         callback(items);
953                     }
954                 };
955 
956             window.setTimeout(timerFun, 1);
957         },
958 
959         /**
960          * Scale and vertically shift a DOM element (usually a JSXGraph div)
961          * inside of a parent DOM
962          * element which is set to fullscreen.
963          * This is realized with a CSS transformation.
964          *
965          * @param  {String} wrap_id  id of the parent DOM element which is in fullscreen mode
966          * @param  {String} inner_id id of the DOM element which is scaled and shifted
967          * @param  {Object} doc      document object or shadow root
968          * @param  {Number} scale    Relative size of the JSXGraph board in the fullscreen window.
969          *
970          * @private
971          * @see JXG.Board#toFullscreen
972          * @see JXG.Board#fullscreenListener
973          *
974          */
975         scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) {
976             var w, h, b,
977                 wi, hi,
978                 wo, ho, inner,
979                 scale_l, vshift_l,
980                 f = scale,
981                 ratio,
982                 pseudo_keys = [
983                     ":fullscreen",
984                     ":-webkit-full-screen",
985                     ":-moz-full-screen",
986                     ":-ms-fullscreen"
987                 ],
988                 len_pseudo = pseudo_keys.length,
989                 i;
990 
991             b = doc.getElementById(wrap_id).getBoundingClientRect();
992             h = b.height;
993             w = b.width;
994 
995             inner = doc.getElementById(inner_id);
996             wo = inner._cssFullscreenStore.w;
997             ho = inner._cssFullscreenStore.h;
998             ratio = ho / wo;
999 
1000             // Scale the div such that fits into the fullscreen.
1001             if (wo > w * f) {
1002                 wo = w * f;
1003                 ho = wo * ratio;
1004             }
1005             if (ho > h * f) {
1006                 ho = h * f;
1007                 wo = ho / ratio;
1008             }
1009 
1010             wi = wo;
1011             hi = ho;
1012             // Compare the code in this.setBoundingBox()
1013             if (ratio > 1) {
1014                 // h > w
1015                 if (ratio < h / w) {
1016                     scale_l =  w * f / wo;
1017                 } else {
1018                     scale_l =  h * f / ho;
1019                 }
1020             } else {
1021                 // h <= w
1022                 if (ratio < h / w) {
1023                     scale_l = w * f / wo;
1024                 } else {
1025                     scale_l = h * f / ho;
1026                 }
1027             }
1028             vshift_l = (h - hi) * 0.5;
1029 
1030             // Set a CSS properties to center the JSXGraph div horizontally and vertically
1031             // at the first position of the fullscreen pseudo classes.
1032             for (i = 0; i < len_pseudo; i++) {
1033                 try {
1034                     inner.style.width = wi + 'px !important';
1035                     inner.style.height = hi + 'px !important';
1036                     inner.style.margin = '0 auto';
1037                     // Add the transform to a possibly already existing transform
1038                     inner.style.transform = inner._cssFullscreenStore.transform +
1039                         ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')';
1040                     break;
1041                 } catch (err) {
1042                     JXG.debug("JXG.scaleJSXGraphDiv:\n" + err);
1043                 }
1044             }
1045             if (i === len_pseudo) {
1046                 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property.");
1047             }
1048         }
1049 
1050     }
1051 );
1052 
1053 export default JXG;
1054