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