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