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