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