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