      1 /*
      2     Copyright 2008-2015
      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,
     89                 hasCanvas = false;
     90
     91             if (this.isNode()) {
     92                 try {
     93                     c = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
     94                     hasCanvas = true;
     95                 } catch (err) { }
     96             }
     97
     98             return hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext);
     99         },
    100
    101         /**
    102          * True, if run inside a node.js environment.
    103          * @returns {Boolean}
    104          */
    105         isNode: function () {
    106             // this is not a 100% sure but should be valid in most cases
    107
    108                 // we are not inside a browser
    109             return !this.isBrowser && (
    110                 // there is a module object (plain node, no requirejs)
    111                 (typeof module === 'object' && !!module.exports) ||
    112                 // there is a global object and requirejs is loaded
    113                 (typeof global === 'object' && global.requirejsVars && !global.requirejsVars.isBrowser)
    114             );
    115         },
    116
    117         /**
    118          * True if run inside a webworker environment.
    119          * @returns {Boolean}
    120          */
    121         isWebWorker: function () {
    122             return !this.isBrowser && (typeof self === 'object' && typeof self.postMessage === 'function');
    123         },
    124
    125         /**
    126          * Checks if the environments supports the W3C Pointer Events API {@link http://www.w3.org/Submission/pointer-events/}
    127          * @return {Boolean}
    128          */
    129         supportsPointerEvents: function () {
    130             return JXG.isBrowser && window.navigator && (window.navigator.msPointerEnabled || window.navigator.pointerEnabled);
    131         },
    132
    133         /**
    134          * Determine if the current browser supports touch events
    135          * @returns {Boolean} True, if the browser supports touch events.
    136          */
    137         isTouchDevice: function () {
    138             return this.isBrowser && window.ontouchstart !== undefined;
    139         },
    140
    141         /**
    142          * Detects if the user is using an Android powered device.
    143          * @returns {Boolean}
    144          */
    145         isAndroid: function () {
    146             return Type.exists(navigator) && navigator.userAgent.toLowerCase().indexOf('android') > -1;
    147         },
    148
    149         /**
    150          * Detects if the user is using the default Webkit browser on an Android powered device.
    151          * @returns {Boolean}
    152          */
    153         isWebkitAndroid: function () {
    154             return this.isAndroid() && navigator.userAgent.indexOf(' AppleWebKit/') > -1;
    155         },
    156
    157         /**
    158          * Detects if the user is using a Apple iPad / iPhone.
    159          * @returns {Boolean}
    160          */
    161         isApple: function () {
    162             return Type.exists(navigator) && (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPhone') > -1);
    163         },
    164
    165         /**
    166          * Detects if the user is using Safari on an Apple device.
    167          * @returns {Boolean}
    168          */
    169         isWebkitApple: function () {
    170             return this.isApple() && (navigator.userAgent.search(/Mobile\/[0-9A-Za-z\.]*Safari/) > -1);
    171         },
    172
    173         /**
    174          * Returns true if the run inside a Windows 8 "Metro" App.
    175          * @return {Boolean}
    176          */
    177         isMetroApp: function () {
    178             return typeof window === 'object' && window.clientInformation && window.clientInformation.appVersion && window.clientInformation.appVersion.indexOf('MSAppHost') > -1;
    179         },
    180
    181         /**
    182          * Detects if the user is using a Mozilla browser
    183          * @returns {Boolean}
    184          */
    185         isMozilla: function () {
    186             return Type.exists(navigator) &&
    187                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 &&
    188                 navigator.userAgent.toLowerCase().indexOf('apple') === -1;
    189         },
    190
    191         /**
    192          * Detects if the user is using a firefoxOS powered device.
    193          * @returns {Boolean}
    194          */
    195         isFirefoxOS: function () {
    196             return Type.exists(navigator) &&
    197                 navigator.userAgent.toLowerCase().indexOf('android') === -1 &&
    198                 navigator.userAgent.toLowerCase().indexOf('apple') === -1 &&
    199                 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 &&
    200                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1;
    201         },
    202
    203         /**
    204          * Internet Explorer version. Works only for IE > 4.
    205          * @type Number
    206          */
    207         ieVersion: (function () {
    208             var undef, div, all,
    209                 v = 3;
    210
    211             if (typeof document !== 'object') {
    212                 return 0;
    213             }
    214
    215             div = document.createElement('div');
    216             all = div.getElementsByTagName('i');
    217
    218             do {
    219                 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><' + 'i><' + '/i><![endif]-->';
    220             } while (all[0]);
    221
    222             return v > 4 ? v : undef;
    223
    224         }()),
    225
    226         /**
    227          * Reads the width and height of an HTML element.
    228          * @param {String} elementId The HTML id of an HTML DOM node.
    229          * @returns {Object} An object with the two properties width and height.
    230          */
    231         getDimensions: function (elementId, doc) {
    232             var element, display, els, originalVisibility, originalPosition,
    233                 originalDisplay, originalWidth, originalHeight, style,
    234                 pixelDimRegExp = /\d+(\.\d*)?px/;
    235
    236             if (!JXG.isBrowser || elementId === null) {
    237                 return {
    238                     width: 500,
    239                     height: 500
    240                 };
    241             }
    242
    243             doc = doc || document;
    244             // Borrowed from prototype.js
    245             element = doc.getElementById(elementId);
    246             if (!Type.exists(element)) {
    247                 throw new Error("\nJSXGraph: HTML container element '" + elementId + "' not found.");
    248             }
    249
    250             display = element.style.display;
    251
    252             // Work around a bug in Safari
    253             if (display !== 'none' && display !== null) {
    254                 if (element.offsetWidth > 0 && element.offsetHeight > 0) {
    255                     return {width: element.offsetWidth, height: element.offsetHeight};
    256                 } else { // a parent might be set to display:none; try reading them from styles
    257                     style = window.getComputedStyle ? window.getComputedStyle(element) : element.style;
    258                     return {
    259                         width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0,
    260                         height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0
    261                     };
    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             if (e.pageX || e.pageY) {
    433                 posx = e.pageX;
    434                 posy = e.pageY;
    435             } else if (e.clientX || e.clientY) {
    436                 posx = e.clientX + doc.body.scrollLeft + doc.documentElement.scrollLeft;
    437                 posy = e.clientY + doc.body.scrollTop + doc.documentElement.scrollTop;
    438             }
    439
    440             return [posx, posy];
    441         },
    442
    443         /**
    444          * Calculates recursively the offset of the DOM element in which the board is stored.
    445          * @param {Object} obj A DOM element
    446          * @returns {Array} An array with the elements left and top offset.
    447          */
    448         getOffset: function (obj) {
    449             var cPos,
    450                 o = obj,
    451                 o2 = obj,
    452                 l = o.offsetLeft - o.scrollLeft,
    453                 t = o.offsetTop - o.scrollTop;
    454
    455             cPos = this.getCSSTransform([l, t], o);
    456             l = cPos[0];
    457             t = cPos[1];
    458
    459             /*
    460              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
    461              * if not to the body. In IE and if we are in an position:absolute environment
    462              * offsetParent walks up the DOM hierarchy.
    463              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
    464              * we need the parentNode steps.
    465              */
    466             o = o.offsetParent;
    467             while (o) {
    468                 l += o.offsetLeft;
    469                 t += o.offsetTop;
    470
    471                 if (o.offsetParent) {
    472                     l += o.clientLeft - o.scrollLeft;
    473                     t += o.clientTop - o.scrollTop;
    474                 }
    475
    476                 cPos = this.getCSSTransform([l, t], o);
    477                 l = cPos[0];
    478                 t = cPos[1];
    479
    480                 o2 = o2.parentNode;
    481
    482                 while (o2 !== o) {
    483                     l += o2.clientLeft - o2.scrollLeft;
    484                     t += o2.clientTop - o2.scrollTop;
    485
    486                     cPos = this.getCSSTransform([l, t], o2);
    487                     l = cPos[0];
    488                     t = cPos[1];
    489
    490                     o2 = o2.parentNode;
    491                 }
    492                 o = o.offsetParent;
    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, doc;
    505
    506             doc = obj.ownerDocument;
    507
    508             // Non-IE
    509             if (window.getComputedStyle) {
    510                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
    511             // IE
    512             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
    513                 r = obj.currentStyle[stylename];
    514             } else {
    515                 if (obj.style) {
    516                     // make stylename lower camelcase
    517                     stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) {
    518                         return letter.toUpperCase();
    519                     });
    520                     r = obj.style[stylename];
    521                 }
    522             }
    523
    524             return r;
    525         },
    526
    527         /**
    528          * Reads css style sheets of a given element. This method is a getStyle wrapper and
    529          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
    530          * @param {DOMElement} el
    531          * @param {string} css
    532          * @returns {number}
    533          */
    534         getProp: function (el, css) {
    535             var n = parseInt(this.getStyle(el, css), 10);
    536             return isNaN(n) ? 0 : n;
    537         },
    538
    539         /**
    540          * Correct position of upper left corner in case of
    541          * a CSS transformation. Here, only translations are
    542          * extracted. All scaling transformations are corrected
    543          * in {@link JXG.Board#getMousePosition}.
    544          * @param {Array} cPos Previously determined position
    545          * @param {Object} obj A DOM element
    546          * @returns {Array} The corrected position.
    547          */
    548         getCSSTransform: function (cPos, obj) {
    549             var i, j, str, arrStr, start, len, len2, arr,
    550                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'];
    551
    552             // Take the first transformation matrix
    553             len = t.length;
    554
    555             for (i = 0, str = ''; i < len; i++) {
    556                 if (Type.exists(obj.style[t[i]])) {
    557                     str = obj.style[t[i]];
    558                     break;
    559                 }
    560             }
    561
    562             /**
    563              * Extract the coordinates and apply the transformation
    564              * to cPos
    565              */
    566             if (str !== '') {
    567                 start = str.indexOf('(');
    568
    569                 if (start > 0) {
    570                     len = str.length;
    571                     arrStr = str.substring(start + 1, len - 1);
    572                     arr = arrStr.split(',');
    573
    574                     for (j = 0, len2 = arr.length; j < len2; j++) {
    575                         arr[j] = parseFloat(arr[j]);
    576                     }
    577
    578                     if (str.indexOf('matrix') === 0) {
    579                         cPos[0] += arr[4];
    580                         cPos[1] += arr[5];
    581                     } else if (str.indexOf('translateX') === 0) {
    582                         cPos[0] += arr[0];
    583                     } else if (str.indexOf('translateY') === 0) {
    584                         cPos[1] += arr[0];
    585                     } else if (str.indexOf('translate') === 0) {
    586                         cPos[0] += arr[0];
    587                         cPos[1] += arr[1];
    588                     }
    589                 }
    590             }
    591             return cPos;
    592         },
    593
    594         /**
    595          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
    596          * are determined. Not implemented are 'rotate', 'skew', 'skewX', 'skewY'.
    597          * @returns {Array} 3x3 transformation matrix. See {@link JXG.Board#updateCSSTransforms}.
    598          */
    599         getCSSTransformMatrix: function (obj) {
    600             var i, j, str, arrstr, start, len, len2, arr,
    601                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'],
    602                 mat = [[1, 0, 0],
    603                     [0, 1, 0],
    604                     [0, 0, 1]];
    605
    606             // Take the first transformation matrix
    607             len = t.length;
    608             for (i = 0, str = ''; i < len; i++) {
    609                 if (Type.exists(obj.style[t[i]])) {
    610                     str = obj.style[t[i]];
    611                     break;
    612                 }
    613             }
    614
    615             if (str !== '') {
    616                 start = str.indexOf('(');
    617
    618                 if (start > 0) {
    619                     len = str.length;
    620                     arrstr = str.substring(start + 1, len - 1);
    621                     arr = arrstr.split(',');
    622
    623                     for (j = 0, len2 = arr.length; j < len2; j++) {
    624                         arr[j] = parseFloat(arr[j]);
    625                     }
    626
    627                     if (str.indexOf('matrix') === 0) {
    628                         mat = [[1, 0, 0],
    629                             [0, arr[0], arr[1]],
    630                             [0, arr[2], arr[3]]];
    631                         // Missing are rotate, skew, skewX, skewY
    632                     } else if (str.indexOf('scaleX') === 0) {
    633                         mat[1][1] = arr[0];
    634                     } else if (str.indexOf('scaleY') === 0) {
    635                         mat[2][2] = arr[0];
    636                     } else if (str.indexOf('scale') === 0) {
    637                         mat[1][1] = arr[0];
    638                         mat[2][2] = arr[1];
    639                     }
    640                 }
    641             }
    642             return mat;
    643         },
    644
    645         /**
    646          * Process data in timed chunks. Data which takes long to process, either because it is such
    647          * a huge amount of data or the processing takes some time, causes warnings in browsers about
    648          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
    649          * called chunks which will be processed in serial order.
    650          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
    651          * @param {Array} items to do
    652          * @param {Function} process Function that is applied for every array item
    653          * @param {Object} context The scope of function process
    654          * @param {Function} callback This function is called after the last array element has been processed.
    655          */
    656         timedChunk: function (items, process, context, callback) {
    657             //create a clone of the original
    658             var todo = items.concat(),
    659                 timerFun = function () {
    660                     var start = +new Date();
    661
    662                     do {
    663                         process.call(context, todo.shift());
    664                     } while (todo.length > 0 && (+new Date() - start < 300));
    665
    666                     if (todo.length > 0) {
    667                         window.setTimeout(timerFun, 1);
    668                     } else {
    669                         callback(items);
    670                     }
    671                 };
    672
    673             window.setTimeout(timerFun, 1);
    674         }
    675     });
    676
    677     return JXG;
    678 });
    679