      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          * Calculate the scale factor and vertical shift for the JSXGraph div
    798          * in full screen mode.
    799          *
    800          * @param {Object} obj Reference to a DOM node.
    801          * @returns Object {scale: number, vshift: number}
    802          * @see JXG.Board#fullscreenListener
    803          * @private
    804          */
    805         _getScaleFactors: function (node) {
    806             var width = node.getBoundingClientRect().width,
    807                 height = node.getBoundingClientRect().height,
    808
    809                 // Determine the maximum scale factor.
    810                 r_w = window.screen.width / width,
    811                 r_h = window.screen.height / height,
    812
    813                 // Determine the vertical shift to place the div in the center of the screen
    814                 vshift = (window.screen.height - height) * 0.5,
    815
    816                 // Scaling factor: if not supplied, it's taken as large as possible
    817                 scale = Math.min(r_w, r_h);
    818
    819             // Adapt vshift and scale for landscape on tablets
    820             if (window.matchMedia && window.matchMedia('(orientation:landscape)').matches &&
    821                 window.screen.width < window.screen.height) {
    822                 // Landscape on iOS: it returns 'landscape', but still width < height.
    823                 r_w = window.screen.height / width;
    824                 r_h = window.screen.width / height;
    825                 scale = Math.min(r_w, r_h);
    826                 vshift = (window.screen.width - height) * 0.5;
    827             }
    828             scale *= 0.85;
    829
    830             return { scale: scale, vshift: vshift, width: width };
    831         },
    832
    833         /**
    834          * Scale and vertically shift a DOM element (usually a JSXGraph div)
    835          * inside of a parent DOM
    836          * element which is set to fullscreen.
    837          * This is realized with a CSS transformation.
    838          *          *
    839          * @param  {String} wrap_id  id of the parent DOM element which is in fullscreen mode
    840          * @param  {String} inner_id id of the DOM element which is scaled and shifted
    841          * @param  {Number} scale    Scaling factor
    842          * @param  {Number} vshift   Vertical shift (in pixel)
    843          *
    844          * @private
    845          * @see JXG.Board#toFullscreen
    846          * @see JXG.Board#fullscreenListener
    847          *
    848          */
    849         scaleJSXGraphDiv: function (wrap_id, inner_id, scale, vshift) {
    850             var len = document.styleSheets.length, style,
    851
    852                 pseudo_keys = [':fullscreen', ':-webkit-full-screen', ':-moz-full-screen', ':-ms-fullscreen'],
    853                 len_pseudo = pseudo_keys.length, i,
    854
    855                 // CSS rules to center the inner div horizontally and vertically.
    856                 rule_inner = '{margin:0 auto;transform:matrix(' + scale + ',0,0,' + scale + ',0,' + vshift + ');}',
    857
    858                 // A previously installed CSS rule to center the JSXGraph div has to
    859                 // be searched and removed again.
    860                 regex = new RegExp('.*#' + wrap_id + ':.*full.*screen.*#' + inner_id + '.*auto;.*transform:.*matrix');
    861
    862             if (len === 0) {
    863                 // In case there is not a single CSS rule defined at all.
    864                 style = document.createElement('style');
    865                 // WebKit hack :(
    866                 style.appendChild(document.createTextNode(''));
    867                 // Add the <style> element to the page
    868                 document.body.appendChild(style);
    869                 len = document.styleSheets.length;
    870             }
    871
    872             // Remove a previously installed CSS rule.
    873             if (document.styleSheets[len - 1].cssRules.length > 0 &&
    874                 regex.test(document.styleSheets[len - 1].cssRules[0].cssText) &&
    875                 document.styleSheets[len - 1].deleteRule) {
    876
    877                 document.styleSheets[len - 1].deleteRule(0);
    878             }
    879
    880             // Install a CSS rule to center the JSXGraph div at the first position of the list.
    881             for (i = 0; i < len_pseudo; i++) {
    882                 try {
    883                     document.styleSheets[len - 1].insertRule('#' + wrap_id + pseudo_keys[i] + ' #' + inner_id + rule_inner, 0);
    884                     break;
    885                 } catch (err) {
    886                     // console.log('JXG.scaleJSXGraphDiv: Could not add CSS rule "' + pseudo_keys[i] + '".');
    887                     // console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.');
    888                 }
    889             }
    890             if (i === len_pseudo) {
    891                 console.log('JXG.scaleJSXGraphDiv: Could not add any CSS rule.');
    892                 console.log('One possible reason could be that the id of the JSXGraph container does not start with a letter.');
    893             }
    894         }
    895     });
    896
    897     return JXG;
    898 });
    899