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