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