1 /* 2 Copyright 2008-2024 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Andreas Walter, 8 Alfred Wassermann, 9 Peter Wilfahrt 10 11 This file is part of JSXGraph. 12 13 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 14 15 You can redistribute it and/or modify it under the terms of the 16 17 * GNU Lesser General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version 20 OR 21 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 22 23 JSXGraph is distributed in the hope that it will be useful, 24 but WITHOUT ANY WARRANTY; without even the implied warranty of 25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 GNU Lesser General Public License for more details. 27 28 You should have received a copy of the GNU Lesser General Public License and 29 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 30 and <https://opensource.org/licenses/MIT/>. 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 /** 37 * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish 38 * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device 39 * the browser runs on is a tablet/cell or a desktop computer. 40 */ 41 42 import JXG from "../jxg.js"; 43 import Type from "./type.js"; 44 45 JXG.extendConstants( 46 JXG, 47 /** @lends JXG */ { 48 // /** 49 // * Determines the property that stores the relevant information in the event object. 50 // * @type String 51 // * @default 'touches' 52 // * @private 53 // */ 54 // touchProperty: "touches" 55 } 56 ); 57 58 JXG.extend( 59 JXG, 60 /** @lends JXG */ { 61 /** 62 * Determines whether evt is a touch event. 63 * @param evt {Event} 64 * @returns {Boolean} 65 */ 66 isTouchEvent: function (evt) { 67 return JXG.exists(evt['touches']); // Old iOS touch events 68 }, 69 70 /** 71 * Determines whether evt is a pointer event. 72 * @param evt {Event} 73 * @returns {Boolean} 74 */ 75 isPointerEvent: function (evt) { 76 return JXG.exists(evt.pointerId); 77 }, 78 79 /** 80 * Determines whether evt is neither a touch event nor a pointer event. 81 * @param evt {Event} 82 * @returns {Boolean} 83 */ 84 isMouseEvent: function (evt) { 85 return !JXG.isTouchEvent(evt) && !JXG.isPointerEvent(evt); 86 }, 87 88 /** 89 * Determines the number of touch points in a touch event. 90 * For other events, -1 is returned. 91 * @param evt {Event} 92 * @returns {Number} 93 */ 94 getNumberOfTouchPoints: function (evt) { 95 var n = -1; 96 97 if (JXG.isTouchEvent(evt)) { 98 n = evt['touches'].length; 99 } 100 101 return n; 102 }, 103 104 /** 105 * Checks whether an mouse, pointer or touch event evt is the first event of a multitouch event. 106 * Attention: When two or more pointer device types are being used concurrently, 107 * it is only checked whether the passed event is the first one of its type! 108 * @param evt {Event} 109 * @returns {boolean} 110 */ 111 isFirstTouch: function (evt) { 112 var touchPoints = JXG.getNumberOfTouchPoints(evt); 113 114 if (JXG.isPointerEvent(evt)) { 115 return evt.isPrimary; 116 } 117 118 return touchPoints === 1; 119 }, 120 121 /** 122 * A document/window environment is available. 123 * @type Boolean 124 * @default false 125 */ 126 isBrowser: typeof window === "object" && typeof document === "object", 127 128 /** 129 * Features of ECMAScript 6+ are available. 130 * @type Boolean 131 * @default false 132 */ 133 supportsES6: function () { 134 // var testMap; 135 /* jshint ignore:start */ 136 try { 137 // This would kill the old uglifyjs: testMap = (a = 0) => a; 138 new Function("(a = 0) => a"); 139 return true; 140 } catch (err) { 141 return false; 142 } 143 /* jshint ignore:end */ 144 }, 145 146 /** 147 * Detect browser support for VML. 148 * @returns {Boolean} True, if the browser supports VML. 149 */ 150 supportsVML: function () { 151 // From stackoverflow.com 152 return this.isBrowser && !!document.namespaces; 153 }, 154 155 /** 156 * Detect browser support for SVG. 157 * @returns {Boolean} True, if the browser supports SVG. 158 */ 159 supportsSVG: function () { 160 var svgSupport; 161 if (!this.isBrowser) { 162 return false; 163 } 164 svgSupport = !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect; 165 return svgSupport; 166 }, 167 168 /** 169 * Detect browser support for Canvas. 170 * @returns {Boolean} True, if the browser supports HTML canvas. 171 */ 172 supportsCanvas: function () { 173 var hasCanvas = false; 174 175 // if (this.isNode()) { 176 // try { 177 // // c = typeof module === "object" ? module.require("canvas") : $__canvas; 178 // c = typeof module === "object" ? module.require("canvas") : import('canvas'); 179 // hasCanvas = !!c; 180 // } catch (err) {} 181 // } 182 183 if (this.isNode()) { 184 //try { 185 // JXG.createCanvas(500, 500); 186 hasCanvas = true; 187 // } catch (err) { 188 // throw new Error('JXG.createCanvas not available.\n' + 189 // 'Install the npm package `canvas`\n' + 190 // 'and call:\n' + 191 // ' import { createCanvas } from "canvas.js";\n' + 192 // ' JXG.createCanvas = createCanvas;\n'); 193 // } 194 } 195 196 return ( 197 hasCanvas || (this.isBrowser && !!document.createElement("canvas").getContext) 198 ); 199 }, 200 201 /** 202 * True, if run inside a node.js environment. 203 * @returns {Boolean} 204 */ 205 isNode: function () { 206 // This is not a 100% sure but should be valid in most cases 207 // We are not inside a browser 208 /* eslint-disable no-undef */ 209 return ( 210 !this.isBrowser && 211 (typeof process !== 'undefined') && 212 (process.release.name.search(/node|io.js/) !== -1) 213 /* eslint-enable no-undef */ 214 215 // there is a module object (plain node, no requirejs) 216 // ((typeof module === "object" && !!module.exports) || 217 // // there is a global object and requirejs is loaded 218 // (typeof global === "object" && 219 // global.requirejsVars && 220 // !global.requirejsVars.isBrowser) 221 // ) 222 ); 223 }, 224 225 /** 226 * True if run inside a webworker environment. 227 * @returns {Boolean} 228 */ 229 isWebWorker: function () { 230 return ( 231 !this.isBrowser && 232 typeof self === "object" && 233 typeof self.postMessage === "function" 234 ); 235 }, 236 237 /** 238 * Checks if the environments supports the W3C Pointer Events API {@link https://www.w3.org/TR/pointerevents/} 239 * @returns {Boolean} 240 */ 241 supportsPointerEvents: function () { 242 return !!( 243 ( 244 this.isBrowser && 245 window.navigator && 246 (window.PointerEvent || // Chrome/Edge/IE11+ 247 window.navigator.pointerEnabled || // IE11+ 248 window.navigator.msPointerEnabled) 249 ) // IE10- 250 ); 251 }, 252 253 /** 254 * Determine if the current browser supports touch events 255 * @returns {Boolean} True, if the browser supports touch events. 256 */ 257 isTouchDevice: function () { 258 return this.isBrowser && window.ontouchstart !== undefined; 259 }, 260 261 /** 262 * Detects if the user is using an Android powered device. 263 * @returns {Boolean} 264 * @deprecated 265 */ 266 isAndroid: function () { 267 return ( 268 Type.exists(navigator) && 269 navigator.userAgent.toLowerCase().indexOf("android") > -1 270 ); 271 }, 272 273 /** 274 * Detects if the user is using the default Webkit browser on an Android powered device. 275 * @returns {Boolean} 276 * @deprecated 277 */ 278 isWebkitAndroid: function () { 279 return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1; 280 }, 281 282 /** 283 * Detects if the user is using a Apple iPad / iPhone. 284 * @returns {Boolean} 285 * @deprecated 286 */ 287 isApple: function () { 288 return ( 289 Type.exists(navigator) && 290 (navigator.userAgent.indexOf("iPad") > -1 || 291 navigator.userAgent.indexOf("iPhone") > -1) 292 ); 293 }, 294 295 /** 296 * Detects if the user is using Safari on an Apple device. 297 * @returns {Boolean} 298 * @deprecated 299 */ 300 isWebkitApple: function () { 301 return ( 302 this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1 303 ); 304 }, 305 306 /** 307 * Returns true if the run inside a Windows 8 "Metro" App. 308 * @returns {Boolean} 309 * @deprecated 310 */ 311 isMetroApp: function () { 312 return ( 313 typeof window === "object" && 314 window.clientInformation && 315 window.clientInformation.appVersion && 316 window.clientInformation.appVersion.indexOf("MSAppHost") > -1 317 ); 318 }, 319 320 /** 321 * Detects if the user is using a Mozilla browser 322 * @returns {Boolean} 323 * @deprecated 324 */ 325 isMozilla: function () { 326 return ( 327 Type.exists(navigator) && 328 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1 && 329 navigator.userAgent.toLowerCase().indexOf("apple") === -1 330 ); 331 }, 332 333 /** 334 * Detects if the user is using a firefoxOS powered device. 335 * @returns {Boolean} 336 * @deprecated 337 */ 338 isFirefoxOS: function () { 339 return ( 340 Type.exists(navigator) && 341 navigator.userAgent.toLowerCase().indexOf("android") === -1 && 342 navigator.userAgent.toLowerCase().indexOf("apple") === -1 && 343 navigator.userAgent.toLowerCase().indexOf("mobile") > -1 && 344 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1 345 ); 346 }, 347 348 /** 349 * Detects if the user is using a desktop device. 350 * @returns {boolean} 351 * 352 * @see https://stackoverflow.com/a/61073480 353 * @deprecated 354 */ 355 isDesktop: function () { 356 return true; 357 // console.log("isDesktop", screen.orientation); 358 // const navigatorAgent = 359 // navigator.userAgent || navigator.vendor || window.opera; 360 // return !( 361 // /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( 362 // navigatorAgent 363 // ) || 364 // /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( 365 // navigatorAgent.substr(0, 4) 366 // ) 367 // ); 368 }, 369 370 /** 371 * Detects if the user is using a mobile device. 372 * @returns {boolean} 373 * 374 * @see https://stackoverflow.com/questions/25542814/html5-detecting-if-youre-on-mobile-or-pc-with-javascript 375 * @deprecated 376 * 377 */ 378 isMobile: function () { 379 return true; 380 // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 381 }, 382 383 /** 384 * Internet Explorer version. Works only for IE > 4. 385 * @type Number 386 * @deprecated 387 */ 388 ieVersion: (function () { 389 var div, 390 all, 391 v = 3; 392 393 if (typeof document !== "object") { 394 return 0; 395 } 396 397 div = document.createElement("div"); 398 all = div.getElementsByTagName("i"); 399 400 do { 401 div.innerHTML = "<!--[if gt IE " + ++v + "]><" + "i><" + "/i><![endif]-->"; 402 } while (all[0]); 403 404 return v > 4 ? v : undefined; 405 })(), 406 407 /** 408 * Reads the width and height of an HTML element. 409 * @param {String|Object} elementId id of or reference to an HTML DOM node. 410 * @returns {Object} An object with the two properties width and height. 411 */ 412 getDimensions: function (elementId, doc) { 413 var element, 414 display, 415 els, 416 originalVisibility, 417 originalPosition, 418 originalDisplay, 419 originalWidth, 420 originalHeight, 421 style, 422 pixelDimRegExp = /\d+(\.\d*)?px/; 423 424 if (!this.isBrowser || elementId === null) { 425 return { 426 width: 500, 427 height: 500 428 }; 429 } 430 431 doc = doc || document; 432 // Borrowed from prototype.js 433 element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId; 434 if (!Type.exists(element)) { 435 throw new Error( 436 "\nJSXGraph: HTML container element '" + elementId + "' not found." 437 ); 438 } 439 440 display = element.style.display; 441 442 // Work around a bug in Safari 443 if (display !== "none" && display !== null) { 444 if (element.clientWidth > 0 && element.clientHeight > 0) { 445 return { width: element.clientWidth, height: element.clientHeight }; 446 } 447 448 // A parent might be set to display:none; try reading them from styles 449 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style; 450 return { 451 width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0, 452 height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0 453 }; 454 } 455 456 // All *Width and *Height properties give 0 on elements with display set to none, 457 // hence we show the element temporarily 458 els = element.style; 459 460 // save style 461 originalVisibility = els.visibility; 462 originalPosition = els.position; 463 originalDisplay = els.display; 464 465 // show element 466 els.visibility = "hidden"; 467 els.position = "absolute"; 468 els.display = "block"; 469 470 // read the dimension 471 originalWidth = element.clientWidth; 472 originalHeight = element.clientHeight; 473 474 // restore original css values 475 els.display = originalDisplay; 476 els.position = originalPosition; 477 els.visibility = originalVisibility; 478 479 return { 480 width: originalWidth, 481 height: originalHeight 482 }; 483 }, 484 485 /** 486 * Adds an event listener to a DOM element. 487 * @param {Object} obj Reference to a DOM node. 488 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 489 * @param {Function} fn The function to call when the event is triggered. 490 * @param {Object} owner The scope in which the event trigger is called. 491 * @param {Object|Boolean} [options=false] This parameter is passed as the third parameter to the method addEventListener. Depending on the data type it is either 492 * an options object or the useCapture Boolean. 493 * 494 */ 495 addEvent: function (obj, type, fn, owner, options) { 496 var el = function () { 497 return fn.apply(owner, arguments); 498 }; 499 500 el.origin = fn; 501 // Check if owner is a board 502 if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) { 503 owner['x_internal' + type] = owner['x_internal' + type] || []; 504 owner['x_internal' + type].push(el); 505 } 506 507 // Non-IE browser 508 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 509 options = options || false; // options or useCapture 510 obj.addEventListener(type, el, options); 511 } 512 513 // IE 514 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 515 obj.attachEvent("on" + type, el); 516 } 517 }, 518 519 /** 520 * Removes an event listener from a DOM element. 521 * @param {Object} obj Reference to a DOM node. 522 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 523 * @param {Function} fn The function to call when the event is triggered. 524 * @param {Object} owner The scope in which the event trigger is called. 525 */ 526 removeEvent: function (obj, type, fn, owner) { 527 var i; 528 529 if (!Type.exists(owner)) { 530 JXG.debug("no such owner"); 531 return; 532 } 533 534 if (!Type.exists(owner["x_internal" + type])) { 535 JXG.debug("removeEvent: no such type: " + type); 536 return; 537 } 538 539 if (!Type.isArray(owner["x_internal" + type])) { 540 JXG.debug("owner[x_internal + " + type + "] is not an array"); 541 return; 542 } 543 544 i = Type.indexOf(owner["x_internal" + type], fn, "origin"); 545 546 if (i === -1) { 547 JXG.debug("removeEvent: no such event function in internal list: " + fn); 548 return; 549 } 550 551 try { 552 // Non-IE browser 553 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 554 obj.removeEventListener(type, owner["x_internal" + type][i], false); 555 } 556 557 // IE 558 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 559 obj.detachEvent("on" + type, owner["x_internal" + type][i]); 560 } 561 } catch (e) { 562 JXG.debug("removeEvent: event not registered in browser: (" + type + " -- " + fn + ")"); 563 } 564 565 owner["x_internal" + type].splice(i, 1); 566 }, 567 568 /** 569 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 570 * of a {@link JXG.Board} because this might corrupt the event handling system. 571 * @param {Object} obj Reference to a DOM node. 572 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 573 * @param {Object} owner The scope in which the event trigger is called. 574 */ 575 removeAllEvents: function (obj, type, owner) { 576 var i, len; 577 if (owner["x_internal" + type]) { 578 len = owner["x_internal" + type].length; 579 580 for (i = len - 1; i >= 0; i--) { 581 JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner); 582 } 583 584 if (owner["x_internal" + type].length > 0) { 585 JXG.debug("removeAllEvents: Not all events could be removed."); 586 } 587 } 588 }, 589 590 /** 591 * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner. 592 * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent. 593 * 594 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 595 * @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. 596 * @param {Object} [doc] The document object. 597 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 598 */ 599 getPosition: function (e, index, doc) { 600 var i, 601 len, 602 evtTouches, 603 posx = 0, 604 posy = 0; 605 606 if (!e) { 607 e = window.event; 608 } 609 610 doc = doc || document; 611 evtTouches = e['touches']; // iOS touch events 612 613 // touchend events have their position in "changedTouches" 614 if (Type.exists(evtTouches) && evtTouches.length === 0) { 615 evtTouches = e.changedTouches; 616 } 617 618 if (Type.exists(index) && Type.exists(evtTouches)) { 619 if (index === -1) { 620 len = evtTouches.length; 621 622 for (i = 0; i < len; i++) { 623 if (evtTouches[i]) { 624 e = evtTouches[i]; 625 break; 626 } 627 } 628 } else { 629 e = evtTouches[index]; 630 } 631 } 632 633 // Scrolling is ignored. 634 // e.clientX is supported since IE6 635 if (e.clientX) { 636 posx = e.clientX; 637 posy = e.clientY; 638 } 639 640 return [posx, posy]; 641 }, 642 643 /** 644 * Calculates recursively the offset of the DOM element in which the board is stored. 645 * @param {Object} obj A DOM element 646 * @returns {Array} An array with the elements left and top offset. 647 */ 648 getOffset: function (obj) { 649 var cPos, 650 o = obj, 651 o2 = obj, 652 l = o.offsetLeft - o.scrollLeft, 653 t = o.offsetTop - o.scrollTop; 654 655 cPos = this.getCSSTransform([l, t], o); 656 l = cPos[0]; 657 t = cPos[1]; 658 659 /* 660 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 661 * if not to the body. In IE and if we are in an position:absolute environment 662 * offsetParent walks up the DOM hierarchy. 663 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 664 * we need the parentNode steps. 665 */ 666 o = o.offsetParent; 667 while (o) { 668 l += o.offsetLeft; 669 t += o.offsetTop; 670 671 if (o.offsetParent) { 672 l += o.clientLeft - o.scrollLeft; 673 t += o.clientTop - o.scrollTop; 674 } 675 676 cPos = this.getCSSTransform([l, t], o); 677 l = cPos[0]; 678 t = cPos[1]; 679 680 o2 = o2.parentNode; 681 682 while (o2 !== o) { 683 l += o2.clientLeft - o2.scrollLeft; 684 t += o2.clientTop - o2.scrollTop; 685 686 cPos = this.getCSSTransform([l, t], o2); 687 l = cPos[0]; 688 t = cPos[1]; 689 690 o2 = o2.parentNode; 691 } 692 o = o.offsetParent; 693 } 694 695 return [l, t]; 696 }, 697 698 /** 699 * Access CSS style sheets. 700 * @param {Object} obj A DOM element 701 * @param {String} stylename The CSS property to read. 702 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 703 */ 704 getStyle: function (obj, stylename) { 705 var r, 706 doc = obj.ownerDocument; 707 708 // Non-IE 709 if (doc.defaultView && doc.defaultView.getComputedStyle) { 710 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 711 // IE 712 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 713 r = obj.currentStyle[stylename]; 714 } else { 715 if (obj.style) { 716 // make stylename lower camelcase 717 stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) { 718 return letter.toUpperCase(); 719 }); 720 r = obj.style[stylename]; 721 } 722 } 723 724 return r; 725 }, 726 727 /** 728 * Reads css style sheets of a given element. This method is a getStyle wrapper and 729 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 730 * @param {DOMElement} el 731 * @param {string} css 732 * @returns {number} 733 */ 734 getProp: function (el, css) { 735 var n = parseInt(this.getStyle(el, css), 10); 736 return isNaN(n) ? 0 : n; 737 }, 738 739 /** 740 * Correct position of upper left corner in case of 741 * a CSS transformation. Here, only translations are 742 * extracted. All scaling transformations are corrected 743 * in {@link JXG.Board#getMousePosition}. 744 * @param {Array} cPos Previously determined position 745 * @param {Object} obj A DOM element 746 * @returns {Array} The corrected position. 747 */ 748 getCSSTransform: function (cPos, obj) { 749 var i, 750 j, 751 str, 752 arrStr, 753 start, 754 len, 755 len2, 756 arr, 757 t = [ 758 "transform", 759 "webkitTransform", 760 "MozTransform", 761 "msTransform", 762 "oTransform" 763 ]; 764 765 // Take the first transformation matrix 766 len = t.length; 767 768 for (i = 0, str = ""; i < len; i++) { 769 if (Type.exists(obj.style[t[i]])) { 770 str = obj.style[t[i]]; 771 break; 772 } 773 } 774 775 /** 776 * Extract the coordinates and apply the transformation 777 * to cPos 778 */ 779 if (str !== "") { 780 start = str.indexOf("("); 781 782 if (start > 0) { 783 len = str.length; 784 arrStr = str.substring(start + 1, len - 1); 785 arr = arrStr.split(","); 786 787 for (j = 0, len2 = arr.length; j < len2; j++) { 788 arr[j] = parseFloat(arr[j]); 789 } 790 791 if (str.indexOf("matrix") === 0) { 792 cPos[0] += arr[4]; 793 cPos[1] += arr[5]; 794 } else if (str.indexOf("translateX") === 0) { 795 cPos[0] += arr[0]; 796 } else if (str.indexOf("translateY") === 0) { 797 cPos[1] += arr[0]; 798 } else if (str.indexOf("translate") === 0) { 799 cPos[0] += arr[0]; 800 cPos[1] += arr[1]; 801 } 802 } 803 } 804 805 // Zoom is used by reveal.js 806 if (Type.exists(obj.style.zoom)) { 807 str = obj.style.zoom; 808 if (str !== "") { 809 cPos[0] *= parseFloat(str); 810 cPos[1] *= parseFloat(str); 811 } 812 } 813 814 return cPos; 815 }, 816 817 /** 818 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 819 * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported. 820 * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}. 821 */ 822 getCSSTransformMatrix: function (obj) { 823 var i, j, str, arrstr, arr, 824 start, len, len2, st, 825 doc = obj.ownerDocument, 826 t = [ 827 "transform", 828 "webkitTransform", 829 "MozTransform", 830 "msTransform", 831 "oTransform" 832 ], 833 mat = [ 834 [1, 0, 0], 835 [0, 1, 0], 836 [0, 0, 1] 837 ]; 838 839 // This should work on all browsers except IE 6-8 840 if (doc.defaultView && doc.defaultView.getComputedStyle) { 841 st = doc.defaultView.getComputedStyle(obj, null); 842 str = 843 st.getPropertyValue("-webkit-transform") || 844 st.getPropertyValue("-moz-transform") || 845 st.getPropertyValue("-ms-transform") || 846 st.getPropertyValue("-o-transform") || 847 st.getPropertyValue("transform"); 848 } else { 849 // Take the first transformation matrix 850 len = t.length; 851 for (i = 0, str = ""; i < len; i++) { 852 if (Type.exists(obj.style[t[i]])) { 853 str = obj.style[t[i]]; 854 break; 855 } 856 } 857 } 858 859 // Convert and reorder the matrix for JSXGraph 860 if (str !== "") { 861 start = str.indexOf("("); 862 863 if (start > 0) { 864 len = str.length; 865 arrstr = str.substring(start + 1, len - 1); 866 arr = arrstr.split(","); 867 868 for (j = 0, len2 = arr.length; j < len2; j++) { 869 arr[j] = parseFloat(arr[j]); 870 } 871 872 if (str.indexOf("matrix") === 0) { 873 mat = [ 874 [1, 0, 0], 875 [0, arr[0], arr[1]], 876 [0, arr[2], arr[3]] 877 ]; 878 } else if (str.indexOf("scaleX") === 0) { 879 mat[1][1] = arr[0]; 880 } else if (str.indexOf("scaleY") === 0) { 881 mat[2][2] = arr[0]; 882 } else if (str.indexOf("scale") === 0) { 883 mat[1][1] = arr[0]; 884 mat[2][2] = arr[1]; 885 } 886 } 887 } 888 889 // CSS style zoom is used by reveal.js 890 // Recursively search for zoom style entries. 891 // This is necessary for reveal.js on webkit. 892 // It fails if the user does zooming 893 if (Type.exists(obj.style.zoom)) { 894 str = obj.style.zoom; 895 if (str !== "") { 896 mat[1][1] *= parseFloat(str); 897 mat[2][2] *= parseFloat(str); 898 } 899 } 900 901 return mat; 902 }, 903 904 /** 905 * Process data in timed chunks. Data which takes long to process, either because it is such 906 * a huge amount of data or the processing takes some time, causes warnings in browsers about 907 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 908 * called chunks which will be processed in serial order. 909 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 910 * @param {Array} items to do 911 * @param {Function} process Function that is applied for every array item 912 * @param {Object} context The scope of function process 913 * @param {Function} callback This function is called after the last array element has been processed. 914 */ 915 timedChunk: function (items, process, context, callback) { 916 //create a clone of the original 917 var todo = items.slice(), 918 timerFun = function () { 919 var start = +new Date(); 920 921 do { 922 process.call(context, todo.shift()); 923 } while (todo.length > 0 && +new Date() - start < 300); 924 925 if (todo.length > 0) { 926 window.setTimeout(timerFun, 1); 927 } else { 928 callback(items); 929 } 930 }; 931 932 window.setTimeout(timerFun, 1); 933 }, 934 935 /** 936 * Scale and vertically shift a DOM element (usually a JSXGraph div) 937 * inside of a parent DOM 938 * element which is set to fullscreen. 939 * This is realized with a CSS transformation. 940 * 941 * @param {String} wrap_id id of the parent DOM element which is in fullscreen mode 942 * @param {String} inner_id id of the DOM element which is scaled and shifted 943 * @param {Object} doc document object or shadow root 944 * @param {Number} scale Relative size of the JSXGraph board in the fullscreen window. 945 * 946 * @private 947 * @see JXG.Board#toFullscreen 948 * @see JXG.Board#fullscreenListener 949 * 950 */ 951 scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) { 952 var w, h, b, 953 wi, hi, 954 wo, ho, inner, 955 scale_l, vshift_l, 956 f = scale, 957 ratio, 958 pseudo_keys = [ 959 ":fullscreen", 960 ":-webkit-full-screen", 961 ":-moz-full-screen", 962 ":-ms-fullscreen" 963 ], 964 len_pseudo = pseudo_keys.length, 965 i; 966 967 b = doc.getElementById(wrap_id).getBoundingClientRect(); 968 h = b.height; 969 w = b.width; 970 971 inner = doc.getElementById(inner_id); 972 wo = inner._cssFullscreenStore.w; 973 ho = inner._cssFullscreenStore.h; 974 ratio = ho / wo; 975 976 // Scale the div such that fits into the fullscreen. 977 if (wo > w * f) { 978 wo = w * f; 979 ho = wo * ratio; 980 } 981 if (ho > h * f) { 982 ho = h * f; 983 wo = ho / ratio; 984 } 985 986 wi = wo; 987 hi = ho; 988 // Compare the code in this.setBoundingBox() 989 if (ratio > 1) { 990 // h > w 991 if (ratio < h / w) { 992 scale_l = w * f / wo; 993 } else { 994 scale_l = h * f / ho; 995 } 996 } else { 997 // h <= w 998 if (ratio < h / w) { 999 scale_l = w * f / wo; 1000 } else { 1001 scale_l = h * f / ho; 1002 } 1003 } 1004 vshift_l = (h - hi) * 0.5; 1005 1006 // Set a CSS properties to center the JSXGraph div horizontally and vertically 1007 // at the first position of the fullscreen pseudo classes. 1008 for (i = 0; i < len_pseudo; i++) { 1009 try { 1010 inner.style.width = wi + 'px !important'; 1011 inner.style.height = hi + 'px !important'; 1012 inner.style.margin = '0 auto'; 1013 // Add the transform to a possibly already existing transform 1014 inner.style.transform = inner._cssFullscreenStore.transform + 1015 ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')'; 1016 break; 1017 } catch (err) { 1018 JXG.debug("JXG.scaleJSXGraphDiv:\n" + err); 1019 } 1020 } 1021 if (i === len_pseudo) { 1022 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property."); 1023 } 1024 } 1025 1026 } 1027 ); 1028 1029 export default JXG; 1030