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