1 /* 2 Copyright 2008-2013 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 && document.documentElement.hasOwnProperty('ontouchstart'); 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.appName && window.clientInformation.appName.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) { 232 var element, display, els, originalVisibility, originalPosition, 233 originalDisplay, originalWidth, originalHeight; 234 235 if (!JXG.isBrowser || elementId === null) { 236 return { 237 width: 500, 238 height: 500 239 }; 240 } 241 242 // Borrowed from prototype.js 243 element = document.getElementById(elementId); 244 if (!Type.exists(element)) { 245 throw new Error("\nJSXGraph: HTML container element '" + elementId + "' not found."); 246 } 247 248 display = element.style.display; 249 250 // Work around a bug in Safari 251 if (display !== 'none' && display !== null) { 252 return {width: element.offsetWidth, height: element.offsetHeight}; 253 } 254 255 // All *Width and *Height properties give 0 on elements with display set to none, 256 // hence we show the element temporarily 257 els = element.style; 258 259 // save style 260 originalVisibility = els.visibility; 261 originalPosition = els.position; 262 originalDisplay = els.display; 263 264 // show element 265 els.visibility = 'hidden'; 266 els.position = 'absolute'; 267 els.display = 'block'; 268 269 // read the dimension 270 originalWidth = element.clientWidth; 271 originalHeight = element.clientHeight; 272 273 // restore original css values 274 els.display = originalDisplay; 275 els.position = originalPosition; 276 els.visibility = originalVisibility; 277 278 return { 279 width: originalWidth, 280 height: originalHeight 281 }; 282 }, 283 284 /** 285 * Adds an event listener to a DOM element. 286 * @param {Object} obj Reference to a DOM node. 287 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 288 * @param {Function} fn The function to call when the event is triggered. 289 * @param {Object} owner The scope in which the event trigger is called. 290 */ 291 addEvent: function (obj, type, fn, owner) { 292 var el = function () { 293 return fn.apply(owner, arguments); 294 }; 295 296 el.origin = fn; 297 owner['x_internal' + type] = owner['x_internal' + type] || []; 298 owner['x_internal' + type].push(el); 299 300 // Non-IE browser 301 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 302 obj.addEventListener(type, el, false); 303 } 304 305 // IE 306 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 307 obj.attachEvent('on' + type, el); 308 } 309 }, 310 311 /** 312 * Removes an event listener from a DOM element. 313 * @param {Object} obj Reference to a DOM node. 314 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 315 * @param {Function} fn The function to call when the event is triggered. 316 * @param {Object} owner The scope in which the event trigger is called. 317 */ 318 removeEvent: function (obj, type, fn, owner) { 319 var i; 320 321 if (!Type.exists(owner)) { 322 JXG.debug('no such owner'); 323 return; 324 } 325 326 if (!Type.exists(owner['x_internal' + type])) { 327 JXG.debug('no such type: ' + type); 328 return; 329 } 330 331 if (!Type.isArray(owner['x_internal' + type])) { 332 JXG.debug('owner[x_internal + ' + type + '] is not an array'); 333 return; 334 } 335 336 i = Type.indexOf(owner['x_internal' + type], fn, 'origin'); 337 338 if (i === -1) { 339 JXG.debug('no such event function in internal list: ' + fn); 340 return; 341 } 342 343 try { 344 // Non-IE browser 345 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 346 obj.removeEventListener(type, owner['x_internal' + type][i], false); 347 } 348 349 // IE 350 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 351 obj.detachEvent('on' + type, owner['x_internal' + type][i]); 352 } 353 } catch (e) { 354 JXG.debug('event not registered in browser: (' + type + ' -- ' + fn + ')'); 355 } 356 357 owner['x_internal' + type].splice(i, 1); 358 }, 359 360 /** 361 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 362 * of a {@link JXG.Board} because this might corrupt the event handling system. 363 * @param {Object} obj Reference to a DOM node. 364 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 365 * @param {Object} owner The scope in which the event trigger is called. 366 */ 367 removeAllEvents: function (obj, type, owner) { 368 var i, len; 369 if (owner['x_internal' + type]) { 370 len = owner['x_internal' + type].length; 371 372 for (i = len - 1; i >= 0; i--) { 373 JXG.removeEvent(obj, type, owner['x_internal' + type][i].origin, owner); 374 } 375 376 if (owner['x_internal' + type].length > 0) { 377 JXG.debug('removeAllEvents: Not all events could be removed.'); 378 } 379 } 380 }, 381 382 /** 383 * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner. 384 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 385 * @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. 386 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 387 */ 388 getPosition: function (e, index) { 389 var i, len, evtTouches, 390 posx = 0, 391 posy = 0; 392 393 if (!e) { 394 e = window.event; 395 } 396 397 evtTouches = e[JXG.touchProperty]; 398 399 if (Type.exists(index)) { 400 if (index === -1) { 401 len = evtTouches.length; 402 403 for (i = 0; i < len; i++) { 404 if (evtTouches[i]) { 405 e = evtTouches[i]; 406 break; 407 } 408 } 409 } else { 410 e = evtTouches[index]; 411 } 412 } 413 414 if (e.pageX || e.pageY) { 415 posx = e.pageX; 416 posy = e.pageY; 417 } else if (e.clientX || e.clientY) { 418 posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 419 posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 420 } 421 422 return [posx, posy]; 423 }, 424 425 /** 426 * Calculates recursively the offset of the DOM element in which the board is stored. 427 * @param {Object} obj A DOM element 428 * @returns {Array} An array with the elements left and top offset. 429 */ 430 getOffset: function (obj) { 431 var cPos, 432 o = obj, 433 o2 = obj, 434 l = o.offsetLeft - o.scrollLeft, 435 t = o.offsetTop - o.scrollTop; 436 437 cPos = this.getCSSTransform([l, t], o); 438 l = cPos[0]; 439 t = cPos[1]; 440 441 /* 442 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 443 * if not to the body. In IE and if we are in an position:absolute environment 444 * offsetParent walks up the DOM hierarchy. 445 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 446 * we need the parentNode steps. 447 */ 448 o = o.offsetParent; 449 while (o) { 450 l += o.offsetLeft; 451 t += o.offsetTop; 452 453 if (o.offsetParent) { 454 l += o.clientLeft - o.scrollLeft; 455 t += o.clientTop - o.scrollTop; 456 } 457 458 cPos = this.getCSSTransform([l, t], o); 459 l = cPos[0]; 460 t = cPos[1]; 461 462 o2 = o2.parentNode; 463 464 while (o2 !== o) { 465 l += o2.clientLeft - o2.scrollLeft; 466 t += o2.clientTop - o2.scrollTop; 467 468 cPos = this.getCSSTransform([l, t], o2); 469 l = cPos[0]; 470 t = cPos[1]; 471 472 o2 = o2.parentNode; 473 } 474 o = o.offsetParent; 475 } 476 return [l, t]; 477 }, 478 479 /** 480 * Access CSS style sheets. 481 * @param {Object} obj A DOM element 482 * @param {String} stylename The CSS property to read. 483 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 484 */ 485 getStyle: function (obj, stylename) { 486 var r; 487 488 // Non-IE 489 if (window.getComputedStyle) { 490 r = document.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 491 // IE 492 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 493 r = obj.currentStyle[stylename]; 494 } else { 495 if (obj.style) { 496 // make stylename lower camelcase 497 stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) { 498 return letter.toUpperCase(); 499 }); 500 r = obj.style[stylename]; 501 } 502 } 503 504 return r; 505 }, 506 507 /** 508 * Reads css style sheets of a given element. This method is a getStyle wrapper and 509 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 510 * @param {DOMElement} el 511 * @param {string} css 512 * @returns {number} 513 */ 514 getProp: function (el, css) { 515 var n = parseInt(this.getStyle(el, css), 10); 516 return isNaN(n) ? 0 : n; 517 }, 518 519 /** 520 * Correct position of upper left corner in case of 521 * a CSS transformation. Here, only translations are 522 * extracted. All scaling transformations are corrected 523 * in {@link JXG.Board#getMousePosition}. 524 * @param {Array} cPos Previously determined position 525 * @param {Object} obj A DOM element 526 * @returns {Array} The corrected position. 527 */ 528 getCSSTransform: function (cPos, obj) { 529 var i, j, str, arrStr, start, len, len2, arr, 530 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform']; 531 532 // Take the first transformation matrix 533 len = t.length; 534 535 for (i = 0, str = ''; i < len; i++) { 536 if (Type.exists(obj.style[t[i]])) { 537 str = obj.style[t[i]]; 538 break; 539 } 540 } 541 542 /** 543 * Extract the coordinates and apply the transformation 544 * to cPos 545 */ 546 if (str !== '') { 547 start = str.indexOf('('); 548 549 if (start > 0) { 550 len = str.length; 551 arrStr = str.substring(start + 1, len - 1); 552 arr = arrStr.split(','); 553 554 for (j = 0, len2 = arr.length; j < len2; j++) { 555 arr[j] = parseFloat(arr[j]); 556 } 557 558 if (str.indexOf('matrix') === 0) { 559 cPos[0] += arr[4]; 560 cPos[1] += arr[5]; 561 } else if (str.indexOf('translateX') === 0) { 562 cPos[0] += arr[0]; 563 } else if (str.indexOf('translateY') === 0) { 564 cPos[1] += arr[0]; 565 } else if (str.indexOf('translate') === 0) { 566 cPos[0] += arr[0]; 567 cPos[1] += arr[1]; 568 } 569 } 570 } 571 return cPos; 572 }, 573 574 /** 575 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 576 * are determined. Not implemented are 'rotate', 'skew', 'skewX', 'skewY'. 577 * @returns {Array} 3x3 transformation matrix. See {@link JXG.Board#updateCSSTransforms}. 578 */ 579 getCSSTransformMatrix: function (obj) { 580 var i, j, str, arrstr, start, len, len2, arr, 581 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'], 582 mat = [[1, 0, 0], 583 [0, 1, 0], 584 [0, 0, 1]]; 585 586 // Take the first transformation matrix 587 len = t.length; 588 for (i = 0, str = ''; i < len; i++) { 589 if (Type.exists(obj.style[t[i]])) { 590 str = obj.style[t[i]]; 591 break; 592 } 593 } 594 595 if (str !== '') { 596 start = str.indexOf('('); 597 598 if (start > 0) { 599 len = str.length; 600 arrstr = str.substring(start + 1, len - 1); 601 arr = arrstr.split(','); 602 603 for (j = 0, len2 = arr.length; j < len2; j++) { 604 arr[j] = parseFloat(arr[j]); 605 } 606 607 if (str.indexOf('matrix') === 0) { 608 mat = [[1, 0, 0], 609 [0, arr[0], arr[1]], 610 [0, arr[2], arr[3]]]; 611 // Missing are rotate, skew, skewX, skewY 612 } else if (str.indexOf('scaleX') === 0) { 613 mat[1][1] = arr[0]; 614 } else if (str.indexOf('scaleY') === 0) { 615 mat[2][2] = arr[0]; 616 } else if (str.indexOf('scale') === 0) { 617 mat[1][1] = arr[0]; 618 mat[2][2] = arr[1]; 619 } 620 } 621 } 622 return mat; 623 }, 624 625 /** 626 * Process data in timed chunks. Data which takes long to process, either because it is such 627 * a huge amount of data or the processing takes some time, causes warnings in browsers about 628 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 629 * called chunks which will be processed in serial order. 630 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 631 * @param {Array} items to do 632 * @param {Function} process Function that is applied for every array item 633 * @param {Object} context The scope of function process 634 * @param {Function} callback This function is called after the last array element has been processed. 635 */ 636 timedChunk: function (items, process, context, callback) { 637 //create a clone of the original 638 var todo = items.concat(), 639 timerFun = function () { 640 var start = +new Date(); 641 642 do { 643 process.call(context, todo.shift()); 644 } while (todo.length > 0 && (+new Date() - start < 300)); 645 646 if (todo.length > 0) { 647 window.setTimeout(timerFun, 1); 648 } else { 649 callback(items); 650 } 651 }; 652 653 window.setTimeout(timerFun, 1); 654 } 655 }); 656 657 return JXG; 658 }); 659