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