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 201 this.orgText = text; 202 if (Type.isFunction(text)) { 203 // <value> tags will not be evaluated. 204 this.updateText = function () { 205 resolvedText = text().toString(); 206 if (ev_p && !ev_um && !ev_uk) { 207 this.plaintext = this.replaceSub(this.replaceSup(this.convertGeonextAndSketchometry2CSS(resolvedText))); 208 } else { 209 this.plaintext = resolvedText; 210 } 211 }; 212 } else if (Type.isString(text) && !ev_p) { // Do not parse 213 this.updateText = function () { 214 this.plaintext = text; 215 }; 216 } else { // Parse 217 if (Type.isNumber(text)) { 218 this.content = Type.toFixed(text, Type.evaluate(this.visProp.digits)); 219 } else { 220 if (Type.evaluate(this.visProp.useasciimathml)) { 221 // Convert via ASCIIMathML 222 this.content = "'`" + text + "`'"; 223 } else if (ev_um || ev_uk) { 224 if (ev_p) { 225 this.content = this.generateTerm(text, true, true, true); // Replace value-tags 226 this.content = this.content.replace(/\\/g, "\\\\"); 227 } else { 228 this.content = "'" + text + "'"; 229 } 230 } else { 231 // Converts GEONExT syntax into JavaScript string 232 // Short math is allowed 233 // Avoid geonext2JS calls 234 this.content = this.generateTerm(text, true, true, false); 235 } 236 } 237 // Convert JessieCode to JS function 238 updateText = this.board.jc.snippet(this.content, true, '', false); 239 this.updateText = function () { 240 this.plaintext = updateText(); 241 }; 242 } 243 }, 244 245 /** 246 * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because 247 * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode. 248 * @param {String|Function|Number} text 249 * @returns {JXG.Text} 250 * @private 251 */ 252 _setText: function (text) { 253 this._createFctUpdateText(text); 254 255 // First evaluation of the string. 256 // We need this for display='internal' and Canvas 257 this.updateText(); 258 this.fullUpdate(); 259 260 // We do not call updateSize for the infobox to speed up rendering 261 if (!this.board.infobox || this.id !== this.board.infobox.id) { 262 this.updateSize(); // updateSize() is called at least once. 263 } 264 265 // This may slow down canvas renderer 266 // if (this.board.renderer.type === 'canvas') { 267 // this.board.fullUpdate(); 268 // } 269 270 return this; 271 }, 272 273 /** 274 * Defines new content but converts < and > to HTML entities before updating the DOM. 275 * @param {String|function} text 276 */ 277 setTextJessieCode: function (text) { 278 var s; 279 280 this.visProp.castext = text; 281 if (Type.isFunction(text)) { 282 s = function () { 283 return Type.sanitizeHTML(text()); 284 }; 285 } else { 286 if (Type.isNumber(text)) { 287 s = text; 288 } else { 289 s = Type.sanitizeHTML(text); 290 } 291 } 292 293 return this._setText(s); 294 }, 295 296 /** 297 * Defines new content. 298 * @param {String|function} text 299 * @returns {JXG.Text} Reference to the text object. 300 */ 301 setText: function (text) { 302 return this._setText(text); 303 }, 304 305 /** 306 * Recompute the width and the height of the text box. 307 * Updates the array {@link JXG.Text#size} with pixel values. 308 * The result may differ from browser to browser 309 * by some pixels. 310 * In canvas an old IEs we use a very crude estimation of the dimensions of 311 * the textbox. 312 * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and 313 * for aligning text. 314 * 315 * @return {[type]} [description] 316 */ 317 updateSize: function () { 318 var tmp, that, node, 319 ev_d = Type.evaluate(this.visProp.display); 320 321 if (!Env.isBrowser || this.board.renderer.type === 'no') { 322 return this; 323 } 324 node = this.rendNode; 325 326 /** 327 * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode. 328 */ 329 if (ev_d === 'html' || this.board.renderer.type === 'vml') { 330 if (Type.exists(node.offsetWidth)) { 331 that = this; 332 window.setTimeout(function () { 333 that.size = [node.offsetWidth, node.offsetHeight]; 334 that.needsUpdate = true; 335 that.updateRenderer(); 336 }, 0); 337 // In case, there is non-zero padding or borders 338 // the following approach does not longer work. 339 // s = [node.offsetWidth, node.offsetHeight]; 340 // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight 341 // that = this; 342 // window.setTimeout(function () { 343 // that.size = [node.offsetWidth, node.offsetHeight]; 344 // that.needsUpdate = true; 345 // that.updateRenderer(); 346 // }, 0); 347 // } else { 348 // this.size = s; 349 // } 350 } else { 351 this.size = this.crudeSizeEstimate(); 352 } 353 } else if (ev_d === 'internal') { 354 if (this.board.renderer.type === 'svg') { 355 that = this; 356 window.setTimeout(function () { 357 try { 358 tmp = node.getBBox(); 359 that.size = [tmp.width, tmp.height]; 360 that.needsUpdate = true; 361 that.updateRenderer(); 362 } catch (e) { 363 } 364 }, 0); 365 } else if (this.board.renderer.type === 'canvas') { 366 this.size = this.crudeSizeEstimate(); 367 } 368 } 369 370 return this; 371 }, 372 373 /** 374 * A very crude estimation of the dimensions of the textbox in case nothing else is available. 375 * @returns {Array} 376 */ 377 crudeSizeEstimate: function () { 378 var ev_fs = parseFloat(Type.evaluate(this.visProp.fontsize)); 379 return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9]; 380 }, 381 382 /** 383 * Decode unicode entities into characters. 384 * @param {String} string 385 * @returns {String} 386 */ 387 utf8_decode: function (string) { 388 return string.replace(/(\w+);/g, function (m, p1) { 389 return String.fromCharCode(parseInt(p1, 16)); 390 }); 391 }, 392 393 /** 394 * Replace _{} by <sub> 395 * @param {String} te String containing _{}. 396 * @returns {String} Given string with _{} replaced by <sub>. 397 */ 398 replaceSub: function (te) { 399 if (!te.indexOf) { 400 return te; 401 } 402 403 var j, 404 i = te.indexOf('_{'); 405 406 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 407 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 408 /*jslint regexp: true*/ 409 410 while (i >= 0) { 411 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>'); 412 j = te.substr(i).indexOf('}'); 413 if (j >= 0) { 414 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>'); 415 } 416 i = te.indexOf('_{'); 417 } 418 419 i = te.indexOf('_'); 420 while (i >= 0) { 421 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>'); 422 i = te.indexOf('_'); 423 } 424 425 return te; 426 }, 427 428 /** 429 * Replace ^{} by <sup> 430 * @param {String} te String containing ^{}. 431 * @returns {String} Given string with ^{} replaced by <sup>. 432 */ 433 replaceSup: function (te) { 434 if (!te.indexOf) { 435 return te; 436 } 437 438 var j, 439 i = te.indexOf('^{'); 440 441 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 442 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 443 /*jslint regexp: true*/ 444 445 while (i >= 0) { 446 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>'); 447 j = te.substr(i).indexOf('}'); 448 if (j >= 0) { 449 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>'); 450 } 451 i = te.indexOf('^{'); 452 } 453 454 i = te.indexOf('^'); 455 while (i >= 0) { 456 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>'); 457 i = te.indexOf('^'); 458 } 459 460 return te; 461 }, 462 463 /** 464 * Return the width of the text element. 465 * @returns {Array} [width, height] in pixel 466 */ 467 getSize: function () { 468 return this.size; 469 }, 470 471 /** 472 * Move the text to new coordinates. 473 * @param {number} x 474 * @param {number} y 475 * @returns {object} reference to the text object. 476 */ 477 setCoords: function (x, y) { 478 var coordsAnchor, dx, dy; 479 if (Type.isArray(x) && x.length > 1) { 480 y = x[1]; 481 x = x[0]; 482 } 483 484 if (Type.evaluate(this.visProp.islabel) && Type.exists(this.element)) { 485 coordsAnchor = this.element.getLabelAnchor(); 486 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX; 487 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY; 488 489 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]); 490 } else { 491 /* 492 this.X = function () { 493 return x; 494 }; 495 496 this.Y = function () { 497 return y; 498 }; 499 */ 500 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 501 } 502 503 // this should be a local update, otherwise there might be problems 504 // with the tick update routine resulting in orphaned tick labels 505 this.fullUpdate(); 506 507 return this; 508 }, 509 510 /** 511 * Evaluates the text. 512 * Then, the update function of the renderer 513 * is called. 514 */ 515 update: function (fromParent) { 516 if (!this.needsUpdate) { 517 return this; 518 } 519 520 this.updateCoords(fromParent); 521 this.updateText(); 522 523 if (Type.evaluate(this.visProp.display) === 'internal') { 524 if (Type.isString(this.plaintext)) { 525 this.plaintext = this.utf8_decode(this.plaintext); 526 } 527 } 528 529 this.checkForSizeUpdate(); 530 if (this.needsSizeUpdate) { 531 this.updateSize(); 532 } 533 534 return this; 535 }, 536 537 /** 538 * Used to save updateSize() calls. 539 * Called in JXG.Text.update 540 * That means this.update() has been called. 541 * More tests are in JXG.Renderer.updateTextStyle. The latter tests 542 * are one update off. But this should pose not too many problems, since 543 * it affects fontSize and cssClass changes. 544 * 545 * @private 546 */ 547 checkForSizeUpdate: function () { 548 if (this.board.infobox && this.id === this.board.infobox.id) { 549 this.needsSizeUpdate = false; 550 } else { 551 // For some magic reason it is more efficient on the iPad to 552 // call updateSize() for EVERY text element EVERY time. 553 this.needsSizeUpdate = (this.plaintextOld !== this.plaintext); 554 555 if (this.needsSizeUpdate) { 556 this.plaintextOld = this.plaintext; 557 } 558 } 559 560 }, 561 562 /** 563 * The update function of the renderert 564 * is called. 565 * @private 566 */ 567 updateRenderer: function () { 568 if (//this.board.updateQuality === this.board.BOARD_QUALITY_HIGH && 569 Type.evaluate(this.visProp.autoposition)) { 570 571 this.setAutoPosition() 572 .updateConstraint(); 573 } 574 return this.updateRendererGeneric('updateText'); 575 }, 576 577 /** 578 * Converts shortened math syntax into correct syntax: 3x instead of 3*x or 579 * (a+b)(3+1) instead of (a+b)*(3+1). 580 * 581 * @private 582 * @param{String} expr Math term 583 * @returns {string} expanded String 584 */ 585 expandShortMath: function (expr) { 586 var re = /([)0-9.])\s*([(a-zA-Z_])/g; 587 return expr.replace(re, '$1*$2'); 588 }, 589 590 /** 591 * Converts the GEONExT syntax of the <value> terms into JavaScript. 592 * Also, all Objects whose name appears in the term are searched and 593 * the text is added as child to these objects. 594 * This method is called if the attribute parse==true is set. 595 * 596 * @param{String} contentStr String to be parsed 597 * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x). 598 * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility 599 * this has to be set explicitely to true. 600 * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX. 601 * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done. 602 * This flag allows the combination of <value> tag containing calculations with TeX output. 603 * 604 * @private 605 * @see JXG.GeonextParser.geonext2JS 606 */ 607 generateTerm: function (contentStr, expand, avoidGeonext2JS, outputTeX) { 608 var res, term, i, j, 609 plaintext = '""'; 610 611 outputTeX = outputTeX || false; 612 613 // Revert possible jc replacement 614 contentStr = contentStr || ''; 615 contentStr = contentStr.replace(/\r/g, ''); 616 contentStr = contentStr.replace(/\n/g, ''); 617 contentStr = contentStr.replace(/"/g, '\''); 618 contentStr = contentStr.replace(/'/g, "\\'"); 619 620 if (!outputTeX) { 621 // Old GEONExT syntax, not (yet) supported as TeX output. 622 // Otherwise, the else clause should be used. 623 // That means, i.e. the <arc> tag and <sqrt> tag are not 624 // converted into TeX syntax. 625 contentStr = contentStr.replace(/&arc;/g, '∠'); 626 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 627 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 628 contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√'); 629 } 630 631 contentStr = contentStr.replace(/<value>/g, '<value>'); 632 contentStr = contentStr.replace(/<\/value>/g, '</value>'); 633 634 // Convert GEONExT syntax into JavaScript syntax 635 i = contentStr.indexOf('<value>'); 636 j = contentStr.indexOf('</value>'); 637 if (i >= 0) { 638 while (i >= 0) { 639 if (!outputTeX) { 640 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 641 // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"'; 642 } else { 643 plaintext += ' + "' + contentStr.slice(0, i) + '"'; 644 } 645 term = contentStr.slice(i + 7, j); 646 term = term.replace(/\s+/g, ''); // Remove all whitespace 647 if (expand === true) { 648 term = this.expandShortMath(term); 649 } 650 if (avoidGeonext2JS) { 651 res = term; 652 } else { 653 res = GeonextParser.geonext2JS(term, this.board); 654 } 655 res = res.replace(/\\"/g, "'"); 656 res = res.replace(/\\'/g, "'"); 657 658 // GEONExT-Hack: apply rounding once only. 659 if (res.indexOf('toFixed') < 0) { 660 // output of a value tag 661 if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) { 662 // may also be a string 663 plaintext += '+(' + res + ').toFixed(' + (Type.evaluate(this.visProp.digits)) + ')'; 664 } else { 665 plaintext += '+(' + res + ')'; 666 } 667 } else { 668 plaintext += '+(' + res + ')'; 669 } 670 671 contentStr = contentStr.slice(j + 8); 672 i = contentStr.indexOf('<value>'); 673 j = contentStr.indexOf('</value>'); 674 } 675 } 676 677 if (!outputTeX) { 678 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 679 plaintext = this.convertGeonextAndSketchometry2CSS(plaintext); 680 } else { 681 plaintext += ' + "' + contentStr + '"'; 682 } 683 684 // This should replace e.g. π by π 685 plaintext = plaintext.replace(/&/g, '&'); 686 plaintext = plaintext.replace(/"/g, "'"); 687 688 return plaintext; 689 }, 690 691 /** 692 * Converts the GEONExT tags <overline> and <arrow> to 693 * HTML span tags with proper CSS formatting. 694 * @private 695 * @see JXG.Text.generateTerm 696 * @see JXG.Text._setText 697 */ 698 convertGeonext2CSS: function (s) { 699 if (Type.isString(s)) { 700 s = s.replace( 701 /(<|<)overline(>|>)/g, 702 '<span style=text-decoration:overline;>' 703 ); 704 s = s.replace( 705 /(<|<)\/overline(>|>)/g, 706 '</span>' 707 ); 708 s = s.replace( 709 /(<|<)arrow(>|>)/g, 710 '<span style=text-decoration:overline;>' 711 ); 712 s = s.replace( 713 /(<|<)\/arrow(>|>)/g, 714 '</span>' 715 ); 716 } 717 718 return s; 719 }, 720 721 /** 722 * Converts the sketchometry tag <sketchofont> to 723 * HTML span tags with proper CSS formatting. 724 * @private 725 * @see JXG.Text.generateTerm 726 * @see JXG.Text._setText 727 */ 728 convertSketchometry2CSS: function (s) { 729 if (Type.isString(s)) { 730 s = s.replace( 731 /(<|<)sketchofont(>|>)/g, 732 '<span style=font-family:sketchometry-light;font-weight:500;>' 733 ); 734 s = s.replace( 735 /(<|<)\/sketchofont(>|>)/g, 736 '</span>' 737 ); 738 s = s.replace( 739 /(<|<)sketchometry-light(>|>)/g, 740 '<span style=font-family:sketchometry-light;font-weight:500;>' 741 ); 742 s = s.replace( 743 /(<|<)\/sketchometry-light(>|>)/g, 744 '</span>' 745 ); 746 } 747 748 return s; 749 }, 750 751 /** 752 * Alias for convertGeonext2CSS and convertSketchometry2CSS 753 * @private 754 * @see JXG.Text.convertGeonext2CSS 755 * @see JXG.Text.convertSketchometry2CSS 756 */ 757 convertGeonextAndSketchometry2CSS: function (s){ 758 s = this.convertGeonext2CSS(s); 759 s = this.convertSketchometry2CSS(s); 760 return s; 761 }, 762 763 /** 764 * Finds dependencies in a given term and notifies the parents by adding the 765 * dependent object to the found objects child elements. 766 * @param {String} content String containing dependencies for the given object. 767 * @private 768 */ 769 notifyParents: function (content) { 770 var search, 771 res = null; 772 773 // revert possible jc replacement 774 content = content.replace(/<value>/g, '<value>'); 775 content = content.replace(/<\/value>/g, '</value>'); 776 777 do { 778 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/; 779 res = search.exec(content); 780 781 if (res !== null) { 782 GeonextParser.findDependencies(this, res[1], this.board); 783 content = content.substr(res.index); 784 content = content.replace(search, ''); 785 } 786 } while (res !== null); 787 788 return this; 789 }, 790 791 // documented in element.js 792 getParents: function () { 793 var p; 794 if (this.relativeCoords !== undefined) { // Texts with anchor elements, excluding labels 795 p = [this.relativeCoords.usrCoords[1], this.relativeCoords.usrCoords[2], this.orgText]; 796 } else { // Other texts 797 p = [this.Z(), this.X(), this.Y(), this.orgText]; 798 } 799 800 if (this.parents.length !== 0) { 801 p = this.parents; 802 } 803 804 return p; 805 }, 806 807 bounds: function () { 808 var c = this.coords.usrCoords; 809 810 if (Type.evaluate(this.visProp.islabel) || this.board.unitY === 0 || this.board.unitX === 0) { 811 return [0, 0, 0, 0]; 812 } 813 return [c[1], c[2] + this.size[1] / this.board.unitY, c[1] + this.size[0] / this.board.unitX, c[2]]; 814 }, 815 816 getAnchorX: function () { 817 var a = Type.evaluate(this.visProp.anchorx); 818 if (a === 'auto') { 819 switch (this.visProp.position) { 820 case 'top': 821 case 'bot': 822 return 'middle'; 823 case 'rt': 824 case 'lrt': 825 case 'urt': 826 return 'left'; 827 case 'lft': 828 case 'llft': 829 case 'ulft': 830 default: 831 return 'right'; 832 } 833 } 834 return a; 835 }, 836 837 getAnchorY: function () { 838 var a = Type.evaluate(this.visProp.anchory); 839 if (a === 'auto') { 840 switch (this.visProp.position) { 841 case 'top': 842 case 'ulft': 843 case 'urt': 844 return 'bottom'; 845 case 'bot': 846 case 'lrt': 847 case 'llft': 848 return 'top'; 849 case 'rt': 850 case 'lft': 851 default: 852 return 'middle'; 853 } 854 } 855 return a; 856 }, 857 858 /** 859 * Computes the number of overlaps of a box of w pixels width, h pixels height 860 * and center (x, y) 861 * 862 * @private 863 * @param {Number} x x-coordinate of the center (screen coordinates) 864 * @param {Number} y y-coordinate of the center (screen coordinates) 865 * @param {Number} w width of the box in pixel 866 * @param {Number} h width of the box in pixel 867 * @return {Number} Number of overlapping elements 868 */ 869 getNumberofConflicts: function (x, y, w, h) { 870 var count = 0, 871 i, obj, le, 872 savePointPrecision; 873 874 // Set the precision of hasPoint to half the max if label isn't too long 875 savePointPrecision = this.board.options.precision.hasPoint; 876 // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 877 this.board.options.precision.hasPoint = (w + h) * 0.25; 878 // TODO: 879 // Make it compatible with the objects' visProp.precision attribute 880 for (i = 0, le = this.board.objectsList.length; i < le; i++) { 881 obj = this.board.objectsList[i]; 882 if (obj.visPropCalc.visible && 883 obj.elType !== 'axis' && 884 obj.elType !== 'ticks' && 885 obj !== this.board.infobox && 886 obj !== this && 887 obj.hasPoint(x, y)) { 888 889 count++; 890 } 891 } 892 this.board.options.precision.hasPoint = savePointPrecision; 893 894 return count; 895 }, 896 897 /** 898 * Sets the offset of a label element to the position with the least number 899 * of overlaps with other elements, while retaining the distance to its 900 * anchor element. Twelve different angles are possible. 901 * 902 * @returns {JXG.Text} Reference to the text object. 903 */ 904 setAutoPosition: function () { 905 var x, y, cx, cy, 906 anchorCoords, 907 // anchorX, anchorY, 908 w = this.size[0], 909 h = this.size[1], 910 start_angle, angle, 911 optimum = { 912 conflicts: Infinity, 913 angle: 0, 914 r: 0 915 }, 916 max_r, delta_r, 917 conflicts, offset, r, 918 num_positions = 12, 919 step = 2 * Math.PI / num_positions, 920 j, dx, dy, co, si; 921 922 if (this === this.board.infobox || 923 !this.visPropCalc.visible || 924 !Type.evaluate(this.visProp.islabel) || 925 !this.element) { 926 return this; 927 } 928 929 // anchorX = Type.evaluate(this.visProp.anchorx); 930 // anchorY = Type.evaluate(this.visProp.anchory); 931 offset = Type.evaluate(this.visProp.offset); 932 anchorCoords = this.element.getLabelAnchor(); 933 cx = anchorCoords.scrCoords[1]; 934 cy = anchorCoords.scrCoords[2]; 935 936 // Set dx, dy as the relative position of the center of the label 937 // to its anchor element ignoring anchorx and anchory. 938 dx = offset[0]; 939 dy = offset[1]; 940 941 conflicts = this.getNumberofConflicts(cx + dx, cy - dy, w, h); 942 if (conflicts === 0) { 943 return this; 944 } 945 // console.log(this.id, conflicts, w, h); 946 // r = Geometry.distance([0, 0], offset, 2); 947 948 r = 12; 949 max_r = 28; 950 delta_r = 0.2 * r; 951 952 start_angle = Math.atan2(dy, dx); 953 954 optimum.conflicts = conflicts; 955 optimum.angle = start_angle; 956 optimum.r = r; 957 958 while (optimum.conflicts > 0 && r < max_r) { 959 for (j = 1, angle = start_angle + step; j < num_positions && optimum.conflicts > 0; j++) { 960 co = Math.cos(angle); 961 si = Math.sin(angle); 962 963 x = cx + r * co; 964 y = cy - r * si; 965 966 conflicts = this.getNumberofConflicts(x, y, w, h); 967 if (conflicts < optimum.conflicts) { 968 optimum.conflicts = conflicts; 969 optimum.angle = angle; 970 optimum.r = r; 971 } 972 if (optimum.conflicts === 0) { 973 break; 974 } 975 angle += step; 976 } 977 r += delta_r; 978 } 979 // console.log(this.id, "after", optimum) 980 r = optimum.r; 981 co = Math.cos(optimum.angle); 982 si = Math.sin(optimum.angle); 983 this.visProp.offset = [r * co, r * si]; 984 985 if (co < -0.2) { 986 this.visProp.anchorx = 'right'; 987 } else if (co > 0.2) { 988 this.visProp.anchorx = 'left'; 989 } else { 990 this.visProp.anchorx = 'middle'; 991 } 992 993 return this; 994 } 995 }); 996 997 /** 998 * @class Construct and handle texts. 999 * 1000 * The coordinates can be relative to the coordinates of an element 1001 * given in {@link JXG.Options#text.anchor}. 1002 * 1003 * MathJaX, HTML and GEONExT syntax can be handled. 1004 * @pseudo 1005 * @description 1006 * @name Text 1007 * @augments JXG.Text 1008 * @constructor 1009 * @type JXG.Text 1010 * 1011 * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements. 1012 * <p> 1013 * Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T 1014 * constraint, or a function which takes no parameter and returns a number. Every parent element determines one coordinate. If a coordinate is 1015 * 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 1016 * 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 1017 * 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 1018 * parent elements are given they will be interpreted as homogeneous coordinates. 1019 * <p> 1020 * The text to display may be given as string or as function returning a string. 1021 * 1022 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display 1023 * 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. 1024 * @see JXG.Text 1025 * @example 1026 * // Create a fixed text at position [0,1]. 1027 * var t1 = board.create('text',[0,1,"Hello World"]); 1028 * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 1029 * <script type="text/javascript"> 1030 * var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1031 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 1032 * </script><pre> 1033 * @example 1034 * // Create a variable text at a variable position. 1035 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1036 * var graph = board.create('text', 1037 * [function(x){ return s.Value();}, 1, 1038 * function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);} 1039 * ] 1040 * ); 1041 * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 1042 * <script type="text/javascript"> 1043 * var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1044 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1045 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]); 1046 * </script><pre> 1047 * @example 1048 * // Create a text bound to the point A 1049 * var p = board.create('point',[0, 1]), 1050 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1051 * 1052 * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div> 1053 * <script type="text/javascript"> 1054 * (function() { 1055 * var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723', 1056 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1057 * var p = board.create('point',[0, 1]), 1058 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1059 * 1060 * })(); 1061 * 1062 * </script><pre> 1063 * 1064 */ 1065 JXG.createText = function (board, parents, attributes) { 1066 var t, 1067 attr = Type.copyAttributes(attributes, board.options, 'text'), 1068 coords = parents.slice(0, -1), 1069 content = parents[parents.length - 1]; 1070 1071 // downwards compatibility 1072 attr.anchor = attr.parent || attr.anchor; 1073 t = CoordsElement.create(JXG.Text, board, coords, attr, content); 1074 1075 if (!t) { 1076 throw new Error("JSXGraph: Can't create text with parent types '" + 1077 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." + 1078 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]"); 1079 } 1080 1081 if (attr.rotate !== 0 && attr.display === 'internal') { // This is the default value, i.e. no rotation 1082 t.addRotation(attr.rotate); 1083 } 1084 1085 return t; 1086 }; 1087 1088 JXG.registerElement('text', JXG.createText); 1089 1090 /** 1091 * @class Labels are text objects tied to other elements like points, lines and curves. 1092 * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)". 1093 * 1094 * @pseudo 1095 * @description 1096 * @name Label 1097 * @augments JXG.Text 1098 * @constructor 1099 * @type JXG.Text 1100 */ 1101 // See element.js#createLabel 1102 1103 /** 1104 * [[x,y], [w px, h px], [range] 1105 */ 1106 JXG.createHTMLSlider = function (board, parents, attributes) { 1107 var t, par, 1108 attr = Type.copyAttributes(attributes, board.options, 'htmlslider'); 1109 1110 if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) { 1111 throw new Error("JSXGraph: Can't create htmlslider with parent types '" + 1112 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." + 1113 "\nPossible parents are: [[x,y], [min, start, max]]"); 1114 } 1115 1116 // backwards compatibility 1117 attr.anchor = attr.parent || attr.anchor; 1118 attr.fixed = attr.fixed || true; 1119 1120 par = [parents[0][0], parents[0][1], 1121 '<form style="display:inline">' + 1122 '<input type="range" /><span></span><input type="text" />' + 1123 '</form>']; 1124 1125 t = JXG.createText(board, par, attr); 1126 t.type = Type.OBJECT_TYPE_HTMLSLIDER; 1127 1128 t.rendNodeForm = t.rendNode.childNodes[0]; 1129 1130 t.rendNodeRange = t.rendNodeForm.childNodes[0]; 1131 t.rendNodeRange.min = parents[1][0]; 1132 t.rendNodeRange.max = parents[1][2]; 1133 t.rendNodeRange.step = attr.step; 1134 t.rendNodeRange.value = parents[1][1]; 1135 1136 t.rendNodeLabel = t.rendNodeForm.childNodes[1]; 1137 t.rendNodeLabel.id = t.rendNode.id + '_label'; 1138 1139 if (attr.withlabel) { 1140 t.rendNodeLabel.innerHTML = t.name + '='; 1141 } 1142 1143 t.rendNodeOut = t.rendNodeForm.childNodes[2]; 1144 t.rendNodeOut.value = parents[1][1]; 1145 1146 try { 1147 t.rendNodeForm.id = t.rendNode.id + '_form'; 1148 t.rendNodeRange.id = t.rendNode.id + '_range'; 1149 t.rendNodeOut.id = t.rendNode.id + '_out'; 1150 } catch (e) { 1151 JXG.debug(e); 1152 } 1153 1154 t.rendNodeRange.style.width = attr.widthrange + 'px'; 1155 t.rendNodeRange.style.verticalAlign = 'middle'; 1156 t.rendNodeOut.style.width = attr.widthout + 'px'; 1157 1158 t._val = parents[1][1]; 1159 1160 if (JXG.supportsVML()) { 1161 /* 1162 * OnChange event is used for IE browsers 1163 * The range element is supported since IE10 1164 */ 1165 Env.addEvent(t.rendNodeForm, 'change', priv.HTMLSliderInputEventHandler, t); 1166 } else { 1167 /* 1168 * OnInput event is used for non-IE browsers 1169 */ 1170 Env.addEvent(t.rendNodeForm, 'input', priv.HTMLSliderInputEventHandler, t); 1171 } 1172 1173 t.Value = function () { 1174 return this._val; 1175 }; 1176 1177 return t; 1178 }; 1179 1180 JXG.registerElement('htmlslider', JXG.createHTMLSlider); 1181 1182 return { 1183 Text: JXG.Text, 1184 createText: JXG.createText, 1185 createHTMLSlider: JXG.createHTMLSlider 1186 }; 1187 }); 1188