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 /* depends:
 37  jxg
 38  utils/type
 39  */
 40 
 41 /**
 42  * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish
 43  * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device
 44  * the browser runs on is a tablet/cell or a desktop computer.
 45  */
 46 
 47 define(['jxg', 'utils/type'], function (JXG, Type) {
 48 
 49     'use strict';
 50 
 51     JXG.extendConstants(JXG, /** @lends JXG */{
 52         /**
 53          * Determines the property that stores the relevant information in the event object.
 54          * @type String
 55          * @default 'touches'
 56          * @private
 57          */
 58         touchProperty: 'touches',
 59     });
 60 
 61     JXG.extend(JXG, /** @lends JXG */ {
 62         /**
 63          * Determines whether evt is a touch event.
 64          * @param evt {Event}
 65          * @returns {Boolean}
 66          */
 67         isTouchEvent: function (evt) {
 68             return JXG.exists(evt[JXG.touchProperty]);
 69         },
 70 
 71         /**
 72          * Determines whether evt is a pointer event.
 73          * @param evt {Event}
 74          * @returns {Boolean}
 75          */
 76         isPointerEvent: function (evt) {
 77             return JXG.exists(evt.pointerId);
 78         },
 79 
 80         /**
 81          * Determines whether evt is neither a touch event nor a pointer event.
 82          * @param evt {Event}
 83          * @returns {Boolean}
 84          */
 85         isMouseEvent: function (evt) {
 86             return !JXG.isTouchEvent(evt)&&!JXG.isPointerEvent(evt);
 87         },
 88 
 89         /**
 90          * Determines the number of touch points in a touch event.
 91          * For other events, -1 is returned.
 92          * @param evt {Event}
 93          * @returns {Number}
 94          */
 95         getNumberOfTouchPoints: function (evt) {
 96             var n = -1;
 97 
 98             if (JXG.isTouchEvent(evt)) {
 99                 n = evt[JXG.touchProperty].length;
100             }
101 
102             return n;
103         },
104 
105         /**
106          * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event.
107          * Attention: When two or more pointer device types are being used concurrently,
108          *            it is only checked whether the passed event is the first one of its type!
109          * @param evt {Event}
110          * @returns {boolean}
111          */
112         isFirstTouch: function (evt) {
113             var touchPoints = JXG.getNumberOfTouchPoints(evt);
114 
115             if (JXG.isPointerEvent(evt)) {
116                 return evt.isPrimary;
117             }
118 
119             return (touchPoints === 1);
120         },
121 
122         /**
123          * A document/window environment is available.
124          * @type Boolean
125          * @default false
126          */
127         isBrowser: typeof window === 'object' && typeof document === 'object',
128 
129         /**
130          * Features of ECMAScript 6+ are available.
131          * @type Boolean
132          * @default false
133          */
134         supportsES6: function () {
135             var testMap;
136             /* jshint ignore:start */
137             try {
138                 // This would kill the old uglifyjs: testMap = (a = 0) => a;
139                 new Function('(a = 0) => a');
140                 return true;
141             } catch (err) {
142                 return false;
143             }
144             /* jshint ignore:end */
145         },
146 
147         /**
148          * Detect browser support for VML.
149          * @returns {Boolean} True, if the browser supports VML.
150          */
151         supportsVML: function () {
152             // From stackoverflow.com
153             return this.isBrowser && !!document.namespaces;
154         },
155 
156         /**
157          * Detect browser support for SVG.
158          * @returns {Boolean} True, if the browser supports SVG.
159          */
160         supportsSVG: function () {
161             return this.isBrowser && document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
162         },
163 
164         /**
165          * Detect browser support for Canvas.
166          * @returns {Boolean} True, if the browser supports HTML canvas.
167          */
168         supportsCanvas: function () {
169             var c, hasCanvas = false;
170 
171             if (this.isNode()) {
172                 try {
173                     c = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
174                     hasCanvas = !!c;
175                 } catch (err) {
176                 }
177             }
178 
179             return hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext);
180         },
181 
182         /**
183          * True, if run inside a node.js environment.
184          * @returns {Boolean}
185          */
186         isNode: function () {
187             // this is not a 100% sure but should be valid in most cases
188 
189             // we are not inside a browser
190             return !this.isBrowser && (
191                 // there is a module object (plain node, no requirejs)
192                 (typeof module === 'object' && !!module.exports) ||
193                 // there is a global object and requirejs is loaded
194                 (typeof global === 'object' && global.requirejsVars && !global.requirejsVars.isBrowser)
195             );
196         },
197 
198         /**
199          * True if run inside a webworker environment.
200          * @returns {Boolean}
201          */
202         isWebWorker: function () {
203             return !this.isBrowser && (typeof self === 'object' && typeof self.postMessage === 'function');
204         },
205 
206         /**
207          * Checks if the environments supports the W3C Pointer Events API {@link http://www.w3.org/Submission/pointer-events/}
208          * @returns {Boolean}
209          */
210         supportsPointerEvents: function () {
211             return !!(this.isBrowser && window.navigator &&
212                 (window.PointerEvent ||             // Chrome/Edge/IE11+
213                     window.navigator.pointerEnabled || // IE11+
214                     window.navigator.msPointerEnabled  // IE10-
215                 )
216             );
217         },
218 
219         /**
220          * Determine if the current browser supports touch events
221          * @returns {Boolean} True, if the browser supports touch events.
222          */
223         isTouchDevice: function () {
224             return this.isBrowser && window.ontouchstart !== undefined;
225         },
226 
227         /**
228          * Detects if the user is using an Android powered device.
229          * @returns {Boolean}
230          */
231         isAndroid: function () {
232             return Type.exists(navigator) && navigator.userAgent.toLowerCase().indexOf('android') > -1;
233         },
234 
235         /**
236          * Detects if the user is using the default Webkit browser on an Android powered device.
237          * @returns {Boolean}
238          */
239         isWebkitAndroid: function () {
240             return this.isAndroid() && navigator.userAgent.indexOf(' AppleWebKit/') > -1;
241         },
242 
243         /**
244          * Detects if the user is using a Apple iPad / iPhone.
245          * @returns {Boolean}
246          */
247         isApple: function () {
248             return Type.exists(navigator) && (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPhone') > -1);
249         },
250 
251         /**
252          * Detects if the user is using Safari on an Apple device.
253          * @returns {Boolean}
254          */
255         isWebkitApple: function () {
256             return this.isApple() && (navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1);
257         },
258 
259         /**
260          * Returns true if the run inside a Windows 8 "Metro" App.
261          * @returns {Boolean}
262          */
263         isMetroApp: function () {
264             return typeof window === 'object' && window.clientInformation && window.clientInformation.appVersion && window.clientInformation.appVersion.indexOf('MSAppHost') > -1;
265         },
266 
267         /**
268          * Detects if the user is using a Mozilla browser
269          * @returns {Boolean}
270          */
271         isMozilla: function () {
272             return Type.exists(navigator) &&
273                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 &&
274                 navigator.userAgent.toLowerCase().indexOf('apple') === -1;
275         },
276 
277         /**
278          * Detects if the user is using a firefoxOS powered device.
279          * @returns {Boolean}
280          */
281         isFirefoxOS: function () {
282             return Type.exists(navigator) &&
283                 navigator.userAgent.toLowerCase().indexOf('android') === -1 &&
284                 navigator.userAgent.toLowerCase().indexOf('apple') === -1 &&
285                 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 &&
286                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1;
287         },
288 
289         /**
290          * Internet Explorer version. Works only for IE > 4.
291          * @type Number
292          */
293         ieVersion: (function () {
294             var div, all,
295                 v = 3;
296 
297             if (typeof document !== 'object') {
298                 return 0;
299             }
300 
301             div = document.createElement('div');
302             all = div.getElementsByTagName('i');
303 
304             do {
305                 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><' + 'i><' + '/i><![endif]-->';
306             } while (all[0]);
307 
308             return v > 4 ? v : undefined;
309 
310         }()),
311 
312         /**
313          * Reads the width and height of an HTML element.
314          * @param {String} elementId The HTML id of an HTML DOM node.
315          * @returns {Object} An object with the two properties width and height.
316          */
317         getDimensions: function (elementId, doc) {
318             var element, display, els, originalVisibility, originalPosition,
319                 originalDisplay, originalWidth, originalHeight, style,
320                 pixelDimRegExp = /\d+(\.\d*)?px/;
321 
322             if (!this.isBrowser || elementId === null) {
323                 return {
324                     width: 500,
325                     height: 500
326                 };
327             }
328 
329             doc = doc || document;
330             // Borrowed from prototype.js
331             element = doc.getElementById(elementId);
332             if (!Type.exists(element)) {
333                 throw new Error('\nJSXGraph: HTML container element \'' + elementId + '\' not found.');
334             }
335 
336             display = element.style.display;
337 
338             // Work around a bug in Safari
339             if (display !== 'none' && display !== null) {
340                 if (element.clientWidth > 0 && element.clientHeight > 0) {
341                     return {width: element.clientWidth, height: element.clientHeight};
342                 }
343 
344                 // a parent might be set to display:none; try reading them from styles
345                 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style;
346                 return {
347                     width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0,
348                     height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0
349                 };
350             }
351 
352             // All *Width and *Height properties give 0 on elements with display set to none,
353             // hence we show the element temporarily
354             els = element.style;
355 
356             // save style
357             originalVisibility = els.visibility;
358             originalPosition = els.position;
359             originalDisplay = els.display;
360 
361             // show element
362             els.visibility = 'hidden';
363             els.position = 'absolute';
364             els.display = 'block';
365 
366             // read the dimension
367             originalWidth = element.clientWidth;
368             originalHeight = element.clientHeight;
369 
370             // restore original css values
371             els.display = originalDisplay;
372             els.position = originalPosition;
373             els.visibility = originalVisibility;
374 
375             return {
376                 width: originalWidth,
377                 height: originalHeight
378             };
379         },
380 
381         /**
382          * Adds an event listener to a DOM element.
383          * @param {Object} obj Reference to a DOM node.
384          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
385          * @param {Function} fn The function to call when the event is triggered.
386          * @param {Object} owner The scope in which the event trigger is called.
387          */
388         addEvent: function (obj, type, fn, owner) {
389             var el = function () {
390                 return fn.apply(owner, arguments);
391             };
392 
393             el.origin = fn;
394             owner['x_internal' + type] = owner['x_internal' + type] || [];
395             owner['x_internal' + type].push(el);
396 
397             // Non-IE browser
398             if (Type.exists(obj) && Type.exists(obj.addEventListener)) {
399                 obj.addEventListener(type, el, false);
400             }
401 
402             // IE
403             if (Type.exists(obj) && Type.exists(obj.attachEvent)) {
404                 obj.attachEvent('on' + type, el);
405             }
406         },
407 
408         /**
409          * Removes an event listener from a DOM element.
410          * @param {Object} obj Reference to a DOM node.
411          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
412          * @param {Function} fn The function to call when the event is triggered.
413          * @param {Object} owner The scope in which the event trigger is called.
414          */
415         removeEvent: function (obj, type, fn, owner) {
416             var i;
417 
418             if (!Type.exists(owner)) {
419                 JXG.debug('no such owner');
420                 return;
421             }
422 
423             if (!Type.exists(owner['x_internal' + type])) {
424                 JXG.debug('no such type: ' + type);
425                 return;
426             }
427 
428             if (!Type.isArray(owner['x_internal' + type])) {
429                 JXG.debug('owner[x_internal + ' + type + '] is not an array');
430                 return;
431             }
432 
433             i = Type.indexOf(owner['x_internal' + type], fn, 'origin');
434 
435             if (i === -1) {
436                 JXG.debug('removeEvent: no such event function in internal list: ' + fn);
437                 return;
438             }
439 
440             try {
441                 // Non-IE browser
442                 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) {
443                     obj.removeEventListener(type, owner['x_internal' + type][i], false);
444                 }
445 
446                 // IE
447                 if (Type.exists(obj) && Type.exists(obj.detachEvent)) {
448                     obj.detachEvent('on' + type, owner['x_internal' + type][i]);
449                 }
450             } catch (e) {
451                 JXG.debug('event not registered in browser: (' + type + ' -- ' + fn + ')');
452             }
453 
454             owner['x_internal' + type].splice(i, 1);
455         },
456 
457         /**
458          * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div
459          * of a {@link JXG.Board} because this might corrupt the event handling system.
460          * @param {Object} obj Reference to a DOM node.
461          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
462          * @param {Object} owner The scope in which the event trigger is called.
463          */
464         removeAllEvents: function (obj, type, owner) {
465             var i, len;
466             if (owner['x_internal' + type]) {
467                 len = owner['x_internal' + type].length;
468 
469                 for (i = len - 1; i >= 0; i--) {
470                     JXG.removeEvent(obj, type, owner['x_internal' + type][i].origin, owner);
471                 }
472 
473                 if (owner['x_internal' + type].length > 0) {
474                     JXG.debug('removeAllEvents: Not all events could be removed.');
475                 }
476             }
477         },
478 
479         /**
480          * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner.
481          * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used.
482          * @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.
483          * @param {Object} [doc] The document object.
484          * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component.
485          */
486         getPosition: function (e, index, doc) {
487             var i, len, evtTouches,
488                 posx = 0,
489                 posy = 0;
490 
491             if (!e) {
492                 e = window.event;
493             }
494 
495             doc = doc || document;
496             evtTouches = e[JXG.touchProperty];
497 
498             // touchend events have their position in "changedTouches"
499             if (Type.exists(evtTouches) && evtTouches.length === 0) {
500                 evtTouches = e.changedTouches;
501             }
502 
503             if (Type.exists(index) && Type.exists(evtTouches)) {
504                 if (index === -1) {
505                     len = evtTouches.length;
506 
507                     for (i = 0; i < len; i++) {
508                         if (evtTouches[i]) {
509                             e = evtTouches[i];
510                             break;
511                         }
512                     }
513 
514                 } else {
515                     e = evtTouches[index];
516                 }
517             }
518 
519             // Scrolling is ignored.
520             // e.clientX is supported since IE6
521             if (e.clientX) {
522                 posx = e.clientX;
523                 posy = e.clientY;
524             }
525 
526             return [posx, posy];
527         },
528 
529         /**
530          * Calculates recursively the offset of the DOM element in which the board is stored.
531          * @param {Object} obj A DOM element
532          * @returns {Array} An array with the elements left and top offset.
533          */
534         getOffset: function (obj) {
535             var cPos,
536                 o = obj,
537                 o2 = obj,
538                 l = o.offsetLeft - o.scrollLeft,
539                 t = o.offsetTop - o.scrollTop;
540 
541             cPos = this.getCSSTransform([l, t], o);
542             l = cPos[0];
543             t = cPos[1];
544 
545             /*
546              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
547              * if not to the body. In IE and if we are in an position:absolute environment
548              * offsetParent walks up the DOM hierarchy.
549              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
550              * we need the parentNode steps.
551              */
552             o = o.offsetParent;
553             while (o) {
554                 l += o.offsetLeft;
555                 t += o.offsetTop;
556 
557                 if (o.offsetParent) {
558                     l += o.clientLeft - o.scrollLeft;
559                     t += o.clientTop - o.scrollTop;
560                 }
561 
562                 cPos = this.getCSSTransform([l, t], o);
563                 l = cPos[0];
564                 t = cPos[1];
565 
566                 o2 = o2.parentNode;
567 
568                 while (o2 !== o) {
569                     l += o2.clientLeft - o2.scrollLeft;
570                     t += o2.clientTop - o2.scrollTop;
571 
572                     cPos = this.getCSSTransform([l, t], o2);
573                     l = cPos[0];
574                     t = cPos[1];
575 
576                     o2 = o2.parentNode;
577                 }
578                 o = o.offsetParent;
579             }
580 
581             return [l, t];
582         },
583 
584         /**
585          * Access CSS style sheets.
586          * @param {Object} obj A DOM element
587          * @param {String} stylename The CSS property to read.
588          * @returns The value of the CSS property and <tt>undefined</tt> if it is not set.
589          */
590         getStyle: function (obj, stylename) {
591             var r,
592                 doc = obj.ownerDocument;
593 
594             // Non-IE
595             if (doc.defaultView && doc.defaultView.getComputedStyle) {
596                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
597                 // IE
598             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
599                 r = obj.currentStyle[stylename];
600             } else {
601                 if (obj.style) {
602                     // make stylename lower camelcase
603                     stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) {
604                         return letter.toUpperCase();
605                     });
606                     r = obj.style[stylename];
607                 }
608             }
609 
610             return r;
611         },
612 
613         /**
614          * Reads css style sheets of a given element. This method is a getStyle wrapper and
615          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
616          * @param {DOMElement} el
617          * @param {string} css
618          * @returns {number}
619          */
620         getProp: function (el, css) {
621             var n = parseInt(this.getStyle(el, css), 10);
622             return isNaN(n) ? 0 : n;
623         },
624 
625         /**
626          * Correct position of upper left corner in case of
627          * a CSS transformation. Here, only translations are
628          * extracted. All scaling transformations are corrected
629          * in {@link JXG.Board#getMousePosition}.
630          * @param {Array} cPos Previously determined position
631          * @param {Object} obj A DOM element
632          * @returns {Array} The corrected position.
633          */
634         getCSSTransform: function (cPos, obj) {
635             var i, j, str, arrStr, start, len, len2, arr,
636                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'];
637 
638             // Take the first transformation matrix
639             len = t.length;
640 
641             for (i = 0, str = ''; i < len; i++) {
642                 if (Type.exists(obj.style[t[i]])) {
643                     str = obj.style[t[i]];
644                     break;
645                 }
646             }
647 
648             /**
649              * Extract the coordinates and apply the transformation
650              * to cPos
651              */
652             if (str !== '') {
653                 start = str.indexOf('(');
654 
655                 if (start > 0) {
656                     len = str.length;
657                     arrStr = str.substring(start + 1, len - 1);
658                     arr = arrStr.split(',');
659 
660                     for (j = 0, len2 = arr.length; j < len2; j++) {
661                         arr[j] = parseFloat(arr[j]);
662                     }
663 
664                     if (str.indexOf('matrix') === 0) {
665                         cPos[0] += arr[4];
666                         cPos[1] += arr[5];
667                     } else if (str.indexOf('translateX') === 0) {
668                         cPos[0] += arr[0];
669                     } else if (str.indexOf('translateY') === 0) {
670                         cPos[1] += arr[0];
671                     } else if (str.indexOf('translate') === 0) {
672                         cPos[0] += arr[0];
673                         cPos[1] += arr[1];
674                     }
675                 }
676             }
677 
678             // Zoom is used by reveal.js
679             if (Type.exists(obj.style.zoom)) {
680                 str = obj.style.zoom;
681                 if (str !== '') {
682                     cPos[0] *= parseFloat(str);
683                     cPos[1] *= parseFloat(str);
684                 }
685             }
686 
687             return cPos;
688         },
689 
690         /**
691          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
692          * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported.
693          * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}.
694          */
695         getCSSTransformMatrix: function (obj) {
696             var i, j, str, arrstr, start, len, len2, arr,
697                 st,
698                 doc = obj.ownerDocument,
699                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'],
700                 mat = [[1, 0, 0],
701                     [0, 1, 0],
702                     [0, 0, 1]];
703 
704             // This should work on all browsers except IE 6-8
705             if (doc.defaultView && doc.defaultView.getComputedStyle) {
706                 st = doc.defaultView.getComputedStyle(obj, null);
707                 str = st.getPropertyValue('-webkit-transform') ||
708                     st.getPropertyValue('-moz-transform') ||
709                     st.getPropertyValue('-ms-transform') ||
710                     st.getPropertyValue('-o-transform') ||
711                     st.getPropertyValue('transform');
712             } else {
713                 // Take the first transformation matrix
714                 len = t.length;
715                 for (i = 0, str = ''; i < len; i++) {
716                     if (Type.exists(obj.style[t[i]])) {
717                         str = obj.style[t[i]];
718                         break;
719                     }
720                 }
721             }
722 
723             if (str !== '') {
724                 start = str.indexOf('(');
725 
726                 if (start > 0) {
727                     len = str.length;
728                     arrstr = str.substring(start + 1, len - 1);
729                     arr = arrstr.split(',');
730 
731                     for (j = 0, len2 = arr.length; j < len2; j++) {
732                         arr[j] = parseFloat(arr[j]);
733                     }
734 
735                     if (str.indexOf('matrix') === 0) {
736                         mat = [[1, 0, 0],
737                             [0, arr[0], arr[1]],
738                             [0, arr[2], arr[3]]];
739                     } else if (str.indexOf('scaleX') === 0) {
740                         mat[1][1] = arr[0];
741                     } else if (str.indexOf('scaleY') === 0) {
742                         mat[2][2] = arr[0];
743                     } else if (str.indexOf('scale') === 0) {
744                         mat[1][1] = arr[0];
745                         mat[2][2] = arr[1];
746                     }
747                 }
748             }
749 
750             // CSS style zoom is used by reveal.js
751             // Recursively search for zoom style entries.
752             // This is necessary for reveal.js on webkit.
753             // It fails if the user does zooming
754             if (Type.exists(obj.style.zoom)) {
755                 str = obj.style.zoom;
756                 if (str !== '') {
757                     mat[1][1] *= parseFloat(str);
758                     mat[2][2] *= parseFloat(str);
759                 }
760             }
761 
762             return mat;
763         },
764 
765         /**
766          * Process data in timed chunks. Data which takes long to process, either because it is such
767          * a huge amount of data or the processing takes some time, causes warnings in browsers about
768          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
769          * called chunks which will be processed in serial order.
770          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
771          * @param {Array} items to do
772          * @param {Function} process Function that is applied for every array item
773          * @param {Object} context The scope of function process
774          * @param {Function} callback This function is called after the last array element has been processed.
775          */
776         timedChunk: function (items, process, context, callback) {
777             //create a clone of the original
778             var todo = items.concat(),
779                 timerFun = function () {
780                     var start = +new Date();
781 
782                     do {
783                         process.call(context, todo.shift());
784                     } while (todo.length > 0 && (+new Date() - start < 300));
785 
786                     if (todo.length > 0) {
787                         window.setTimeout(timerFun, 1);
788                     } else {
789                         callback(items);
790                     }
791                 };
792 
793             window.setTimeout(timerFun, 1);
794         },
795 
796         /**
797          * Scale and vertically shift a DOM element (usually a JSXGraph div)
798          * inside of a parent DOM
799          * element which is set to fullscreen.
800          * This is realized with a CSS transformation.
801          *
802          * @param  {String} wrap_id  id of the parent DOM element which is in fullscreen mode
803          * @param  {String} inner_id id of the DOM element which is scaled and shifted
804          * @param  {Object} doc      document object or shadow root
805          *
806          * @private
807          * @see JXG.Board#toFullscreen
808          * @see JXG.Board#fullscreenListener
809          *
810          */
811          scaleJSXGraphDiv: function (wrap_id, inner_id, doc) {
812             var len = doc.styleSheets.length, style, rule, w, h, b, wi, hi, bi,
813                 scale_l, vshift_l, // scale_p, vshift_p,
814                 f = 0.85,
815                 rule_inner_l, // rule_inner_p,
816 
817                 pseudo_keys = [':fullscreen', ':-webkit-full-screen', ':-moz-full-screen', ':-ms-fullscreen'],
818                 len_pseudo = pseudo_keys.length, i,
819 
820                 // A previously installed CSS rule to center the JSXGraph div has to
821                 // be searched and removed again.
822                 regex = new RegExp('.*#' + wrap_id + ':.*full.*screen.*#' + inner_id + '.*auto;.*transform:.*matrix');
823 
824             b = doc.getElementById(wrap_id).getBoundingClientRect();
825             h = b.height;
826             w = b.width;
827 
828             bi = doc.getElementById(inner_id).getBoundingClientRect();
829             hi = bi.height;
830             wi = bi.width;
831 
832             if (wi / hi >= w / h) {
833                 scale_l = f * w / wi;
834             } else {
835                 scale_l = f * h / hi;
836             }
837             vshift_l = (h - hi) * 0.5;
838 
839             // CSS rules to center the inner div horizontally and vertically.
840             rule_inner_l = '{margin:0 auto;transform:matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ');}';
841 
842             if (len === 0) {
843                 // In case there is not a single CSS rule defined at all.
844                 style = document.createElement('style');
845                 // WebKit hack :(
846                 style.appendChild(document.createTextNode(''));
847                 // Add the <style> element to the page
848                 doc.appendChild(style);
849                 len = doc.styleSheets.length;
850             }
851 
852             // Remove a previously installed CSS rule.
853             if (doc.styleSheets[len - 1].cssRules.length > 0 &&
854                 regex.test(doc.styleSheets[len - 1].cssRules[0].cssText) &&
855                 doc.styleSheets[len - 1].deleteRule) {
856                     doc.styleSheets[len - 1].deleteRule(0);
857             }
858 
859             // Install a CSS rule to center the JSXGraph div at the first position of the list.
860             for (i = 0; i < len_pseudo; i++) {
861                 try {
862                     rule = '#' + wrap_id + pseudo_keys[i] + ' #' + inner_id + rule_inner_l;
863                     // rule = '@media all and (orientation:landscape) {' + rule + '}';
864                     doc.styleSheets[len - 1].insertRule(rule, 0);
865 
866                     break;
867                 } catch (err) {
868                     // console.log('JXG.scaleJSXGraphDiv: Could not add CSS rule "' + pseudo_keys[i] + '".');
869                     // console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.');
870                 }
871             }
872             if (i === len_pseudo) {
873                 console.log('JXG.scaleJSXGraphDiv: Could not add any CSS rule.');
874                 console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.');
875             }
876         }
877 
878     });
879 
880     return JXG;
881 });
882