1 /* 2 Copyright 2008-2023 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 <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true, window: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview In this file the Text element is defined. 37 */ 38 39 import JXG from "../jxg"; 40 import Const from "./constants"; 41 import GeometryElement from "./element"; 42 import GeonextParser from "../parser/geonext"; 43 import Env from "../utils/env"; 44 import Type from "../utils/type"; 45 import Mat from "../math/math"; 46 import CoordsElement from "./coordselement"; 47 48 var priv = { 49 HTMLSliderInputEventHandler: function () { 50 this._val = parseFloat(this.rendNodeRange.value); 51 this.rendNodeOut.value = this.rendNodeRange.value; 52 this.board.update(); 53 } 54 }; 55 56 /** 57 * Construct and handle texts. 58 * 59 * The coordinates can be relative to the coordinates of an element 60 * given in {@link JXG.Options#text.anchor}. 61 * 62 * MathJax, HTML and GEONExT syntax can be handled. 63 * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with 64 * type {@link Text} instead. 65 * @augments JXG.GeometryElement 66 * @augments JXG.CoordsElement 67 * @param {string|JXG.Board} board The board the new text is drawn on. 68 * @param {Array} coordinates An array with the user coordinates of the text. 69 * @param {Object} attributes An object containing visual properties and optional a name and a id. 70 * @param {string|function} content A string or a function returning a string. 71 * 72 */ 73 JXG.Text = function (board, coords, attributes, content) { 74 this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT); 75 76 this.element = this.board.select(attributes.anchor); 77 this.coordsConstructor(coords, Type.evaluate(this.visProp.islabel)); 78 79 this.content = ""; 80 this.plaintext = ""; 81 this.plaintextOld = null; 82 this.orgText = ""; 83 84 this.needsSizeUpdate = false; 85 // Only used by infobox anymore 86 this.hiddenByParent = false; 87 88 /** 89 * Width and height of the text element in pixel. 90 * 91 * @private 92 * @type Array 93 */ 94 this.size = [1.0, 1.0]; 95 this.id = this.board.setId(this, "T"); 96 97 this.board.renderer.drawText(this); 98 this.board.finalizeAdding(this); 99 100 // Set text before drawing 101 // this._createFctUpdateText(content); 102 // this.updateText(); 103 104 this.setText(content); 105 106 if (Type.isString(this.content)) { 107 this.notifyParents(this.content); 108 } 109 this.elType = "text"; 110 111 this.methodMap = Type.deepCopy(this.methodMap, { 112 setText: "setTextJessieCode", 113 // free: 'free', 114 move: "setCoords" 115 }); 116 }; 117 118 JXG.Text.prototype = new GeometryElement(); 119 Type.copyPrototypeMethods(JXG.Text, CoordsElement, "coordsConstructor"); 120 121 JXG.extend( 122 JXG.Text.prototype, 123 /** @lends JXG.Text.prototype */ { 124 /** 125 * @private 126 * Test if the screen coordinates (x,y) are in a small stripe 127 * at the left side or at the right side of the text. 128 * Sensitivity is set in this.board.options.precision.hasPoint. 129 * If dragarea is set to 'all' (default), tests if the screen 130 * coordinates (x,y) are in within the text boundary. 131 * @param {Number} x 132 * @param {Number} y 133 * @returns {Boolean} 134 */ 135 hasPoint: function (x, y) { 136 var lft, rt, top, bot, ax, ay, type, r; 137 138 if (Type.isObject(Type.evaluate(this.visProp.precision))) { 139 type = this.board._inputDevice; 140 r = Type.evaluate(this.visProp.precision[type]); 141 } else { 142 // 'inherit' 143 r = this.board.options.precision.hasPoint; 144 } 145 if (this.transformations.length > 0) { 146 //Transform the mouse/touch coordinates 147 // back to the original position of the text. 148 lft = Mat.matVecMult( 149 Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)), 150 [1, x, y] 151 ); 152 x = lft[1]; 153 y = lft[2]; 154 } 155 156 ax = this.getAnchorX(); 157 if (ax === "right") { 158 lft = this.coords.scrCoords[1] - this.size[0]; 159 } else if (ax === "middle") { 160 lft = this.coords.scrCoords[1] - 0.5 * this.size[0]; 161 } else { 162 lft = this.coords.scrCoords[1]; 163 } 164 rt = lft + this.size[0]; 165 166 ay = this.getAnchorY(); 167 if (ay === "top") { 168 bot = this.coords.scrCoords[2] + this.size[1]; 169 } else if (ay === "middle") { 170 bot = this.coords.scrCoords[2] + 0.5 * this.size[1]; 171 } else { 172 bot = this.coords.scrCoords[2]; 173 } 174 top = bot - this.size[1]; 175 176 if (Type.evaluate(this.visProp.dragarea) === "all") { 177 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r; 178 } 179 // e.g. 'small' 180 return ( 181 y >= top - r && 182 y <= bot + r && 183 ((x >= lft - r && x <= lft + 2 * r) || (x >= rt - 2 * r && x <= rt + r)) 184 ); 185 }, 186 187 /** 188 * This sets the updateText function of this element depending on the type of text content passed. 189 * Used by {@link JXG.Text#_setText} and {@link JXG.Text} constructor. 190 * @param {String|Function|Number} text 191 * @private 192 */ 193 _createFctUpdateText: function (text) { 194 var updateText, e, 195 resolvedText, 196 ev_p = Type.evaluate(this.visProp.parse), 197 ev_um = Type.evaluate(this.visProp.usemathjax), 198 ev_uk = Type.evaluate(this.visProp.usekatex), 199 convertJessieCode = false; 200 201 this.orgText = text; 202 203 if (Type.isFunction(text)) { 204 // <value> tags will not be evaluated if text is provided by a function 205 this.updateText = function () { 206 resolvedText = text().toString(); // Evaluate function 207 if (ev_p && !ev_um && !ev_uk) { 208 this.plaintext = this.replaceSub( 209 this.replaceSup( 210 this.convertGeonextAndSketchometry2CSS(resolvedText) 211 ) 212 ); 213 } else { 214 this.plaintext = resolvedText; 215 } 216 }; 217 } else { 218 if (Type.isNumber(text)) { 219 this.content = Type.toFixed(text, Type.evaluate(this.visProp.digits)); 220 } else if (Type.isString(text) && ev_p) { 221 if (Type.evaluate(this.visProp.useasciimathml)) { 222 // ASCIIMathML 223 this.content = "'`" + text + "`'"; 224 } else if (ev_um || ev_uk) { 225 // MathJax or KaTeX 226 // Replace value-tags by functions 227 this.content = this.valueTagToJessieCode(text); 228 this.content = this.content.replace(/\\/g, "\\\\"); // Replace single backshlash by double 229 } else { 230 // No TeX involved. 231 // Converts GEONExT syntax into JavaScript string 232 // Short math is allowed 233 // Replace value-tags by functions 234 // Avoid geonext2JS calls 235 this.content = this.poorMansTeX(this.valueTagToJessieCode(text)); 236 } 237 convertJessieCode = true; 238 } 239 240 // Generate function which returns the text to be displayed 241 if (convertJessieCode) { 242 // Convert JessieCode to JS function 243 updateText = this.board.jc.snippet(this.content, true, "", false); 244 for (e in updateText.deps) { 245 this.addParents(updateText.deps[e]); 246 updateText.deps[e].addChild(this); 247 } 248 249 // Ticks have been esacped in valueTagToJessieCode 250 this.updateText = function () { 251 this.plaintext = this.unescapeTicks(updateText()); 252 }; 253 } else { 254 this.updateText = function () { 255 this.plaintext = text; 256 }; 257 } 258 } 259 }, 260 261 /** 262 * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because 263 * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode. 264 * @param {String|Function|Number} text 265 * @returns {JXG.Text} 266 * @private 267 */ 268 _setText: function (text) { 269 this._createFctUpdateText(text); 270 271 // First evaluation of the string. 272 // We need this for display='internal' and Canvas 273 this.updateText(); 274 this.fullUpdate(); 275 276 // We do not call updateSize for the infobox to speed up rendering 277 if (!this.board.infobox || this.id !== this.board.infobox.id) { 278 this.updateSize(); // updateSize() is called at least once. 279 } 280 281 // This may slow down canvas renderer 282 // if (this.board.renderer.type === 'canvas') { 283 // this.board.fullUpdate(); 284 // } 285 286 return this; 287 }, 288 289 /** 290 * Defines new content but converts < and > to HTML entities before updating the DOM. 291 * @param {String|function} text 292 */ 293 setTextJessieCode: function (text) { 294 var s; 295 296 this.visProp.castext = text; 297 if (Type.isFunction(text)) { 298 s = function () { 299 return Type.sanitizeHTML(text()); 300 }; 301 } else { 302 if (Type.isNumber(text)) { 303 s = text; 304 } else { 305 s = Type.sanitizeHTML(text); 306 } 307 } 308 309 return this._setText(s); 310 }, 311 312 /** 313 * Defines new content. 314 * @param {String|function} text 315 * @returns {JXG.Text} Reference to the text object. 316 */ 317 setText: function (text) { 318 return this._setText(text); 319 }, 320 321 /** 322 * Recompute the width and the height of the text box. 323 * Updates the array {@link JXG.Text#size} with pixel values. 324 * The result may differ from browser to browser 325 * by some pixels. 326 * In canvas an old IEs we use a very crude estimation of the dimensions of 327 * the textbox. 328 * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and 329 * for aligning text. 330 * 331 * @return {[type]} [description] 332 */ 333 updateSize: function () { 334 var tmp, 335 that, 336 node, 337 ev_d = Type.evaluate(this.visProp.display); 338 339 if (!Env.isBrowser || this.board.renderer.type === "no") { 340 return this; 341 } 342 node = this.rendNode; 343 344 /** 345 * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode. 346 */ 347 if (ev_d === "html" || this.board.renderer.type === "vml") { 348 if (Type.exists(node.offsetWidth)) { 349 that = this; 350 window.setTimeout(function () { 351 that.size = [node.offsetWidth, node.offsetHeight]; 352 that.needsUpdate = true; 353 that.updateRenderer(); 354 }, 0); 355 // In case, there is non-zero padding or borders 356 // the following approach does not longer work. 357 // s = [node.offsetWidth, node.offsetHeight]; 358 // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight 359 // that = this; 360 // window.setTimeout(function () { 361 // that.size = [node.offsetWidth, node.offsetHeight]; 362 // that.needsUpdate = true; 363 // that.updateRenderer(); 364 // }, 0); 365 // } else { 366 // this.size = s; 367 // } 368 } else { 369 this.size = this.crudeSizeEstimate(); 370 } 371 } else if (ev_d === "internal") { 372 if (this.board.renderer.type === "svg") { 373 that = this; 374 window.setTimeout(function () { 375 try { 376 tmp = node.getBBox(); 377 that.size = [tmp.width, tmp.height]; 378 that.needsUpdate = true; 379 that.updateRenderer(); 380 } catch (e) {} 381 }, 0); 382 } else if (this.board.renderer.type === "canvas") { 383 this.size = this.crudeSizeEstimate(); 384 } 385 } 386 387 return this; 388 }, 389 390 /** 391 * A very crude estimation of the dimensions of the textbox in case nothing else is available. 392 * @returns {Array} 393 */ 394 crudeSizeEstimate: function () { 395 var ev_fs = parseFloat(Type.evaluate(this.visProp.fontsize)); 396 return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9]; 397 }, 398 399 /** 400 * Decode unicode entities into characters. 401 * @param {String} string 402 * @returns {String} 403 */ 404 utf8_decode: function (string) { 405 return string.replace(/(\w+);/g, function (m, p1) { 406 return String.fromCharCode(parseInt(p1, 16)); 407 }); 408 }, 409 410 /** 411 * Replace _{} by <sub> 412 * @param {String} te String containing _{}. 413 * @returns {String} Given string with _{} replaced by <sub>. 414 */ 415 replaceSub: function (te) { 416 if (!te.indexOf) { 417 return te; 418 } 419 420 var j, 421 i = te.indexOf("_{"); 422 423 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 424 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 425 /*jslint regexp: true*/ 426 427 while (i >= 0) { 428 te = te.substr(0, i) + te.substr(i).replace(/_\{/, "<sub>"); 429 j = te.substr(i).indexOf("}"); 430 if (j >= 0) { 431 te = te.substr(0, j) + te.substr(j).replace(/\}/, "</sub>"); 432 } 433 i = te.indexOf("_{"); 434 } 435 436 i = te.indexOf("_"); 437 while (i >= 0) { 438 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, "<sub>$1</sub>"); 439 i = te.indexOf("_"); 440 } 441 442 return te; 443 }, 444 445 /** 446 * Replace ^{} by <sup> 447 * @param {String} te String containing ^{}. 448 * @returns {String} Given string with ^{} replaced by <sup>. 449 */ 450 replaceSup: function (te) { 451 if (!te.indexOf) { 452 return te; 453 } 454 455 var j, 456 i = te.indexOf("^{"); 457 458 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 459 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 460 /*jslint regexp: true*/ 461 462 while (i >= 0) { 463 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, "<sup>"); 464 j = te.substr(i).indexOf("}"); 465 if (j >= 0) { 466 te = te.substr(0, j) + te.substr(j).replace(/\}/, "</sup>"); 467 } 468 i = te.indexOf("^{"); 469 } 470 471 i = te.indexOf("^"); 472 while (i >= 0) { 473 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, "<sup>$1</sup>"); 474 i = te.indexOf("^"); 475 } 476 477 return te; 478 }, 479 480 /** 481 * Return the width of the text element. 482 * @returns {Array} [width, height] in pixel 483 */ 484 getSize: function () { 485 return this.size; 486 }, 487 488 /** 489 * Move the text to new coordinates. 490 * @param {number} x 491 * @param {number} y 492 * @returns {object} reference to the text object. 493 */ 494 setCoords: function (x, y) { 495 var coordsAnchor, dx, dy; 496 if (Type.isArray(x) && x.length > 1) { 497 y = x[1]; 498 x = x[0]; 499 } 500 501 if (Type.evaluate(this.visProp.islabel) && Type.exists(this.element)) { 502 coordsAnchor = this.element.getLabelAnchor(); 503 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX; 504 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY; 505 506 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]); 507 } else { 508 /* 509 this.X = function () { 510 return x; 511 }; 512 513 this.Y = function () { 514 return y; 515 }; 516 */ 517 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 518 } 519 520 // this should be a local update, otherwise there might be problems 521 // with the tick update routine resulting in orphaned tick labels 522 this.fullUpdate(); 523 524 return this; 525 }, 526 527 /** 528 * Evaluates the text. 529 * Then, the update function of the renderer 530 * is called. 531 */ 532 update: function (fromParent) { 533 if (!this.needsUpdate) { 534 return this; 535 } 536 537 this.updateCoords(fromParent); 538 this.updateText(); 539 540 if (Type.evaluate(this.visProp.display) === "internal") { 541 if (Type.isString(this.plaintext)) { 542 this.plaintext = this.utf8_decode(this.plaintext); 543 } 544 } 545 546 this.checkForSizeUpdate(); 547 if (this.needsSizeUpdate) { 548 this.updateSize(); 549 } 550 551 return this; 552 }, 553 554 /** 555 * Used to save updateSize() calls. 556 * Called in JXG.Text.update 557 * That means this.update() has been called. 558 * More tests are in JXG.Renderer.updateTextStyle. The latter tests 559 * are one update off. But this should pose not too many problems, since 560 * it affects fontSize and cssClass changes. 561 * 562 * @private 563 */ 564 checkForSizeUpdate: function () { 565 if (this.board.infobox && this.id === this.board.infobox.id) { 566 this.needsSizeUpdate = false; 567 } else { 568 // For some magic reason it is more efficient on the iPad to 569 // call updateSize() for EVERY text element EVERY time. 570 this.needsSizeUpdate = this.plaintextOld !== this.plaintext; 571 572 if (this.needsSizeUpdate) { 573 this.plaintextOld = this.plaintext; 574 } 575 } 576 }, 577 578 /** 579 * The update function of the renderert 580 * is called. 581 * @private 582 */ 583 updateRenderer: function () { 584 if ( 585 //this.board.updateQuality === this.board.BOARD_QUALITY_HIGH && 586 Type.evaluate(this.visProp.autoposition) 587 ) { 588 this.setAutoPosition().updateConstraint(); 589 } 590 return this.updateRendererGeneric("updateText"); 591 }, 592 593 /** 594 * Converts shortened math syntax into correct syntax: 3x instead of 3*x or 595 * (a+b)(3+1) instead of (a+b)*(3+1). 596 * 597 * @private 598 * @param{String} expr Math term 599 * @returns {string} expanded String 600 */ 601 expandShortMath: function (expr) { 602 var re = /([)0-9.])\s*([(a-zA-Z_])/g; 603 return expr.replace(re, "$1*$2"); 604 }, 605 606 /** 607 * Converts the GEONExT syntax of the <value> terms into JavaScript. 608 * Also, all Objects whose name appears in the term are searched and 609 * the text is added as child to these objects. 610 * This method is called if the attribute parse==true is set. 611 * 612 * @param{String} contentStr String to be parsed 613 * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x). 614 * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility 615 * this has to be set explicitely to true. 616 * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX. 617 * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done. 618 * This flag allows the combination of <value> tag containing calculations with TeX output. 619 * 620 * @private 621 * @see JXG.GeonextParser.geonext2JS 622 */ 623 generateTerm: function (contentStr, expand, avoidGeonext2JS) { 624 var res, 625 term, 626 i, 627 j, 628 plaintext = '""'; 629 630 // Revert possible jc replacement 631 contentStr = contentStr || ""; 632 contentStr = contentStr.replace(/\r/g, ""); 633 contentStr = contentStr.replace(/\n/g, ""); 634 contentStr = contentStr.replace(/"/g, "'"); 635 contentStr = contentStr.replace(/'/g, "\\'"); 636 637 // Old GEONExT syntax, not (yet) supported as TeX output. 638 // Otherwise, the else clause should be used. 639 // That means, i.e. the <arc> tag and <sqrt> tag are not 640 // converted into TeX syntax. 641 contentStr = contentStr.replace(/&arc;/g, "∠"); 642 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 643 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 644 contentStr = contentStr.replace(/<sqrt\s*\/>/g, "√"); 645 646 contentStr = contentStr.replace(/<value>/g, "<value>"); 647 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 648 649 // Convert GEONExT syntax into JavaScript syntax 650 i = contentStr.indexOf("<value>"); 651 j = contentStr.indexOf("</value>"); 652 if (i >= 0) { 653 while (i >= 0) { 654 plaintext += 655 ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 656 // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"'; 657 658 term = contentStr.slice(i + 7, j); 659 term = term.replace(/\s+/g, ""); // Remove all whitespace 660 if (expand === true) { 661 term = this.expandShortMath(term); 662 } 663 if (avoidGeonext2JS) { 664 res = term; 665 } else { 666 res = GeonextParser.geonext2JS(term, this.board); 667 } 668 res = res.replace(/\\"/g, "'"); 669 res = res.replace(/\\'/g, "'"); 670 671 // GEONExT-Hack: apply rounding once only. 672 if (res.indexOf("toFixed") < 0) { 673 // output of a value tag 674 if ( 675 Type.isNumber( 676 Type.bind(this.board.jc.snippet(res, true, "", false), this)() 677 ) 678 ) { 679 // may also be a string 680 plaintext += 681 "+(" + 682 res + 683 ").toFixed(" + 684 Type.evaluate(this.visProp.digits) + 685 ")"; 686 } else { 687 plaintext += "+(" + res + ")"; 688 } 689 } else { 690 plaintext += "+(" + res + ")"; 691 } 692 693 contentStr = contentStr.slice(j + 8); 694 i = contentStr.indexOf("<value>"); 695 j = contentStr.indexOf("</value>"); 696 } 697 } 698 699 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 700 plaintext = this.convertGeonextAndSketchometry2CSS(plaintext); 701 702 // This should replace e.g. π by π 703 plaintext = plaintext.replace(/&/g, "&"); 704 plaintext = plaintext.replace(/"/g, "'"); 705 706 return plaintext; 707 }, 708 709 valueTagToJessieCode: function (contentStr) { 710 var res, 711 term, 712 i, 713 j, 714 expandShortMath = true, 715 textComps = [], 716 tick = '"'; 717 718 contentStr = contentStr || ""; 719 contentStr = contentStr.replace(/\r/g, ""); 720 contentStr = contentStr.replace(/\n/g, ""); 721 722 contentStr = contentStr.replace(/<value>/g, "<value>"); 723 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 724 725 // Convert content of value tag (GEONExT/JessieCode) syntax into JavaScript syntax 726 i = contentStr.indexOf("<value>"); 727 j = contentStr.indexOf("</value>"); 728 if (i >= 0) { 729 while (i >= 0) { 730 // Add string fragment before <value> tag 731 textComps.push(tick + this.escapeTicks(contentStr.slice(0, i)) + tick); 732 733 term = contentStr.slice(i + 7, j); 734 term = term.replace(/\s+/g, ""); // Remove all whitespace 735 if (expandShortMath === true) { 736 term = this.expandShortMath(term); 737 } 738 res = term; 739 res = res.replace(/\\"/g, "'").replace(/\\'/g, "'"); 740 741 // Hack: apply rounding once only. 742 if (res.indexOf("toFixed") < 0) { 743 // Output of a value tag 744 // Run the JessieCode parser 745 if ( 746 Type.isNumber( 747 Type.bind(this.board.jc.snippet(res, true, "", false), this)() 748 ) 749 ) { 750 // Output is number 751 textComps.push( 752 "(" + 753 res + 754 ").toFixed(" + 755 Type.evaluate(this.visProp.digits) + 756 ")" 757 ); 758 } else { 759 // Output is a string 760 textComps.push("(" + res + ")"); 761 } 762 } else { 763 textComps.push("(" + res + ")"); 764 } 765 contentStr = contentStr.slice(j + 8); 766 i = contentStr.indexOf("<value>"); 767 j = contentStr.indexOf("</value>"); 768 } 769 } 770 // Add trailing string fragment 771 textComps.push(tick + this.escapeTicks(contentStr) + tick); 772 773 return textComps.join(" + ").replace(/&/g, "&"); 774 }, 775 776 poorMansTeX: function (s) { 777 s = s 778 .replace(/<arc\s*\/*>/g, "∠") 779 .replace(/<arc\s*\/*>/g, "∠") 780 .replace(/<sqrt\s*\/*>/g, "√") 781 .replace(/<sqrt\s*\/*>/g, "√"); 782 783 return this.convertGeonextAndSketchometry2CSS(this.replaceSub(this.replaceSup(s))); 784 }, 785 786 escapeTicks: function (s) { 787 return s.replace(/"/g, "%22").replace(/'/g, "%27"); 788 }, 789 790 unescapeTicks: function (s) { 791 return s.replace(/%22/g, '"').replace(/%27/g, "'"); 792 }, 793 794 /** 795 * Converts the GEONExT tags <overline> and <arrow> to 796 * HTML span tags with proper CSS formatting. 797 * @private 798 * @see JXG.Text.generateTerm 799 * @see JXG.Text._setText 800 */ 801 convertGeonext2CSS: function (s) { 802 if (Type.isString(s)) { 803 s = s.replace( 804 /(<|<)overline(>|>)/g, 805 "<span style=text-decoration:overline;>" 806 ); 807 s = s.replace(/(<|<)\/overline(>|>)/g, "</span>"); 808 s = s.replace( 809 /(<|<)arrow(>|>)/g, 810 "<span style=text-decoration:overline;>" 811 ); 812 s = s.replace(/(<|<)\/arrow(>|>)/g, "</span>"); 813 } 814 815 return s; 816 }, 817 818 /** 819 * Converts the sketchometry tag <sketchofont> to 820 * HTML span tags with proper CSS formatting. 821 * @private 822 * @see JXG.Text.generateTerm 823 * @see JXG.Text._setText 824 */ 825 convertSketchometry2CSS: function (s) { 826 if (Type.isString(s)) { 827 s = s.replace( 828 /(<|<)sketchofont(>|>)/g, 829 "<span style=font-family:sketchometry-light;font-weight:500;>" 830 ); 831 s = s.replace(/(<|<)\/sketchofont(>|>)/g, "</span>"); 832 s = s.replace( 833 /(<|<)sketchometry-light(>|>)/g, 834 "<span style=font-family:sketchometry-light;font-weight:500;>" 835 ); 836 s = s.replace(/(<|<)\/sketchometry-light(>|>)/g, "</span>"); 837 } 838 839 return s; 840 }, 841 842 /** 843 * Alias for convertGeonext2CSS and convertSketchometry2CSS 844 * @private 845 * @see JXG.Text.convertGeonext2CSS 846 * @see JXG.Text.convertSketchometry2CSS 847 */ 848 convertGeonextAndSketchometry2CSS: function (s) { 849 s = this.convertGeonext2CSS(s); 850 s = this.convertSketchometry2CSS(s); 851 return s; 852 }, 853 854 /** 855 * Finds dependencies in a given term and notifies the parents by adding the 856 * dependent object to the found objects child elements. 857 * @param {String} content String containing dependencies for the given object. 858 * @private 859 */ 860 notifyParents: function (content) { 861 var search, 862 res = null; 863 864 // revert possible jc replacement 865 content = content.replace(/<value>/g, "<value>"); 866 content = content.replace(/<\/value>/g, "</value>"); 867 868 do { 869 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/; 870 res = search.exec(content); 871 872 if (res !== null) { 873 GeonextParser.findDependencies(this, res[1], this.board); 874 content = content.substr(res.index); 875 content = content.replace(search, ""); 876 } 877 } while (res !== null); 878 879 return this; 880 }, 881 882 // documented in element.js 883 getParents: function () { 884 var p; 885 if (this.relativeCoords !== undefined) { 886 // Texts with anchor elements, excluding labels 887 p = [ 888 this.relativeCoords.usrCoords[1], 889 this.relativeCoords.usrCoords[2], 890 this.orgText 891 ]; 892 } else { 893 // Other texts 894 p = [this.Z(), this.X(), this.Y(), this.orgText]; 895 } 896 897 if (this.parents.length !== 0) { 898 p = this.parents; 899 } 900 901 return p; 902 }, 903 904 bounds: function () { 905 var c = this.coords.usrCoords; 906 907 if ( 908 Type.evaluate(this.visProp.islabel) || 909 this.board.unitY === 0 || 910 this.board.unitX === 0 911 ) { 912 return [0, 0, 0, 0]; 913 } 914 return [ 915 c[1], 916 c[2] + this.size[1] / this.board.unitY, 917 c[1] + this.size[0] / this.board.unitX, 918 c[2] 919 ]; 920 }, 921 922 getAnchorX: function () { 923 var a = Type.evaluate(this.visProp.anchorx); 924 if (a === "auto") { 925 switch (this.visProp.position) { 926 case "top": 927 case "bot": 928 return "middle"; 929 case "rt": 930 case "lrt": 931 case "urt": 932 return "left"; 933 case "lft": 934 case "llft": 935 case "ulft": 936 default: 937 return "right"; 938 } 939 } 940 return a; 941 }, 942 943 getAnchorY: function () { 944 var a = Type.evaluate(this.visProp.anchory); 945 if (a === "auto") { 946 switch (this.visProp.position) { 947 case "top": 948 case "ulft": 949 case "urt": 950 return "bottom"; 951 case "bot": 952 case "lrt": 953 case "llft": 954 return "top"; 955 case "rt": 956 case "lft": 957 default: 958 return "middle"; 959 } 960 } 961 return a; 962 }, 963 964 /** 965 * Computes the number of overlaps of a box of w pixels width, h pixels height 966 * and center (x, y) 967 * 968 * @private 969 * @param {Number} x x-coordinate of the center (screen coordinates) 970 * @param {Number} y y-coordinate of the center (screen coordinates) 971 * @param {Number} w width of the box in pixel 972 * @param {Number} h width of the box in pixel 973 * @return {Number} Number of overlapping elements 974 */ 975 getNumberofConflicts: function (x, y, w, h) { 976 var count = 0, 977 i, 978 obj, 979 le, 980 savePointPrecision; 981 982 // Set the precision of hasPoint to half the max if label isn't too long 983 savePointPrecision = this.board.options.precision.hasPoint; 984 // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 985 this.board.options.precision.hasPoint = (w + h) * 0.25; 986 // TODO: 987 // Make it compatible with the objects' visProp.precision attribute 988 for (i = 0, le = this.board.objectsList.length; i < le; i++) { 989 obj = this.board.objectsList[i]; 990 if ( 991 obj.visPropCalc.visible && 992 obj.elType !== "axis" && 993 obj.elType !== "ticks" && 994 obj !== this.board.infobox && 995 obj !== this && 996 obj.hasPoint(x, y) 997 ) { 998 count++; 999 } 1000 } 1001 this.board.options.precision.hasPoint = savePointPrecision; 1002 1003 return count; 1004 }, 1005 1006 /** 1007 * Sets the offset of a label element to the position with the least number 1008 * of overlaps with other elements, while retaining the distance to its 1009 * anchor element. Twelve different angles are possible. 1010 * 1011 * @returns {JXG.Text} Reference to the text object. 1012 */ 1013 setAutoPosition: function () { 1014 var x, 1015 y, 1016 cx, 1017 cy, 1018 anchorCoords, 1019 // anchorX, anchorY, 1020 w = this.size[0], 1021 h = this.size[1], 1022 start_angle, 1023 angle, 1024 optimum = { 1025 conflicts: Infinity, 1026 angle: 0, 1027 r: 0 1028 }, 1029 max_r, 1030 delta_r, 1031 conflicts, 1032 offset, 1033 r, 1034 num_positions = 12, 1035 step = (2 * Math.PI) / num_positions, 1036 j, 1037 dx, 1038 dy, 1039 co, 1040 si; 1041 1042 if ( 1043 this === this.board.infobox || 1044 !this.visPropCalc.visible || 1045 !Type.evaluate(this.visProp.islabel) || 1046 !this.element 1047 ) { 1048 return this; 1049 } 1050 1051 // anchorX = Type.evaluate(this.visProp.anchorx); 1052 // anchorY = Type.evaluate(this.visProp.anchory); 1053 offset = Type.evaluate(this.visProp.offset); 1054 anchorCoords = this.element.getLabelAnchor(); 1055 cx = anchorCoords.scrCoords[1]; 1056 cy = anchorCoords.scrCoords[2]; 1057 1058 // Set dx, dy as the relative position of the center of the label 1059 // to its anchor element ignoring anchorx and anchory. 1060 dx = offset[0]; 1061 dy = offset[1]; 1062 1063 conflicts = this.getNumberofConflicts(cx + dx, cy - dy, w, h); 1064 if (conflicts === 0) { 1065 return this; 1066 } 1067 // console.log(this.id, conflicts, w, h); 1068 // r = Geometry.distance([0, 0], offset, 2); 1069 1070 r = 12; 1071 max_r = 28; 1072 delta_r = 0.2 * r; 1073 1074 start_angle = Math.atan2(dy, dx); 1075 1076 optimum.conflicts = conflicts; 1077 optimum.angle = start_angle; 1078 optimum.r = r; 1079 1080 while (optimum.conflicts > 0 && r < max_r) { 1081 for ( 1082 j = 1, angle = start_angle + step; 1083 j < num_positions && optimum.conflicts > 0; 1084 j++ 1085 ) { 1086 co = Math.cos(angle); 1087 si = Math.sin(angle); 1088 1089 x = cx + r * co; 1090 y = cy - r * si; 1091 1092 conflicts = this.getNumberofConflicts(x, y, w, h); 1093 if (conflicts < optimum.conflicts) { 1094 optimum.conflicts = conflicts; 1095 optimum.angle = angle; 1096 optimum.r = r; 1097 } 1098 if (optimum.conflicts === 0) { 1099 break; 1100 } 1101 angle += step; 1102 } 1103 r += delta_r; 1104 } 1105 // console.log(this.id, "after", optimum) 1106 r = optimum.r; 1107 co = Math.cos(optimum.angle); 1108 si = Math.sin(optimum.angle); 1109 this.visProp.offset = [r * co, r * si]; 1110 1111 if (co < -0.2) { 1112 this.visProp.anchorx = "right"; 1113 } else if (co > 0.2) { 1114 this.visProp.anchorx = "left"; 1115 } else { 1116 this.visProp.anchorx = "middle"; 1117 } 1118 1119 return this; 1120 } 1121 } 1122 ); 1123 1124 /** 1125 * @class Construct and handle texts. 1126 * 1127 * The coordinates can be relative to the coordinates of an element 1128 * given in {@link JXG.Options#text.anchor}. 1129 * 1130 * MathJaX, HTML and GEONExT syntax can be handled. 1131 * @pseudo 1132 * @description 1133 * @name Text 1134 * @augments JXG.Text 1135 * @constructor 1136 * @type JXG.Text 1137 * 1138 * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements. 1139 * <p> 1140 * Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T 1141 * constraint, or a function which takes no parameter and returns a number. Every parent element determines one coordinate. If a coordinate is 1142 * given by a number, the number determines the initial position of a free text. If given by a string or a function that coordinate will be constrained 1143 * that means the user won't be able to change the texts's position directly by mouse because it will be calculated automatically depending on the string 1144 * or the function's return value. If two parent elements are given the coordinates will be interpreted as 2D affine Euclidean coordinates, if three such 1145 * parent elements are given they will be interpreted as homogeneous coordinates. 1146 * <p> 1147 * The text to display may be given as string or as function returning a string. 1148 * 1149 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display 1150 * the text. In this case it is also possible to use ASCIIMathML. Incase of 'internal', a SVG or VML text element is used to display the text. 1151 * @see JXG.Text 1152 * @example 1153 * // Create a fixed text at position [0,1]. 1154 * var t1 = board.create('text',[0,1,"Hello World"]); 1155 * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 1156 * <script type="text/javascript"> 1157 * var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1158 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 1159 * </script><pre> 1160 * @example 1161 * // Create a variable text at a variable position. 1162 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1163 * var graph = board.create('text', 1164 * [function(x){ return s.Value();}, 1, 1165 * function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);} 1166 * ] 1167 * ); 1168 * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 1169 * <script type="text/javascript"> 1170 * var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1171 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1172 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]); 1173 * </script><pre> 1174 * @example 1175 * // Create a text bound to the point A 1176 * var p = board.create('point',[0, 1]), 1177 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1178 * 1179 * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div> 1180 * <script type="text/javascript"> 1181 * (function() { 1182 * var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723', 1183 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1184 * var p = board.create('point',[0, 1]), 1185 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1186 * 1187 * })(); 1188 * 1189 * </script><pre> 1190 * 1191 */ 1192 JXG.createText = function (board, parents, attributes) { 1193 var t, 1194 attr = Type.copyAttributes(attributes, board.options, "text"), 1195 coords = parents.slice(0, -1), 1196 content = parents[parents.length - 1]; 1197 1198 // downwards compatibility 1199 attr.anchor = attr.parent || attr.anchor; 1200 t = CoordsElement.create(JXG.Text, board, coords, attr, content); 1201 1202 if (!t) { 1203 throw new Error( 1204 "JSXGraph: Can't create text with parent types '" + 1205 typeof parents[0] + 1206 "' and '" + 1207 typeof parents[1] + 1208 "'." + 1209 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]" 1210 ); 1211 } 1212 1213 if (attr.rotate !== 0 && attr.display === "internal") { 1214 // This is the default value, i.e. no rotation 1215 t.addRotation(attr.rotate); 1216 } 1217 1218 return t; 1219 }; 1220 1221 JXG.registerElement("text", JXG.createText); 1222 1223 /** 1224 * @class Labels are text objects tied to other elements like points, lines and curves. 1225 * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)". 1226 * 1227 * @pseudo 1228 * @description 1229 * @name Label 1230 * @augments JXG.Text 1231 * @constructor 1232 * @type JXG.Text 1233 */ 1234 // See element.js#createLabel 1235 1236 /** 1237 * [[x,y], [w px, h px], [range] 1238 */ 1239 JXG.createHTMLSlider = function (board, parents, attributes) { 1240 var t, 1241 par, 1242 attr = Type.copyAttributes(attributes, board.options, "htmlslider"); 1243 1244 if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) { 1245 throw new Error( 1246 "JSXGraph: Can't create htmlslider with parent types '" + 1247 typeof parents[0] + 1248 "' and '" + 1249 typeof parents[1] + 1250 "'." + 1251 "\nPossible parents are: [[x,y], [min, start, max]]" 1252 ); 1253 } 1254 1255 // backwards compatibility 1256 attr.anchor = attr.parent || attr.anchor; 1257 attr.fixed = attr.fixed || true; 1258 1259 par = [ 1260 parents[0][0], 1261 parents[0][1], 1262 '<form style="display:inline">' + 1263 '<input type="range" /><span></span><input type="text" />' + 1264 "</form>" 1265 ]; 1266 1267 t = JXG.createText(board, par, attr); 1268 t.type = Type.OBJECT_TYPE_HTMLSLIDER; 1269 1270 t.rendNodeForm = t.rendNode.childNodes[0]; 1271 1272 t.rendNodeRange = t.rendNodeForm.childNodes[0]; 1273 t.rendNodeRange.min = parents[1][0]; 1274 t.rendNodeRange.max = parents[1][2]; 1275 t.rendNodeRange.step = attr.step; 1276 t.rendNodeRange.value = parents[1][1]; 1277 1278 t.rendNodeLabel = t.rendNodeForm.childNodes[1]; 1279 t.rendNodeLabel.id = t.rendNode.id + "_label"; 1280 1281 if (attr.withlabel) { 1282 t.rendNodeLabel.innerHTML = t.name + "="; 1283 } 1284 1285 t.rendNodeOut = t.rendNodeForm.childNodes[2]; 1286 t.rendNodeOut.value = parents[1][1]; 1287 1288 try { 1289 t.rendNodeForm.id = t.rendNode.id + "_form"; 1290 t.rendNodeRange.id = t.rendNode.id + "_range"; 1291 t.rendNodeOut.id = t.rendNode.id + "_out"; 1292 } catch (e) { 1293 JXG.debug(e); 1294 } 1295 1296 t.rendNodeRange.style.width = attr.widthrange + "px"; 1297 t.rendNodeRange.style.verticalAlign = "middle"; 1298 t.rendNodeOut.style.width = attr.widthout + "px"; 1299 1300 t._val = parents[1][1]; 1301 1302 if (JXG.supportsVML()) { 1303 /* 1304 * OnChange event is used for IE browsers 1305 * The range element is supported since IE10 1306 */ 1307 Env.addEvent(t.rendNodeForm, "change", priv.HTMLSliderInputEventHandler, t); 1308 } else { 1309 /* 1310 * OnInput event is used for non-IE browsers 1311 */ 1312 Env.addEvent(t.rendNodeForm, "input", priv.HTMLSliderInputEventHandler, t); 1313 } 1314 1315 t.Value = function () { 1316 return this._val; 1317 }; 1318 1319 return t; 1320 }; 1321 1322 JXG.registerElement("htmlslider", JXG.createHTMLSlider); 1323 1324 export default JXG.Text; 1325 // export default { 1326 // Text: JXG.Text, 1327 // createText: JXG.createText, 1328 // createHTMLSlider: JXG.createHTMLSlider 1329 // }; 1330