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