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