1 /* 2 Copyright 2008-2013 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 33 /*global JXG: true, define: true, window: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /* depends: 37 jxg 38 base/constants 39 base/coords 40 base/element 41 parser/geonext 42 math/statistics 43 utils/env 44 utils/type 45 */ 46 47 /** 48 * @fileoverview In this file the Text element is defined. 49 */ 50 51 define([ 52 'jxg', 'base/constants', 'base/coords', 'base/element', 'parser/geonext', 'math/statistics', 'utils/env', 'utils/type' 53 ], function (JXG, Const, Coords, GeometryElement, GeonextParser, Statistics, Env, Type) { 54 55 "use strict"; 56 57 /** 58 * Construct and handle texts. 59 * @class Text: On creation the GEONExT syntax 60 * of <value>-terms 61 * are converted into JavaScript syntax. 62 * The coordinates can be relative to the coordinates of an element "element". 63 * @constructor 64 * @return A new geometry element Text 65 */ 66 JXG.Text = function (board, content, coords, attributes) { 67 this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_OTHER); 68 69 var i, anchor; 70 71 this.content = ''; 72 this.plaintext = ''; 73 74 this.isDraggable = false; 75 this.needsSizeUpdate = false; 76 this.element = this.board.select(attributes.anchor); 77 78 this.hiddenByParent = false; 79 80 if (this.element) { 81 if (this.visProp.islabel) { 82 this.relativeCoords = new Coords(Const.COORDS_BY_SCREEN, [parseFloat(coords[0]), parseFloat(coords[1])], this.board); 83 } else { 84 this.relativeCoords = new Coords(Const.COORDS_BY_USER, [parseFloat(coords[0]), parseFloat(coords[1])], this.board); 85 } 86 this.element.addChild(this); 87 88 this.X = function () { 89 var sx, coords, anchor; 90 91 if (this.visProp.islabel) { 92 sx = parseFloat(this.visProp.offset[0]); 93 anchor = this.element.getLabelAnchor(); 94 coords = new Coords(Const.COORDS_BY_SCREEN, [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board); 95 96 return coords.usrCoords[1]; 97 } 98 99 anchor = this.element.getTextAnchor(); 100 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1]; 101 }; 102 103 this.Y = function () { 104 var sy, coords, anchor; 105 106 if (this.visProp.islabel) { 107 sy = -parseFloat(this.visProp.offset[1]); 108 anchor = this.element.getLabelAnchor(); 109 coords = new Coords(Const.COORDS_BY_SCREEN, [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board); 110 111 return coords.usrCoords[2]; 112 } 113 114 anchor = this.element.getTextAnchor(); 115 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]; 116 }; 117 118 this.coords = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this.board); 119 this.isDraggable = true; 120 } else { 121 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 122 this.isDraggable = true; 123 } 124 this.X = Type.createFunction(coords[0], this.board, null, true); 125 this.Y = Type.createFunction(coords[1], this.board, null, true); 126 127 this.coords = new Coords(Const.COORDS_BY_USER, [this.X(), this.Y()], this.board); 128 } 129 130 this.Z = Type.createFunction(1, this.board, ''); 131 this.size = [1.0, 1.0]; 132 this.id = this.board.setId(this, 'T'); 133 this.board.renderer.drawText(this); 134 135 this.setText(content); 136 this.updateSize(); 137 138 if (!this.visProp.visible) { 139 this.board.renderer.hide(this); 140 } 141 142 if (typeof this.content === 'string') { 143 this.notifyParents(this.content); 144 } 145 146 this.elType = 'text'; 147 148 this.methodMap = Type.deepCopy(this.methodMap, { 149 setText: 'setTextJessieCode', 150 free: 'free', 151 move: 'setCoords' 152 }); 153 154 return this; 155 }; 156 157 JXG.Text.prototype = new GeometryElement(); 158 159 JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ { 160 /** 161 * @private 162 * Test if the the screen coordinates (x,y) are in a small stripe 163 * at the left side or at the right side of the text. 164 * Sensitivity is set in this.board.options.precision.hasPoint. 165 * @param {Number} x 166 * @param {Number} y 167 * @return {Boolean} 168 */ 169 hasPoint: function (x, y) { 170 var lft, rt, top, bot, 171 r = this.board.options.precision.hasPoint; 172 173 if (this.visProp.anchorx === 'right') { 174 lft = this.coords.scrCoords[1] - this.size[0]; 175 } else if (this.visProp.anchorx === 'middle') { 176 lft = this.coords.scrCoords[1] - 0.5 * this.size[0]; 177 } else { 178 lft = this.coords.scrCoords[1]; 179 } 180 rt = lft + this.size[0]; 181 182 if (this.visProp.anchory === 'top') { 183 bot = this.coords.scrCoords[2] + this.size[1]; 184 } else if (this.visProp.anchory === 'middle') { 185 bot = this.coords.scrCoords[2] + 0.5 * this.size[1]; 186 } else { 187 bot = this.coords.scrCoords[2]; 188 } 189 top = bot - this.size[1]; 190 191 if (this.visProp.dragarea === 'all') { 192 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r; 193 } 194 195 return (y >= top - r && y <= bot + r) && 196 ((x >= lft - r && x <= lft + 2 * r) || 197 (x >= rt - 2 * r && x <= rt + r)); 198 }, 199 200 /** 201 * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because 202 * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode. 203 * @param {String|Function|Number} text 204 * @return {JXG.Text} 205 * @private 206 */ 207 _setText: function (text) { 208 var updateText; 209 210 this.needsSizeUpdate = false; 211 212 if (typeof text === 'function') { 213 this.updateText = function () { 214 this.plaintext = text(); 215 }; 216 this.needsSizeUpdate = true; 217 } else if (Type.isString(text) && !this.visProp.parse) { 218 this.updateText = function () { 219 this.plaintext = text; 220 }; 221 this.needsSizeUpdate = false; // true; 222 } else { 223 if (Type.isNumber(text)) { 224 this.content = text.toFixed(this.visProp.digits); 225 } else { 226 if (this.visProp.useasciimathml) { 227 // Convert via ASCIIMathML 228 this.content = "'`" + text + "`'"; 229 this.needsSizeUpdate = true; 230 } else { 231 // Converts GEONExT syntax into JavaScript string 232 this.content = this.generateTerm(text); 233 } 234 } 235 updateText = this.board.jc.snippet(this.content, true, '', false); 236 this.updateText = function () { 237 this.plaintext = updateText(); 238 }; 239 } 240 241 // First evaluation of the string. 242 // We need this for display='internal' and Canvas 243 this.updateText(); 244 245 this.prepareUpdate().update().updateRenderer(); 246 247 // call updateSize() at least once. 248 if (this.needsSizeUpdate) { 249 this.updateSize(); 250 } 251 252 return this; 253 }, 254 255 /** 256 * Defines new content but converts < and > to HTML entities before updating the DOM. 257 * @param {String|function} text 258 */ 259 setTextJessieCode: function (text) { 260 var s; 261 262 this.visProp.castext = text; 263 264 if (typeof text === 'function') { 265 s = function () { 266 return Type.sanitizeHTML(text()); 267 }; 268 } else { 269 if (Type.isNumber(text)) { 270 s = text; 271 } else { 272 s = Type.sanitizeHTML(text); 273 274 } 275 } 276 277 return this._setText(s); 278 }, 279 280 /** 281 * Defines new content. 282 * @param {String|function} text 283 * @return {JXG.Text} Reference to the text object. 284 */ 285 setText: function (text) { 286 return this._setText(text); 287 }, 288 289 /** 290 * Recompute the width and the height of the text box. 291 * Update array this.size with pixel values. 292 * The result may differ from browser to browser 293 * by some pixels. 294 * In IE and canvas we use a very crude estimation of the dimensions of 295 * the textbox. 296 * In JSXGraph this.size is necessary for applying rotations in IE and 297 * for aligning text. 298 */ 299 updateSize: function () { 300 var tmp, s, that; 301 302 if (!Env.isBrowser) { 303 return this; 304 } 305 306 if (this.visProp.display === 'html' && this.board.renderer.type !== 'vml' && this.board.renderer.type !== 'no') { 307 s = [this.rendNode.offsetWidth, this.rendNode.offsetHeight]; 308 if (s[0] === 0 && s[1] === 0) { 309 // Some browsers need some time to set offsetWidth and offsetHeight 310 that = this; 311 window.setTimeout(function () { 312 that.size = [that.rendNode.offsetWidth, that.rendNode.offsetHeight]; 313 }, 0); 314 } else { 315 this.size = s; 316 } 317 318 } else if (this.visProp.display === 'internal' && this.board.renderer.type === 'svg') { 319 try { 320 tmp = this.rendNode.getBBox(); 321 this.size = [tmp.width, tmp.height]; 322 } catch (e) { 323 } 324 } else if (this.board.renderer.type === 'vml' || (this.visProp.display === 'internal' && this.board.renderer.type === 'canvas')) { 325 // Here comes a very crude estimation of the dimensions of the textbox. 326 this.size = [parseFloat(this.visProp.fontsize) * this.plaintext.length * 0.45, parseFloat(this.visProp.fontsize) * 0.9]; 327 } 328 329 return this; 330 }, 331 332 /** 333 * Decode unicode entities into characters. 334 * @param {String} string 335 * @returns {String} 336 */ 337 utf8_decode : function (string) { 338 return string.replace(/(\w+);/g, function (m, p1) { 339 return String.fromCharCode(parseInt(p1, 16)); 340 }); 341 }, 342 343 /** 344 * Replace _{} by <sub> 345 * @param {String} te String containing _{}. 346 * @returns {String} Given string with _{} replaced by <sub>. 347 */ 348 replaceSub: function (te) { 349 if (!te.indexOf) { 350 return te; 351 } 352 353 var j, 354 i = te.indexOf('_{'); 355 356 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 357 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 358 /*jslint regexp: true*/ 359 360 while (i >= 0) { 361 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>'); 362 j = te.substr(i).indexOf('}'); 363 if (j >= 0) { 364 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>'); 365 } 366 i = te.indexOf('_{'); 367 } 368 369 i = te.indexOf('_'); 370 while (i >= 0) { 371 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>'); 372 i = te.indexOf('_'); 373 } 374 375 return te; 376 }, 377 378 /** 379 * Replace ^{} by <sup> 380 * @param {String} te String containing ^{}. 381 * @returns {String} Given string with ^{} replaced by <sup>. 382 */ 383 replaceSup: function (te) { 384 if (!te.indexOf) { 385 return te; 386 } 387 388 var j, 389 i = te.indexOf('^{'); 390 391 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 392 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 393 /*jslint regexp: true*/ 394 395 while (i >= 0) { 396 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>'); 397 j = te.substr(i).indexOf('}'); 398 if (j >= 0) { 399 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>'); 400 } 401 i = te.indexOf('^{'); 402 } 403 404 i = te.indexOf('^'); 405 while (i >= 0) { 406 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>'); 407 i = te.indexOf('^'); 408 } 409 410 return te; 411 }, 412 413 /** 414 * Return the width of the text element. 415 * @return {Array} [width, height] in pixel 416 */ 417 getSize: function () { 418 return this.size; 419 }, 420 421 /** 422 * Move the text to new coordinates. 423 * @param {number} x 424 * @param {number} y 425 * @return {object} reference to the text object. 426 */ 427 setCoords: function (x, y) { 428 if (Type.isArray(x) && x.length > 1) { 429 y = x[1]; 430 x = x[0]; 431 } 432 433 this.X = function () { 434 return x; 435 }; 436 437 this.Y = function () { 438 return y; 439 }; 440 441 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 442 443 // this should be a local update, otherwise there might be problems 444 // with the tick update routine resulting in orphaned tick labels 445 this.prepareUpdate().update().updateRenderer(); 446 447 return this; 448 }, 449 450 free: function () { 451 this.X = Type.createFunction(this.X(), this.board, ''); 452 this.Y = Type.createFunction(this.Y(), this.board, ''); 453 454 this.isDraggable = true; 455 }, 456 457 /** 458 * Evaluates the text. 459 * Then, the update function of the renderer 460 * is called. 461 */ 462 update: function () { 463 if (this.needsUpdate) { 464 if (!this.visProp.frozen) { 465 this.updateCoords(); 466 } 467 this.updateText(); 468 469 if (this.visProp.display === 'internal') { 470 this.plaintext = this.utf8_decode(this.plaintext); 471 } 472 473 if (this.needsSizeUpdate) { 474 this.updateSize(); 475 } 476 this.updateTransform(); 477 478 } 479 480 return this; 481 }, 482 483 /** 484 * Updates the coordinates of the text element. 485 */ 486 updateCoords: function () { 487 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.X(), this.Y()]); 488 }, 489 490 /** 491 * The update function of the renderert 492 * is called. 493 * @private 494 */ 495 updateRenderer: function () { 496 if (this.needsUpdate) { 497 this.board.renderer.updateText(this); 498 this.needsUpdate = false; 499 } 500 return this; 501 }, 502 503 updateTransform: function () { 504 var i; 505 506 if (this.transformations.length === 0) { 507 return this; 508 } 509 510 for (i = 0; i < this.transformations.length; i++) { 511 this.transformations[i].update(); 512 } 513 514 return this; 515 }, 516 517 /** 518 * Converts the GEONExT syntax of the <value> terms into JavaScript. 519 * Also, all Objects whose name appears in the term are searched and 520 * the text is added as child to these objects. 521 * @private 522 * @see JXG.GeonextParser.geonext2JS. 523 */ 524 generateTerm: function (contentStr) { 525 var res, term, i, j, 526 plaintext = '""'; 527 528 // revert possible jc replacement 529 contentStr = contentStr || ''; 530 contentStr = contentStr.replace(/\r/g, ''); 531 contentStr = contentStr.replace(/\n/g, ''); 532 contentStr = contentStr.replace(/"/g, '\''); 533 contentStr = contentStr.replace(/'/g, "\\'"); 534 contentStr = contentStr.replace(/&arc;/g, '∠'); 535 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 536 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 537 contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√'); 538 contentStr = contentStr.replace(/<value>/g, '<value>'); 539 contentStr = contentStr.replace(/<\/value>/g, '</value>'); 540 541 // Convert GEONExT syntax into JavaScript syntax 542 i = contentStr.indexOf('<value>'); 543 j = contentStr.indexOf('</value>'); 544 if (i >= 0) { 545 this.needsSizeUpdate = true; 546 while (i >= 0) { 547 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 548 term = contentStr.slice(i + 7, j); 549 res = GeonextParser.geonext2JS(term, this.board); 550 res = res.replace(/\\"/g, "'"); 551 res = res.replace(/\\'/g, "'"); 552 553 // GEONExT-Hack: apply rounding once only. 554 if (res.indexOf('toFixed') < 0) { 555 // output of a value tag 556 if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) { 557 // may also be a string 558 plaintext += '+(' + res + ').toFixed(' + (this.visProp.digits) + ')'; 559 } else { 560 plaintext += '+(' + res + ')'; 561 } 562 } else { 563 plaintext += '+(' + res + ')'; 564 } 565 566 contentStr = contentStr.slice(j + 8); 567 i = contentStr.indexOf('<value>'); 568 j = contentStr.indexOf('</value>'); 569 } 570 } 571 572 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 573 plaintext = plaintext.replace(/<overline>/g, '<span style=text-decoration:overline>'); 574 plaintext = plaintext.replace(/<overline</g, '<span style=text-decoration:overline>'); 575 plaintext = plaintext.replace(/<\/overline>/g, '</span>'); 576 plaintext = plaintext.replace(/<\/overline>/g, '</span>'); 577 plaintext = plaintext.replace(/<arrow>/g, '<span style=text-decoration:overline>'); 578 plaintext = plaintext.replace(/<arrow>/g, '<span style=text-decoration:overline>'); 579 plaintext = plaintext.replace(/<\/arrow>/g, '</span>'); 580 plaintext = plaintext.replace(/<\/arrow>/g, '</span>'); 581 582 // This should replace π by π 583 plaintext = plaintext.replace(/&/g, '&'); 584 plaintext = plaintext.replace(/"/g, "'"); 585 586 return plaintext; 587 }, 588 589 /** 590 * Finds dependencies in a given term and notifies the parents by adding the 591 * dependent object to the found objects child elements. 592 * @param {String} content String containing dependencies for the given object. 593 * @private 594 */ 595 notifyParents: function (content) { 596 var search, 597 res = null; 598 599 // revert possible jc replacement 600 content = content.replace(/<value>/g, '<value>'); 601 content = content.replace(/<\/value>/g, '</value>'); 602 603 do { 604 search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/; 605 res = search.exec(content); 606 607 if (res !== null) { 608 GeonextParser.findDependencies(this, res[1], this.board); 609 content = content.substr(res.index); 610 content = content.replace(search, ''); 611 } 612 } while (res !== null); 613 614 return this; 615 }, 616 617 bounds: function () { 618 var c = this.coords.usrCoords; 619 620 return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2] + this.size[1], c[1] + this.size[0], c[2]]; 621 }, 622 623 /** 624 * Sets x and y coordinate of the text. 625 * @param {Number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 626 * @param {Array} coords coordinates in screen/user units 627 * @param {Array} oldcoords previous coordinates in screen/user units 628 * @returns {JXG.Text} this element 629 */ 630 setPositionDirectly: function (method, coords, oldcoords) { 631 var dc, v, 632 c = new Coords(method, coords, this.board), 633 oldc = new Coords(method, oldcoords, this.board); 634 635 if (this.relativeCoords) { 636 if (this.visProp.islabel) { 637 dc = Statistics.subtract(c.scrCoords, oldc.scrCoords); 638 this.relativeCoords.scrCoords[1] += dc[1]; 639 this.relativeCoords.scrCoords[2] += dc[2]; 640 } else { 641 dc = Statistics.subtract(c.usrCoords, oldc.usrCoords); 642 this.relativeCoords.usrCoords[1] += dc[1]; 643 this.relativeCoords.usrCoords[2] += dc[2]; 644 } 645 } else { 646 dc = Statistics.subtract(c.usrCoords, oldc.usrCoords); 647 v = [this.Z(), this.X(), this.Y()]; 648 this.X = Type.createFunction(v[1] + dc[1], this.board, ''); 649 this.Y = Type.createFunction(v[2] + dc[2], this.board, ''); 650 } 651 652 return this; 653 } 654 655 }); 656 657 /** 658 * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}. 659 * @pseudo 660 * @description 661 * @name Text 662 * @augments JXG.GeometryElement 663 * @constructor 664 * @type JXG.Text 665 * 666 * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements. 667 * <p> 668 * x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed, 669 * x and y are numbers. The position is variable if x or y are functions. 670 * <p> 671 * The text to display may be given as string or as function returning a string. 672 * 673 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display 674 * 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. 675 * @see JXG.Text 676 * @example 677 * // Create a fixed text at position [0,1]. 678 * var t1 = board.create('text',[0,1,"Hello World"]); 679 * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 680 * <script type="text/javascript"> 681 * var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 682 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 683 * </script><pre> 684 * @example 685 * // Create a variable text at a variable position. 686 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 687 * var graph = board.create('text', 688 * [function(x){ return s.Value();}, 1, 689 * function(){return "The value of s is"+s.Value().toFixed(2);} 690 * ] 691 * ); 692 * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 693 * <script type="text/javascript"> 694 * var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 695 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 696 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]); 697 * </script><pre> 698 */ 699 JXG.createText = function (board, parents, attributes) { 700 var t, 701 attr = Type.copyAttributes(attributes, board.options, 'text'); 702 703 // downwards compatibility 704 attr.anchor = attr.parent || attr.anchor; 705 706 t = new JXG.Text(board, parents[parents.length - 1], parents, attr); 707 708 if (typeof parents[parents.length - 1] !== 'function') { 709 t.parents = parents; 710 } 711 712 if (Type.evaluate(attr.rotate) !== 0 && attr.display === 'internal') { 713 t.addRotation(Type.evaluate(attr.rotate)); 714 } 715 716 return t; 717 }; 718 719 JXG.registerElement('text', JXG.createText); 720 721 return { 722 Text: JXG.Text, 723 createText: JXG.createText 724 }; 725 }); 726