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