1 /* 2 Copyright 2008-2023 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"; 43 import Type from "./type"; 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[JXG.touchProperty]); 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[JXG.touchProperty].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";\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 */ 265 isAndroid: function () { 266 return ( 267 Type.exists(navigator) && 268 navigator.userAgent.toLowerCase().indexOf("android") > -1 269 ); 270 }, 271 272 /** 273 * Detects if the user is using the default Webkit browser on an Android powered device. 274 * @returns {Boolean} 275 */ 276 isWebkitAndroid: function () { 277 return this.isAndroid() && navigator.userAgent.indexOf(" AppleWebKit/") > -1; 278 }, 279 280 /** 281 * Detects if the user is using a Apple iPad / iPhone. 282 * @returns {Boolean} 283 */ 284 isApple: function () { 285 return ( 286 Type.exists(navigator) && 287 (navigator.userAgent.indexOf("iPad") > -1 || 288 navigator.userAgent.indexOf("iPhone") > -1) 289 ); 290 }, 291 292 /** 293 * Detects if the user is using Safari on an Apple device. 294 * @returns {Boolean} 295 */ 296 isWebkitApple: function () { 297 return ( 298 this.isApple() && navigator.userAgent.search(/Mobile\/[0-9A-Za-z.]*Safari/) > -1 299 ); 300 }, 301 302 /** 303 * Returns true if the run inside a Windows 8 "Metro" App. 304 * @returns {Boolean} 305 */ 306 isMetroApp: function () { 307 return ( 308 typeof window === "object" && 309 window.clientInformation && 310 window.clientInformation.appVersion && 311 window.clientInformation.appVersion.indexOf("MSAppHost") > -1 312 ); 313 }, 314 315 /** 316 * Detects if the user is using a Mozilla browser 317 * @returns {Boolean} 318 */ 319 isMozilla: function () { 320 return ( 321 Type.exists(navigator) && 322 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1 && 323 navigator.userAgent.toLowerCase().indexOf("apple") === -1 324 ); 325 }, 326 327 /** 328 * Detects if the user is using a firefoxOS powered device. 329 * @returns {Boolean} 330 */ 331 isFirefoxOS: function () { 332 return ( 333 Type.exists(navigator) && 334 navigator.userAgent.toLowerCase().indexOf("android") === -1 && 335 navigator.userAgent.toLowerCase().indexOf("apple") === -1 && 336 navigator.userAgent.toLowerCase().indexOf("mobile") > -1 && 337 navigator.userAgent.toLowerCase().indexOf("mozilla") > -1 338 ); 339 }, 340 341 /** 342 * Detects if the user is using a desktop device. 343 * @returns {boolean} 344 * 345 * @see https://stackoverflow.com/a/61073480 346 */ 347 isDesktop: function () { 348 return true; 349 // console.log("isDesktop", screen.orientation); 350 // const navigatorAgent = 351 // navigator.userAgent || navigator.vendor || window.opera; 352 // return !( 353 // /(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( 354 // navigatorAgent 355 // ) || 356 // /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( 357 // navigatorAgent.substr(0, 4) 358 // ) 359 // ); 360 }, 361 362 /** 363 * Detects if the user is using a mobile device. 364 * @returns {boolean} 365 * 366 * @see https://stackoverflow.com/questions/25542814/html5-detecting-if-youre-on-mobile-or-pc-with-javascript 367 */ 368 isMobile: function () { 369 return true; 370 // return Type.exists(navigator) && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 371 }, 372 373 /** 374 * Internet Explorer version. Works only for IE > 4. 375 * @type Number 376 */ 377 ieVersion: (function () { 378 var div, 379 all, 380 v = 3; 381 382 if (typeof document !== "object") { 383 return 0; 384 } 385 386 div = document.createElement("div"); 387 all = div.getElementsByTagName("i"); 388 389 do { 390 div.innerHTML = "<!--[if gt IE " + ++v + "]><" + "i><" + "/i><![endif]-->"; 391 } while (all[0]); 392 393 return v > 4 ? v : undefined; 394 })(), 395 396 /** 397 * Reads the width and height of an HTML element. 398 * @param {String|Object} elementId id of or reference to an HTML DOM node. 399 * @returns {Object} An object with the two properties width and height. 400 */ 401 getDimensions: function (elementId, doc) { 402 var element, 403 display, 404 els, 405 originalVisibility, 406 originalPosition, 407 originalDisplay, 408 originalWidth, 409 originalHeight, 410 style, 411 pixelDimRegExp = /\d+(\.\d*)?px/; 412 413 if (!this.isBrowser || elementId === null) { 414 return { 415 width: 500, 416 height: 500 417 }; 418 } 419 420 doc = doc || document; 421 // Borrowed from prototype.js 422 element = (Type.isString(elementId)) ? doc.getElementById(elementId) : elementId; 423 if (!Type.exists(element)) { 424 throw new Error( 425 "\nJSXGraph: HTML container element '" + elementId + "' not found." 426 ); 427 } 428 429 display = element.style.display; 430 431 // Work around a bug in Safari 432 if (display !== "none" && display !== null) { 433 if (element.clientWidth > 0 && element.clientHeight > 0) { 434 return { width: element.clientWidth, height: element.clientHeight }; 435 } 436 437 // A parent might be set to display:none; try reading them from styles 438 style = window.getComputedStyle ? window.getComputedStyle(element) : element.style; 439 return { 440 width: pixelDimRegExp.test(style.width) ? parseFloat(style.width) : 0, 441 height: pixelDimRegExp.test(style.height) ? parseFloat(style.height) : 0 442 }; 443 } 444 445 // All *Width and *Height properties give 0 on elements with display set to none, 446 // hence we show the element temporarily 447 els = element.style; 448 449 // save style 450 originalVisibility = els.visibility; 451 originalPosition = els.position; 452 originalDisplay = els.display; 453 454 // show element 455 els.visibility = "hidden"; 456 els.position = "absolute"; 457 els.display = "block"; 458 459 // read the dimension 460 originalWidth = element.clientWidth; 461 originalHeight = element.clientHeight; 462 463 // restore original css values 464 els.display = originalDisplay; 465 els.position = originalPosition; 466 els.visibility = originalVisibility; 467 468 return { 469 width: originalWidth, 470 height: originalHeight 471 }; 472 }, 473 474 /** 475 * Adds an event listener to a DOM element. 476 * @param {Object} obj Reference to a DOM node. 477 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 478 * @param {Function} fn The function to call when the event is triggered. 479 * @param {Object} owner The scope in which the event trigger is called. 480 * @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 481 * an options object or the useCapture Boolean. 482 * 483 */ 484 addEvent: function (obj, type, fn, owner, options) { 485 var el = function () { 486 return fn.apply(owner, arguments); 487 }; 488 489 el.origin = fn; 490 // Check if owner is a board 491 if (typeof owner === 'object' && Type.exists(owner.BOARD_MODE_NONE)) { 492 owner['x_internal' + type] = owner['x_internal' + type] || []; 493 owner['x_internal' + type].push(el); 494 } 495 496 // Non-IE browser 497 if (Type.exists(obj) && Type.exists(obj.addEventListener)) { 498 options = options || false; // options or useCapture 499 obj.addEventListener(type, el, options); 500 } 501 502 // IE 503 if (Type.exists(obj) && Type.exists(obj.attachEvent)) { 504 obj.attachEvent("on" + type, el); 505 } 506 }, 507 508 /** 509 * Removes an event listener from a DOM element. 510 * @param {Object} obj Reference to a DOM node. 511 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 512 * @param {Function} fn The function to call when the event is triggered. 513 * @param {Object} owner The scope in which the event trigger is called. 514 */ 515 removeEvent: function (obj, type, fn, owner) { 516 var i; 517 518 if (!Type.exists(owner)) { 519 JXG.debug("no such owner"); 520 return; 521 } 522 523 if (!Type.exists(owner["x_internal" + type])) { 524 JXG.debug("no such type: " + type); 525 return; 526 } 527 528 if (!Type.isArray(owner["x_internal" + type])) { 529 JXG.debug("owner[x_internal + " + type + "] is not an array"); 530 return; 531 } 532 533 i = Type.indexOf(owner["x_internal" + type], fn, "origin"); 534 535 if (i === -1) { 536 JXG.debug("removeEvent: no such event function in internal list: " + fn); 537 return; 538 } 539 540 try { 541 // Non-IE browser 542 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) { 543 obj.removeEventListener(type, owner["x_internal" + type][i], false); 544 } 545 546 // IE 547 if (Type.exists(obj) && Type.exists(obj.detachEvent)) { 548 obj.detachEvent("on" + type, owner["x_internal" + type][i]); 549 } 550 } catch (e) { 551 JXG.debug("event not registered in browser: (" + type + " -- " + fn + ")"); 552 } 553 554 owner["x_internal" + type].splice(i, 1); 555 }, 556 557 /** 558 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 559 * of a {@link JXG.Board} because this might corrupt the event handling system. 560 * @param {Object} obj Reference to a DOM node. 561 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 562 * @param {Object} owner The scope in which the event trigger is called. 563 */ 564 removeAllEvents: function (obj, type, owner) { 565 var i, len; 566 if (owner["x_internal" + type]) { 567 len = owner["x_internal" + type].length; 568 569 for (i = len - 1; i >= 0; i--) { 570 JXG.removeEvent(obj, type, owner["x_internal" + type][i].origin, owner); 571 } 572 573 if (owner["x_internal" + type].length > 0) { 574 JXG.debug("removeAllEvents: Not all events could be removed."); 575 } 576 } 577 }, 578 579 /** 580 * Cross browser mouse / pointer / touch coordinates retrieval relative to the documents's top left corner. 581 * This method might be a bit outdated today, since pointer events and clientX/Y are omnipresent. 582 * 583 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 584 * @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. 585 * @param {Object} [doc] The document object. 586 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 587 */ 588 getPosition: function (e, index, doc) { 589 var i, 590 len, 591 evtTouches, 592 posx = 0, 593 posy = 0; 594 595 if (!e) { 596 e = window.event; 597 } 598 599 doc = doc || document; 600 evtTouches = e[JXG.touchProperty]; 601 602 // touchend events have their position in "changedTouches" 603 if (Type.exists(evtTouches) && evtTouches.length === 0) { 604 evtTouches = e.changedTouches; 605 } 606 607 if (Type.exists(index) && Type.exists(evtTouches)) { 608 if (index === -1) { 609 len = evtTouches.length; 610 611 for (i = 0; i < len; i++) { 612 if (evtTouches[i]) { 613 e = evtTouches[i]; 614 break; 615 } 616 } 617 } else { 618 e = evtTouches[index]; 619 } 620 } 621 622 // Scrolling is ignored. 623 // e.clientX is supported since IE6 624 if (e.clientX) { 625 posx = e.clientX; 626 posy = e.clientY; 627 } 628 629 return [posx, posy]; 630 }, 631 632 /** 633 * Calculates recursively the offset of the DOM element in which the board is stored. 634 * @param {Object} obj A DOM element 635 * @returns {Array} An array with the elements left and top offset. 636 */ 637 getOffset: function (obj) { 638 var cPos, 639 o = obj, 640 o2 = obj, 641 l = o.offsetLeft - o.scrollLeft, 642 t = o.offsetTop - o.scrollTop; 643 644 cPos = this.getCSSTransform([l, t], o); 645 l = cPos[0]; 646 t = cPos[1]; 647 648 /* 649 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 650 * if not to the body. In IE and if we are in an position:absolute environment 651 * offsetParent walks up the DOM hierarchy. 652 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 653 * we need the parentNode steps. 654 */ 655 o = o.offsetParent; 656 while (o) { 657 l += o.offsetLeft; 658 t += o.offsetTop; 659 660 if (o.offsetParent) { 661 l += o.clientLeft - o.scrollLeft; 662 t += o.clientTop - o.scrollTop; 663 } 664 665 cPos = this.getCSSTransform([l, t], o); 666 l = cPos[0]; 667 t = cPos[1]; 668 669 o2 = o2.parentNode; 670 671 while (o2 !== o) { 672 l += o2.clientLeft - o2.scrollLeft; 673 t += o2.clientTop - o2.scrollTop; 674 675 cPos = this.getCSSTransform([l, t], o2); 676 l = cPos[0]; 677 t = cPos[1]; 678 679 o2 = o2.parentNode; 680 } 681 o = o.offsetParent; 682 } 683 684 return [l, t]; 685 }, 686 687 /** 688 * Access CSS style sheets. 689 * @param {Object} obj A DOM element 690 * @param {String} stylename The CSS property to read. 691 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 692 */ 693 getStyle: function (obj, stylename) { 694 var r, 695 doc = obj.ownerDocument; 696 697 // Non-IE 698 if (doc.defaultView && doc.defaultView.getComputedStyle) { 699 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 700 // IE 701 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 702 r = obj.currentStyle[stylename]; 703 } else { 704 if (obj.style) { 705 // make stylename lower camelcase 706 stylename = stylename.replace(/-([a-z]|[0-9])/gi, function (all, letter) { 707 return letter.toUpperCase(); 708 }); 709 r = obj.style[stylename]; 710 } 711 } 712 713 return r; 714 }, 715 716 /** 717 * Reads css style sheets of a given element. This method is a getStyle wrapper and 718 * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value. 719 * @param {DOMElement} el 720 * @param {string} css 721 * @returns {number} 722 */ 723 getProp: function (el, css) { 724 var n = parseInt(this.getStyle(el, css), 10); 725 return isNaN(n) ? 0 : n; 726 }, 727 728 /** 729 * Correct position of upper left corner in case of 730 * a CSS transformation. Here, only translations are 731 * extracted. All scaling transformations are corrected 732 * in {@link JXG.Board#getMousePosition}. 733 * @param {Array} cPos Previously determined position 734 * @param {Object} obj A DOM element 735 * @returns {Array} The corrected position. 736 */ 737 getCSSTransform: function (cPos, obj) { 738 var i, 739 j, 740 str, 741 arrStr, 742 start, 743 len, 744 len2, 745 arr, 746 t = [ 747 "transform", 748 "webkitTransform", 749 "MozTransform", 750 "msTransform", 751 "oTransform" 752 ]; 753 754 // Take the first transformation matrix 755 len = t.length; 756 757 for (i = 0, str = ""; i < len; i++) { 758 if (Type.exists(obj.style[t[i]])) { 759 str = obj.style[t[i]]; 760 break; 761 } 762 } 763 764 /** 765 * Extract the coordinates and apply the transformation 766 * to cPos 767 */ 768 if (str !== "") { 769 start = str.indexOf("("); 770 771 if (start > 0) { 772 len = str.length; 773 arrStr = str.substring(start + 1, len - 1); 774 arr = arrStr.split(","); 775 776 for (j = 0, len2 = arr.length; j < len2; j++) { 777 arr[j] = parseFloat(arr[j]); 778 } 779 780 if (str.indexOf("matrix") === 0) { 781 cPos[0] += arr[4]; 782 cPos[1] += arr[5]; 783 } else if (str.indexOf("translateX") === 0) { 784 cPos[0] += arr[0]; 785 } else if (str.indexOf("translateY") === 0) { 786 cPos[1] += arr[0]; 787 } else if (str.indexOf("translate") === 0) { 788 cPos[0] += arr[0]; 789 cPos[1] += arr[1]; 790 } 791 } 792 } 793 794 // Zoom is used by reveal.js 795 if (Type.exists(obj.style.zoom)) { 796 str = obj.style.zoom; 797 if (str !== "") { 798 cPos[0] *= parseFloat(str); 799 cPos[1] *= parseFloat(str); 800 } 801 } 802 803 return cPos; 804 }, 805 806 /** 807 * Scaling CSS transformations applied to the div element containing the JSXGraph constructions 808 * are determined. In IE prior to 9, 'rotate', 'skew', 'skewX', 'skewY' are not supported. 809 * @returns {Array} 3x3 transformation matrix without translation part. See {@link JXG.Board#updateCSSTransforms}. 810 */ 811 getCSSTransformMatrix: function (obj) { 812 var i, j, str, arrstr, arr, 813 start, len, len2, st, 814 doc = obj.ownerDocument, 815 t = [ 816 "transform", 817 "webkitTransform", 818 "MozTransform", 819 "msTransform", 820 "oTransform" 821 ], 822 mat = [ 823 [1, 0, 0], 824 [0, 1, 0], 825 [0, 0, 1] 826 ]; 827 828 // This should work on all browsers except IE 6-8 829 if (doc.defaultView && doc.defaultView.getComputedStyle) { 830 st = doc.defaultView.getComputedStyle(obj, null); 831 str = 832 st.getPropertyValue("-webkit-transform") || 833 st.getPropertyValue("-moz-transform") || 834 st.getPropertyValue("-ms-transform") || 835 st.getPropertyValue("-o-transform") || 836 st.getPropertyValue("transform"); 837 } else { 838 // Take the first transformation matrix 839 len = t.length; 840 for (i = 0, str = ""; i < len; i++) { 841 if (Type.exists(obj.style[t[i]])) { 842 str = obj.style[t[i]]; 843 break; 844 } 845 } 846 } 847 848 // Convert and reorder the matrix for JSXGraph 849 if (str !== "") { 850 start = str.indexOf("("); 851 852 if (start > 0) { 853 len = str.length; 854 arrstr = str.substring(start + 1, len - 1); 855 arr = arrstr.split(","); 856 857 for (j = 0, len2 = arr.length; j < len2; j++) { 858 arr[j] = parseFloat(arr[j]); 859 } 860 861 if (str.indexOf("matrix") === 0) { 862 mat = [ 863 [1, 0, 0], 864 [0, arr[0], arr[1]], 865 [0, arr[2], arr[3]] 866 ]; 867 } else if (str.indexOf("scaleX") === 0) { 868 mat[1][1] = arr[0]; 869 } else if (str.indexOf("scaleY") === 0) { 870 mat[2][2] = arr[0]; 871 } else if (str.indexOf("scale") === 0) { 872 mat[1][1] = arr[0]; 873 mat[2][2] = arr[1]; 874 } 875 } 876 } 877 878 // CSS style zoom is used by reveal.js 879 // Recursively search for zoom style entries. 880 // This is necessary for reveal.js on webkit. 881 // It fails if the user does zooming 882 if (Type.exists(obj.style.zoom)) { 883 str = obj.style.zoom; 884 if (str !== "") { 885 mat[1][1] *= parseFloat(str); 886 mat[2][2] *= parseFloat(str); 887 } 888 } 889 890 return mat; 891 }, 892 893 /** 894 * Process data in timed chunks. Data which takes long to process, either because it is such 895 * a huge amount of data or the processing takes some time, causes warnings in browsers about 896 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 897 * called chunks which will be processed in serial order. 898 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 899 * @param {Array} items to do 900 * @param {Function} process Function that is applied for every array item 901 * @param {Object} context The scope of function process 902 * @param {Function} callback This function is called after the last array element has been processed. 903 */ 904 timedChunk: function (items, process, context, callback) { 905 //create a clone of the original 906 var todo = items.concat(), 907 timerFun = function () { 908 var start = +new Date(); 909 910 do { 911 process.call(context, todo.shift()); 912 } while (todo.length > 0 && +new Date() - start < 300); 913 914 if (todo.length > 0) { 915 window.setTimeout(timerFun, 1); 916 } else { 917 callback(items); 918 } 919 }; 920 921 window.setTimeout(timerFun, 1); 922 }, 923 924 /** 925 * Scale and vertically shift a DOM element (usually a JSXGraph div) 926 * inside of a parent DOM 927 * element which is set to fullscreen. 928 * This is realized with a CSS transformation. 929 * 930 * @param {String} wrap_id id of the parent DOM element which is in fullscreen mode 931 * @param {String} inner_id id of the DOM element which is scaled and shifted 932 * @param {Object} doc document object or shadow root 933 * @param {Number} scale Relative size of the JSXGraph board in the fullscreen window. 934 * 935 * @private 936 * @see JXG.Board#toFullscreen 937 * @see JXG.Board#fullscreenListener 938 * 939 */ 940 scaleJSXGraphDiv: function (wrap_id, inner_id, doc, scale) { 941 var w, h, b, 942 wi, hi, 943 wo, ho, inner, 944 scale_l, vshift_l, 945 f = scale, 946 ratio, 947 pseudo_keys = [ 948 ":fullscreen", 949 ":-webkit-full-screen", 950 ":-moz-full-screen", 951 ":-ms-fullscreen" 952 ], 953 len_pseudo = pseudo_keys.length, 954 i; 955 956 b = doc.getElementById(wrap_id).getBoundingClientRect(); 957 h = b.height; 958 w = b.width; 959 960 inner = doc.getElementById(inner_id); 961 wo = inner._cssFullscreenStore.w; 962 ho = inner._cssFullscreenStore.h; 963 ratio = ho / wo; 964 965 // Scale the div such that fits into the fullscreen. 966 if (wo > w * f) { 967 wo = w * f; 968 ho = wo * ratio; 969 } 970 if (ho > h * f) { 971 ho = h * f; 972 wo = ho / ratio; 973 } 974 975 wi = wo; 976 hi = ho; 977 // Compare the code in this.setBoundingBox() 978 if (ratio > 1) { 979 // h > w 980 if (ratio < h / w) { 981 scale_l = w * f / wo; 982 } else { 983 scale_l = h * f / ho; 984 } 985 } else { 986 // h <= w 987 if (ratio < h / w) { 988 scale_l = w * f / wo; 989 } else { 990 scale_l = h * f / ho; 991 } 992 } 993 vshift_l = (h - hi) * 0.5; 994 995 // Set a CSS properties to center the JSXGraph div horizontally and vertically 996 // at the first position of the fullscreen pseudo classes. 997 for (i = 0; i < len_pseudo; i++) { 998 try { 999 inner.style.width = wi + 'px !important'; 1000 inner.style.height = hi + 'px !important'; 1001 inner.style.margin = '0 auto'; 1002 // Add the transform to a possibly already existing transform 1003 inner.style.transform = inner._cssFullscreenStore.transform + 1004 ' matrix(' + scale_l + ',0,0,' + scale_l + ',0,' + vshift_l + ')'; 1005 break; 1006 } catch (err) { 1007 JXG.debug("JXG.scaleJSXGraphDiv:\n" + err); 1008 } 1009 } 1010 if (i === len_pseudo) { 1011 JXG.debug("JXG.scaleJSXGraphDiv: Could not set any CSS property."); 1012 } 1013 } 1014 1015 } 1016 ); 1017 1018 export default JXG; 1019