      1 /*
      2     Copyright 2008-2018
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Alfred Wassermann,
      8         Peter Wilfahrt
      9
     10     This file is part of JSXGraph.
     11
     12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     13
     14     You can redistribute it and/or modify it under the terms of the
     15
     16       * GNU Lesser General Public License as published by
     17         the Free Software Foundation, either version 3 of the License, or
     18         (at your option) any later version
     19       OR
     20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     21
     22     JSXGraph is distributed in the hope that it will be useful,
     23     but WITHOUT ANY WARRANTY; without even the implied warranty of
     24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     25     GNU Lesser General Public License for more details.
     26
     27     You should have received a copy of the GNU Lesser General Public License and
     28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
     29     and <http://opensource.org/licenses/MIT/>.
     30  */
     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.extend(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          * A document/window environment is available.
     61          * @type Boolean
     62          * @default false
     63          */
     64         isBrowser: typeof window === 'object' && typeof document === 'object',
     65
     66         /**
     67          * Detect browser support for VML.
     68          * @returns {Boolean} True, if the browser supports VML.
     69          */
     70         supportsVML: function () {
     71             // From stackoverflow.com
     72             return this.isBrowser && !!document.namespaces;
     73         },
     74
     75         /**
     76          * Detect browser support for SVG.
     77          * @returns {Boolean} True, if the browser supports SVG.
     78          */
     79         supportsSVG: function () {
     80             return this.isBrowser && document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
     81         },
     82
     83         /**
     84          * Detect browser support for Canvas.
     85          * @returns {Boolean} True, if the browser supports HTML canvas.
     86          */
     87         supportsCanvas: function () {
     88             var c, hasCanvas = false;
     89
     90             if (this.isNode()) {
     91                 try {
     92                     c = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
     93                     hasCanvas = !!c;
     94                 } catch (err) { }
     95             }
     96
     97             return hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext);
     98         },
     99
    100         /**
    101          * True, if run inside a node.js environment.
    102          * @returns {Boolean}
    103          */
    104         isNode: function () {
    105             // this is not a 100% sure but should be valid in most cases
    106
    107                 // we are not inside a browser
    108             return !this.isBrowser && (
    109                 // there is a module object (plain node, no requirejs)
    110                 (typeof module === 'object' && !!module.exports) ||
    111                 // there is a global object and requirejs is loaded
    112                 (typeof global === 'object' && global.requirejsVars && !global.requirejsVars.isBrowser)
    113             );
    114         },
    115
    116         /**
    117          * True if run inside a webworker environment.
    118          * @returns {Boolean}
    119          */
    120         isWebWorker: function () {
    121             return !this.isBrowser && (typeof self === 'object' && typeof self.postMessage === 'function');
    122         },
    123
    124         /**
    125          * Checks if the environments supports the W3C Pointer Events API {@link http://www.w3.org/Submission/pointer-events/}
    126          * @returns {Boolean}
    127          */
    128         supportsPointerEvents: function () {
    129             return !!(this.isBrowser && window.navigator &&
    130                      // Chrome/IE11+                IE11+                              IE10-
    131                     (/*window.PointerEvent ||*/ window.navigator.pointerEnabled || window.navigator.msPointerEnabled));
    132         },
    133
    134         /**
    135          * Determine if the current browser supports touch events
    136          * @returns {Boolean} True, if the browser supports touch events.
    137          */
    138         isTouchDevice: function () {
    139             return this.isBrowser && window.ontouchstart !== undefined;
    140         },
    141
    142         /**
    143          * Detects if the user is using an Android powered device.
    144          * @returns {Boolean}
    145          */
    146         isAndroid: function () {
    147             return Type.exists(navigator) && navigator.userAgent.toLowerCase().indexOf('android') > -1;
    148         },
    149
    150         /**
    151          * Detects if the user is using the default Webkit browser on an Android powered device.
    152          * @returns {Boolean}
    153          */
    154         isWebkitAndroid: function () {
    155             return this.isAndroid() && navigator.userAgent.indexOf(' AppleWebKit/') > -1;
    156         },
    157
    158         /**
    159          * Detects if the user is using a Apple iPad / iPhone.
    160          * @returns {Boolean}
    161          */
    162         isApple: function () {
    163             return Type.exists(navigator) && (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPhone') > -1);
    164         },
    165
    166         /**
    167          * Detects if the user is using Safari on an Apple device.
    168          * @returns {Boolean}
    169          */
    170         isWebkitApple: function () {
    171             return this.isApple() && (navigator.userAgent.search(/Mobile\/[0-9A-Za-z\.]*Safari/) > -1);
    172         },
    173
    174         /**
    175          * Returns true if the run inside a Windows 8 "Metro" App.
    176          * @returns {Boolean}
    177          */
    178         isMetroApp: function () {
    179             return typeof window === 'object' && window.clientInformation && window.clientInformation.appVersion && window.clientInformation.appVersion.indexOf('MSAppHost') > -1;
    180         },
    181
    182         /**
    183          * Detects if the user is using a Mozilla browser
    184          * @returns {Boolean}
    185          */
    186         isMozilla: function () {
    187             return Type.exists(navigator) &&
    188                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 &&
    189                 navigator.userAgent.toLowerCase().indexOf('apple') === -1;
    190         },
    191
    192         /**
    193          * Detects if the user is using a firefoxOS powered device.
    194          * @returns {Boolean}
    195          */
    196         isFirefoxOS: function () {
    197             return Type.exists(navigator) &&
    198                 navigator.userAgent.toLowerCase().indexOf('android') === -1 &&
    199                 navigator.userAgent.toLowerCase().indexOf('apple') === -1 &&
    200                 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 &&
    201                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1;
    202         },
    203
    204         /**
    205          * Internet Explorer version. Works only for IE > 4.
    206          * @type Number
    207          */
    208         ieVersion: (function () {
    209             var div, all,
    210                 v = 3;
    211
    212             if (typeof document !== 'object') {
    213                 return 0;
    214             }
    215
    216             div = document.createElement('div');
    217             all = div.getElementsByTagName('i');
    218
    219             do {
    220                 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><' + 'i><' + '/i><![endif]-->';
    221             } while (all[0]);
    222
    223             return v > 4 ? v : undefined;
    224
    225         }()),
    226
    227         /**
    228          * Reads the width and height of an HTML element.
    229          * @param {String} elementId The HTML id of an HTML DOM node.
    230          * @returns {Object} An object with the two properties width and height.
    231          */
    232         getDimensions: function (elementId, doc) {
    233             var element, display, els, originalVisibility, originalPosition,
    234                 originalDisplay, originalWidth, originalHeight, style,
    235                 pixelDimRegExp = /\d+(\.\d*)?px/;
    236
    237             if (!this.isBrowser || elementId === null) {
    238                 return {
    239                     width: 500,
    240                     height: 500
    241                 };
    242             }
    243
    244             doc = doc || document;
    245             // Borrowed from prototype.js
    246             element = doc.getElementById(elementId);
    247             if (!Type.exists(element)) {
    248                 throw new Error("\nJSXGraph: HTML container element '" + elementId + "' not found.");
    249             }
    250
    251             display = element.style.display;
    252
    253             // Work around a bug in Safari
    254             if (display !== 'none' && display !== null) {
    255                 if (element.clientWidth > 0 && element.clientHeight > 0) {
    256                     return {width: element.clientWidth, height: element.clientHeight};
    257                 }
    258
    259                 // a parent might be set to display:none; try reading them from styles
    260                 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style;
    261                 return {
    262                     width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0,
    263                     height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0
    264                 };
    265             }
    266
    267             // All *Width and *Height properties give 0 on elements with display set to none,
    268             // hence we show the element temporarily
    269             els = element.style;
    270
    271             // save style
    272             originalVisibility = els.visibility;
    273             originalPosition = els.position;
    274             originalDisplay = els.display;
    275
    276             // show element
    277             els.visibility = 'hidden';
    278             els.position = 'absolute';
    279             els.display = 'block';
    280
    281             // read the dimension
    282             originalWidth = element.clientWidth;
    283             originalHeight = element.clientHeight;
    284
    285             // restore original css values
    286             els.display = originalDisplay;
    287             els.position = originalPosition;
    288             els.visibility = originalVisibility;
    289
    290             return {
    291                 width: originalWidth,
    292                 height: originalHeight
    293             };
    294         },
    295
    296         /**
    297          * Adds an event listener to a DOM element.
    298          * @param {Object} obj Reference to a DOM node.
    299          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    300          * @param {Function} fn The function to call when the event is triggered.
    301          * @param {Object} owner The scope in which the event trigger is called.
    302          */
    303         addEvent: function (obj, type, fn, owner) {
    304             var el = function () {
    305                 return fn.apply(owner, arguments);
    306             };
    307
    308             el.origin = fn;
    309             owner['x_internal' + type] = owner['x_internal' + type] || [];
    310             owner['x_internal' + type].push(el);
    311
    312             // Non-IE browser
    313             if (Type.exists(obj) && Type.exists(obj.addEventListener)) {
    314                 obj.addEventListener(type, el, false);
    315             }
    316
    317             // IE
    318             if (Type.exists(obj) && Type.exists(obj.attachEvent)) {
    319                 obj.attachEvent('on' + type, el);
    320             }
    321         },
    322
    323         /**
    324          * Removes an event listener from a DOM element.
    325          * @param {Object} obj Reference to a DOM node.
    326          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    327          * @param {Function} fn The function to call when the event is triggered.
    328          * @param {Object} owner The scope in which the event trigger is called.
    329          */
    330         removeEvent: function (obj, type, fn, owner) {
    331             var i;
    332
    333             if (!Type.exists(owner)) {
    334                 JXG.debug('no such owner');
    335                 return;
    336             }
    337
    338             if (!Type.exists(owner['x_internal' + type])) {
    339                 JXG.debug('no such type: ' + type);
    340                 return;
    341             }
    342
    343             if (!Type.isArray(owner['x_internal' + type])) {
    344                 JXG.debug('owner[x_internal + ' + type + '] is not an array');
    345                 return;
    346             }
    347
    348             i = Type.indexOf(owner['x_internal' + type], fn, 'origin');
    349
    350             if (i === -1) {
    351                 JXG.debug('no such event function in internal list: ' + fn);
    352                 return;
    353             }
    354
    355             try {
    356                 // Non-IE browser
    357                 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) {
    358                     obj.removeEventListener(type, owner['x_internal' + type][i], false);
    359                 }
    360
    361                 // IE
    362                 if (Type.exists(obj) && Type.exists(obj.detachEvent)) {
    363                     obj.detachEvent('on' + type, owner['x_internal' + type][i]);
    364                 }
    365             } catch (e) {
    366                 JXG.debug('event not registered in browser: (' + type + ' -- ' + fn + ')');
    367             }
    368
    369             owner['x_internal' + type].splice(i, 1);
    370         },
    371
    372         /**
    373          * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div
    374          * of a {@link JXG.Board} because this might corrupt the event handling system.
    375          * @param {Object} obj Reference to a DOM node.
    376          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
    377          * @param {Object} owner The scope in which the event trigger is called.
    378          */
    379         removeAllEvents: function (obj, type, owner) {
    380             var i, len;
    381             if (owner['x_internal' + type]) {
    382                 len = owner['x_internal' + type].length;
    383
    384                 for (i = len - 1; i >= 0; i--) {
    385                     JXG.removeEvent(obj, type, owner['x_internal' + type][i].origin, owner);
    386                 }
    387
    388                 if (owner['x_internal' + type].length > 0) {
    389                     JXG.debug('removeAllEvents: Not all events could be removed.');
    390                 }
    391             }
    392         },
    393
    394         /**
    395          * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner.
    396          * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used.
    397          * @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.
    398          * @param {Object} [doc] The document object.
    399          * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component.
    400          */
    401         getPosition: function (e, index, doc) {
    402             var i, len, evtTouches,
    403                 posx = 0,
    404                 posy = 0;
    405
    406             if (!e) {
    407                 e = window.event;
    408             }
    409
    410             doc = doc || document;
    411             evtTouches = e[JXG.touchProperty];
    412
    413             // touchend events have their position in "changedTouches"
    414             if (Type.exists(evtTouches) && evtTouches.length === 0) {
    415                 evtTouches = e.changedTouches;
    416             }
    417
    418             if (Type.exists(index) && Type.exists(evtTouches)) {
    419                 if (index === -1) {
    420                     len = evtTouches.length;
    421
    422                     for (i = 0; i < len; i++) {
    423                         if (evtTouches[i]) {
    424                             e = evtTouches[i];
    425                             break;
    426                         }
    427                     }
    428
    429                 } else {
    430                     e = evtTouches[index];
    431                 }
    432             }
    433
    434             // Scrolling is ignored.
    435             // e.clientX is supported since IE6
    436             if (e.clientX) {
    437                 posx = e.clientX;
    438                 posy = e.clientY;
    439             }
    440
    441             return [posx, posy];
    442         },
    443
    444         /**
    445          * Calculates recursively the offset of the DOM element in which the board is stored.
    446          * @param {Object} obj A DOM element
    447          * @returns {Array} An array with the elements left and top offset.
    448          */
    449         getOffset: function (obj) {
    450             var cPos,
    451                 o = obj,
    452                 o2 = obj,
    453                 l = o.offsetLeft - o.scrollLeft,
    454                 t = o.offsetTop - o.scrollTop;
    455
    456             cPos = this.getCSSTransform([l, t], o);
    457             l = cPos[0];
    458             t = cPos[1];
    459
    460             /*
    461              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
    462              * if not to the body. In IE and if we are in an position:absolute environment
    463              * offsetParent walks up the DOM hierarchy.
    464              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
    465              * we need the parentNode steps.
    466              */
    467             o = o.offsetParent;
    468             while (o) {
    469                 l += o.offsetLeft;
    470                 t += o.offsetTop;
    471
    472                 if (o.offsetParent) {
    473                     l += o.clientLeft - o.scrollLeft;
    474                     t += o.clientTop - o.scrollTop;
    475                 }
    476
    477                 cPos = this.getCSSTransform([l, t], o);
    478                 l = cPos[0];
    479                 t = cPos[1];
    480
    481                 o2 = o2.parentNode;
    482
    483                 while (o2 !== o) {
    484                     l += o2.clientLeft - o2.scrollLeft;
    485                     t += o2.clientTop - o2.scrollTop;
    486
    487                     cPos = this.getCSSTransform([l, t], o2);
    488                     l = cPos[0];
    489                     t = cPos[1];
    490
    491                     o2 = o2.parentNode;
    492                 }
    493                 o = o.offsetParent;
    494             }
    495
    496             return [l, t];
    497         },
    498
    499         /**
    500          * Access CSS style sheets.
    501          * @param {Object} obj A DOM element
    502          * @param {String} stylename The CSS property to read.
    503          * @returns The value of the CSS property and <tt>undefined</tt> if it is not set.
    504          */
    505         getStyle: function (obj, stylename) {
    506             var r,
    507                 doc = obj.ownerDocument;
    508
    509             // Non-IE
    510             if (doc.defaultView && doc.defaultView.getComputedStyle) {
    511                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
    512             // IE
    513             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
    514                 r = obj.currentStyle[stylename];
    515             } else {
    516                 if (obj.style) {
    517                     // make stylename lower camelcase
    518                     stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) {
    519                         return letter.toUpperCase();
    520                     });
    521                     r = obj.style[stylename];
    522                 }
    523             }
    524
    525             return r;
    526         },
    527
    528         /**
    529          * Reads css style sheets of a given element. This method is a getStyle wrapper and
    530          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
    531          * @param {DOMElement} el
    532          * @param {string} css
    533          * @returns {number}
    534          */
    535         getProp: function (el, css) {
    536             var n = parseInt(this.getStyle(el, css), 10);
    537             return isNaN(n) ? 0 : n;
    538         },
    539
    540         /**
    541          * Correct position of upper left corner in case of
    542          * a CSS transformation. Here, only translations are
    543          * extracted. All scaling transformations are corrected
    544          * in {@link JXG.Board#getMousePosition}.
    545          * @param {Array} cPos Previously determined position
    546          * @param {Object} obj A DOM element
    547          * @returns {Array} The corrected position.
    548          */
    549         getCSSTransform: function (cPos, obj) {
    550             var i, j, str, arrStr, start, len, len2, arr,
    551                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'];
    552
    553             // Take the first transformation matrix
    554             len = t.length;
    555
    556             for (i = 0, str = ''; i < len; i++) {
    557                 if (Type.exists(obj.style[t[i]])) {
    558                     str = obj.style[t[i]];
    559                     break;
    560                 }
    561             }
    562
    563             /**
    564              * Extract the coordinates and apply the transformation
    565              * to cPos
    566              */
    567             if (str !== '') {
    568                 start = str.indexOf('(');
    569
    570                 if (start > 0) {
    571                     len = str.length;
    572                     arrStr = str.substring(start + 1, len - 1);
    573                     arr = arrStr.split(',');
    574
    575                     for (j = 0, len2 = arr.length; j < len2; j++) {
    576                         arr[j] = parseFloat(arr[j]);
    577                     }
    578
    579                     if (str.indexOf('matrix') === 0) {
    580                         cPos[0] += arr[4];
    581                         cPos[1] += arr[5];
    582                     } else if (str.indexOf('translateX') === 0) {
    583                         cPos[0] += arr[0];
    584                     } else if (str.indexOf('translateY') === 0) {
    585                         cPos[1] += arr[0];
    586                     } else if (str.indexOf('translate') === 0) {
    587                         cPos[0] += arr[0];
    588                         cPos[1] += arr[1];
    589                     }
    590                 }
    591             }
    592
    593             // Zoom is used by reveal.js
    594             if (Type.exists(obj.style.zoom)) {
    595                 str = obj.style.zoom;
    596                 if (str !== '') {
    597                     cPos[0] *= parseFloat(str);
    598                     cPos[1] *= parseFloat(str);
    599                 }
    600             }
    601
    602             return cPos;
    603         },
    604
    605         /**
    606          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
    607          * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported.
    608          * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}.
    609          */
    610         getCSSTransformMatrix: function (obj) {
    611             var i, j, str, arrstr, start, len, len2, arr,
    612                 st,
    613                 doc = obj.ownerDocument,
    614                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'],
    615                 mat = [[1, 0, 0],
    616                     [0, 1, 0],
    617                     [0, 0, 1]];
    618
    619             // This should work on all browsers except IE 6-8
    620             if (doc.defaultView && doc.defaultView.getComputedStyle) {
    621                 st = doc.defaultView.getComputedStyle(obj, null);
    622                 str = st.getPropertyValue("-webkit-transform") ||
    623                      st.getPropertyValue("-moz-transform") ||
    624                      st.getPropertyValue("-ms-transform") ||
    625                      st.getPropertyValue("-o-transform") ||
    626                      st.getPropertyValue("transform");
    627             } else {
    628                 // Take the first transformation matrix
    629                 len = t.length;
    630                 for (i = 0, str = ''; i < len; i++) {
    631                     if (Type.exists(obj.style[t[i]])) {
    632                         str = obj.style[t[i]];
    633                         break;
    634                     }
    635                 }
    636             }
    637
    638             if (str !== '') {
    639                 start = str.indexOf('(');
    640
    641                 if (start > 0) {
    642                     len = str.length;
    643                     arrstr = str.substring(start + 1, len - 1);
    644                     arr = arrstr.split(',');
    645
    646                     for (j = 0, len2 = arr.length; j < len2; j++) {
    647                         arr[j] = parseFloat(arr[j]);
    648                     }
    649
    650                     if (str.indexOf('matrix') === 0) {
    651                         mat = [[1, 0, 0],
    652                             [0, arr[0], arr[1]],
    653                             [0, arr[2], arr[3]]];
    654                     } else if (str.indexOf('scaleX') === 0) {
    655                         mat[1][1] = arr[0];
    656                     } else if (str.indexOf('scaleY') === 0) {
    657                         mat[2][2] = arr[0];
    658                     } else if (str.indexOf('scale') === 0) {
    659                         mat[1][1] = arr[0];
    660                         mat[2][2] = arr[1];
    661                     }
    662                 }
    663             }
    664
    665             // CSS style zoom is used by reveal.js
    666             // Recursively search for zoom style entries.
    667             // This is necessary for reveal.js on webkit.
    668             // It fails if the user does zooming
    669             if (Type.exists(obj.style.zoom)) {
    670                 str = obj.style.zoom;
    671                 if (str !== '') {
    672                     mat[1][1] *= parseFloat(str);
    673                     mat[2][2] *= parseFloat(str);
    674                 }
    675             }
    676
    677             return mat;
    678         },
    679
    680         /**
    681          * Process data in timed chunks. Data which takes long to process, either because it is such
    682          * a huge amount of data or the processing takes some time, causes warnings in browsers about
    683          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
    684          * called chunks which will be processed in serial order.
    685          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
    686          * @param {Array} items to do
    687          * @param {Function} process Function that is applied for every array item
    688          * @param {Object} context The scope of function process
    689          * @param {Function} callback This function is called after the last array element has been processed.
    690          */
    691         timedChunk: function (items, process, context, callback) {
    692             //create a clone of the original
    693             var todo = items.concat(),
    694                 timerFun = function () {
    695                     var start = +new Date();
    696
    697                     do {
    698                         process.call(context, todo.shift());
    699                     } while (todo.length > 0 && (+new Date() - start < 300));
    700
    701                     if (todo.length > 0) {
    702                         window.setTimeout(timerFun, 1);
    703                     } else {
    704                         callback(items);
    705                     }
    706                 };
    707
    708             window.setTimeout(timerFun, 1);
    709         }
    710     });
    711
    712     return JXG;
    713 });
    714