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