1 /* 2 Copyright 2008-2014 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 this.plaintextOld = null; 74 75 this.isDraggable = false; 76 this.needsSizeUpdate = false; 77 78 this.element = this.board.select(attributes.anchor); 79 80 this.hiddenByParent = false; 81 82 if (this.element) { 83 if (this.visProp.islabel) { 84 this.relativeCoords = new Coords(Const.COORDS_BY_SCREEN, [parseFloat(coords[0]), parseFloat(coords[1])], this.board); 85 } else { 86 this.relativeCoords = new Coords(Const.COORDS_BY_USER, [parseFloat(coords[0]), parseFloat(coords[1])], this.board); 87 } 88 this.element.addChild(this); 89 90 this.X = function () { 91 var sx, coords, anchor; 92 93 if (this.visProp.islabel) { 94 sx = parseFloat(this.visProp.offset[0]); 95 anchor = this.element.getLabelAnchor(); 96 coords = new Coords(Const.COORDS_BY_SCREEN, [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board); 97 98 return coords.usrCoords[1]; 99 } 100 101 anchor = this.element.getTextAnchor(); 102 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1]; 103 }; 104 105 this.Y = function () { 106 var sy, coords, anchor; 107 108 if (this.visProp.islabel) { 109 sy = -parseFloat(this.visProp.offset[1]); 110 anchor = this.element.getLabelAnchor(); 111 coords = new Coords(Const.COORDS_BY_SCREEN, [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board); 112 113 return coords.usrCoords[2]; 114 } 115 116 anchor = this.element.getTextAnchor(); 117 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]; 118 }; 119 120 this.coords = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this.board); 121 this.isDraggable = true; 122 } else { 123 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 124 this.isDraggable = true; 125 } 126 this.X = Type.createFunction(coords[0], this.board, null, true); 127 this.Y = Type.createFunction(coords[1], this.board, null, true); 128 129 this.coords = new Coords(Const.COORDS_BY_USER, [this.X(), this.Y()], this.board); 130 } 131 132 this.Z = Type.createFunction(1, this.board, ''); 133 this.size = [1.0, 1.0]; 134 this.id = this.board.setId(this, 'T'); 135 136 // Set text before drawing 137 this._setUpdateText(content); 138 this.updateText(); 139 140 this.board.renderer.drawText(this); 141 142 if (!this.visProp.visible) { 143 this.board.renderer.hide(this); 144 } 145 146 if (typeof this.content === 'string') { 147 this.notifyParents(this.content); 148 } 149 150 this.elType = 'text'; 151 152 this.methodMap = Type.deepCopy(this.methodMap, { 153 setText: 'setTextJessieCode', 154 free: 'free', 155 move: 'setCoords' 156 }); 157 158 return this; 159 }; 160 161 JXG.Text.prototype = new GeometryElement(); 162 163 JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ { 164 /** 165 * @private 166 * Test if the the screen coordinates (x,y) are in a small stripe 167 * at the left side or at the right side of the text. 168 * Sensitivity is set in this.board.options.precision.hasPoint. 169 * @param {Number} x 170 * @param {Number} y 171 * @return {Boolean} 172 */ 173 hasPoint: function (x, y) { 174 var lft, rt, top, bot, 175 r = this.board.options.precision.hasPoint; 176 177 if (this.visProp.anchorx === 'right') { 178 lft = this.coords.scrCoords[1] - this.size[0]; 179 } else if (this.visProp.anchorx === 'middle') { 180 lft = this.coords.scrCoords[1] - 0.5 * this.size[0]; 181 } else { 182 lft = this.coords.scrCoords[1]; 183 } 184 rt = lft + this.size[0]; 185 186 if (this.visProp.anchory === 'top') { 187 bot = this.coords.scrCoords[2] + this.size[1]; 188 } else if (this.visProp.anchory === 'middle') { 189 bot = this.coords.scrCoords[2] + 0.5 * this.size[1]; 190 } else { 191 bot = this.coords.scrCoords[2]; 192 } 193 top = bot - this.size[1]; 194 195 if (this.visProp.dragarea === 'all') { 196 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r; 197 } 198 199 return (y >= top - r && y <= bot + r) && 200 ((x >= lft - r && x <= lft + 2 * r) || 201 (x >= rt - 2 * r && x <= rt + r)); 202 }, 203 204 /** 205 * This sets the updateText function of this element that depending on the type of text content passed. 206 * Used by {@link JXG.Text#_setText} and {@link JXG.Text} constructor. 207 * @param {String|Function|Number} text 208 * @private 209 */ 210 _setUpdateText: function (text) { 211 var updateText; 212 213 if (typeof text === 'function') { 214 this.updateText = function () { 215 this.plaintext = this.convertGeonext2CSS(text()); 216 }; 217 } else if (Type.isString(text) && !this.visProp.parse) { 218 this.updateText = function () { 219 this.plaintext = text; 220 }; 221 } else { 222 if (Type.isNumber(text)) { 223 this.content = text.toFixed(this.visProp.digits); 224 } else { 225 if (this.visProp.useasciimathml) { 226 // Convert via ASCIIMathML 227 this.content = "'`" + text + "`'"; 228 } else { 229 // Converts GEONExT syntax into JavaScript string 230 this.content = this.generateTerm(text); 231 } 232 } 233 updateText = this.board.jc.snippet(this.content, true, '', false); 234 this.updateText = function () { 235 this.plaintext = updateText(); 236 }; 237 } 238 }, 239 240 /** 241 * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because 242 * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode. 243 * @param {String|Function|Number} text 244 * @return {JXG.Text} 245 * @private 246 */ 247 _setText: function (text) { 248 this._setUpdateText(text); 249 250 // First evaluation of the string. 251 // We need this for display='internal' and Canvas 252 this.updateText(); 253 this.prepareUpdate().update().updateRenderer(); 254 255 // We do not call updateSize for the infobox to speed up rendering 256 if (!this.board.infobox || this.id !== this.board.infobox.id) { 257 this.updateSize(); // updateSize() is called at least once. 258 } 259 260 return this; 261 }, 262 263 /** 264 * Defines new content but converts < and > to HTML entities before updating the DOM. 265 * @param {String|function} text 266 */ 267 setTextJessieCode: function (text) { 268 var s; 269 270 this.visProp.castext = text; 271 272 if (typeof text === 'function') { 273 s = function () { 274 return Type.sanitizeHTML(text()); 275 }; 276 } else { 277 if (Type.isNumber(text)) { 278 s = text; 279 } else { 280 s = Type.sanitizeHTML(text); 281 } 282 } 283 284 return this._setText(s); 285 }, 286 287 /** 288 * Defines new content. 289 * @param {String|function} text 290 * @return {JXG.Text} Reference to the text object. 291 */ 292 setText: function (text) { 293 return this._setText(text); 294 }, 295 296 /** 297 * Recompute the width and the height of the text box. 298 * Update array this.size with pixel values. 299 * The result may differ from browser to browser 300 * by some pixels. 301 * In canvas an old IEs we use a very crude estimation of the dimensions of 302 * the textbox. 303 * In JSXGraph this.size is necessary for applying rotations in IE and 304 * for aligning text. 305 */ 306 updateSize: function () { 307 var tmp, s, that; 308 309 if (!Env.isBrowser || this.board.renderer.type === 'no') { 310 return this; 311 } 312 313 /** 314 * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode. 315 */ 316 if (this.visProp.display === 'html' || this.board.renderer.type === 'vml') { 317 if (JXG.exists(this.rendNode.offsetWidth)) { 318 s = [this.rendNode.offsetWidth, this.rendNode.offsetHeight]; 319 if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight 320 that = this; 321 window.setTimeout(function () { 322 that.size = [that.rendNode.offsetWidth, that.rendNode.offsetHeight]; 323 }, 0); 324 } else { 325 this.size = s; 326 } 327 } else { 328 this.size = this.crudeSizeEstimate(); 329 } 330 } else if (this.visProp.display === 'internal') { 331 if (this.board.renderer.type === 'svg') { 332 try { 333 tmp = this.rendNode.getBBox(); 334 this.size = [tmp.width, tmp.height]; 335 } catch (e) {} 336 } else if (this.board.renderer.type === 'canvas') { 337 this.size = this.crudeSizeEstimate(); 338 } 339 } 340 341 return this; 342 }, 343 344 /** 345 * A very crude estimation of the dimensions of the textbox in case nothing else is available. 346 * @return {Array} 347 */ 348 crudeSizeEstimate: function () { 349 return [parseFloat(this.visProp.fontsize) * this.plaintext.length * 0.45, parseFloat(this.visProp.fontsize) * 0.9]; 350 }, 351 352 /** 353 * Decode unicode entities into characters. 354 * @param {String} string 355 * @returns {String} 356 */ 357 utf8_decode : function (string) { 358 return string.replace(/(\w+);/g, function (m, p1) { 359 return String.fromCharCode(parseInt(p1, 16)); 360 }); 361 }, 362 363 /** 364 * Replace _{} by <sub> 365 * @param {String} te String containing _{}. 366 * @returns {String} Given string with _{} replaced by <sub>. 367 */ 368 replaceSub: function (te) { 369 if (!te.indexOf) { 370 return te; 371 } 372 373 var j, 374 i = te.indexOf('_{'); 375 376 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 377 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 378 /*jslint regexp: true*/ 379 380 while (i >= 0) { 381 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>'); 382 j = te.substr(i).indexOf('}'); 383 if (j >= 0) { 384 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>'); 385 } 386 i = te.indexOf('_{'); 387 } 388 389 i = te.indexOf('_'); 390 while (i >= 0) { 391 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>'); 392 i = te.indexOf('_'); 393 } 394 395 return te; 396 }, 397 398 /** 399 * Replace ^{} by <sup> 400 * @param {String} te String containing ^{}. 401 * @returns {String} Given string with ^{} replaced by <sup>. 402 */ 403 replaceSup: function (te) { 404 if (!te.indexOf) { 405 return te; 406 } 407 408 var j, 409 i = te.indexOf('^{'); 410 411 // the regexp in here are not used for filtering but to provide some kind of sugar for label creation, 412 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 413 /*jslint regexp: true*/ 414 415 while (i >= 0) { 416 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>'); 417 j = te.substr(i).indexOf('}'); 418 if (j >= 0) { 419 te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>'); 420 } 421 i = te.indexOf('^{'); 422 } 423 424 i = te.indexOf('^'); 425 while (i >= 0) { 426 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>'); 427 i = te.indexOf('^'); 428 } 429 430 return te; 431 }, 432 433 /** 434 * Return the width of the text element. 435 * @return {Array} [width, height] in pixel 436 */ 437 getSize: function () { 438 return this.size; 439 }, 440 441 /** 442 * Move the text to new coordinates. 443 * @param {number} x 444 * @param {number} y 445 * @return {object} reference to the text object. 446 */ 447 setCoords: function (x, y) { 448 if (Type.isArray(x) && x.length > 1) { 449 y = x[1]; 450 x = x[0]; 451 } 452 453 this.X = function () { 454 return x; 455 }; 456 457 this.Y = function () { 458 return y; 459 }; 460 461 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 462 463 // this should be a local update, otherwise there might be problems 464 // with the tick update routine resulting in orphaned tick labels 465 this.prepareUpdate().update().updateRenderer(); 466 467 return this; 468 }, 469 470 free: function () { 471 this.X = Type.createFunction(this.X(), this.board, ''); 472 this.Y = Type.createFunction(this.Y(), this.board, ''); 473 474 this.isDraggable = true; 475 }, 476 477 /** 478 * Evaluates the text. 479 * Then, the update function of the renderer 480 * is called. 481 */ 482 update: function () { 483 if (this.needsUpdate) { 484 if (!this.visProp.frozen) { 485 this.updateCoords(); 486 } 487 488 this.updateText(); 489 490 if (this.visProp.display === 'internal') { 491 this.plaintext = this.utf8_decode(this.plaintext); 492 } 493 494 this.checkForSizeUpdate(); 495 if (this.needsSizeUpdate) { 496 this.updateSize(); 497 } 498 this.updateTransform(); 499 } 500 501 return this; 502 }, 503 504 /** 505 * Used to save updateSize() calls. 506 * Called in JXG.Text.update 507 * That means this.update() has been called. 508 * More tests are in JXG.Renderer.updateTextStyle. The latter tests 509 * are one update off. But this should pose not too many problems, since 510 * it affects fontSize and cssClass changes. 511 * 512 * @private 513 */ 514 checkForSizeUpdate: function () { 515 if (this.board.infobox && this.id === this.board.infobox.id) { 516 this.needsSizeUpdate = false; 517 } else { 518 // For some magic reason it is more efficient on the iPad to 519 // call updateSize() for EVERY text element EVERY time. 520 this.needsSizeUpdate = (this.plaintextOld !== this.plaintext); 521 522 if (this.needsSizeUpdate) { 523 this.plaintextOld = this.plaintext; 524 } 525 } 526 527 }, 528 529 /** 530 * Updates the coordinates of the text element. 531 */ 532 updateCoords: function () { 533 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.X(), this.Y()]); 534 }, 535 536 /** 537 * The update function of the renderert 538 * is called. 539 * @private 540 */ 541 updateRenderer: function () { 542 if (this.needsUpdate) { 543 this.board.renderer.updateText(this); 544 this.needsUpdate = false; 545 } 546 return this; 547 }, 548 549 updateTransform: function () { 550 var i; 551 552 if (this.transformations.length === 0) { 553 return this; 554 } 555 556 for (i = 0; i < this.transformations.length; i++) { 557 this.transformations[i].update(); 558 } 559 560 return this; 561 }, 562 563 /** 564 * Converts the GEONExT syntax of the <value> terms into JavaScript. 565 * Also, all Objects whose name appears in the term are searched and 566 * the text is added as child to these objects. 567 * @private 568 * @see JXG.GeonextParser.geonext2JS. 569 */ 570 generateTerm: function (contentStr) { 571 var res, term, i, j, 572 plaintext = '""'; 573 574 // revert possible jc replacement 575 contentStr = contentStr || ''; 576 contentStr = contentStr.replace(/\r/g, ''); 577 contentStr = contentStr.replace(/\n/g, ''); 578 contentStr = contentStr.replace(/"/g, '\''); 579 contentStr = contentStr.replace(/'/g, "\\'"); 580 581 contentStr = contentStr.replace(/&arc;/g, '∠'); 582 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 583 contentStr = contentStr.replace(/<arc\s*\/>/g, '∠'); 584 contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√'); 585 586 contentStr = contentStr.replace(/<value>/g, '<value>'); 587 contentStr = contentStr.replace(/<\/value>/g, '</value>'); 588 589 // Convert GEONExT syntax into JavaScript syntax 590 i = contentStr.indexOf('<value>'); 591 j = contentStr.indexOf('</value>'); 592 if (i >= 0) { 593 while (i >= 0) { 594 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 595 term = contentStr.slice(i + 7, j); 596 res = GeonextParser.geonext2JS(term, this.board); 597 res = res.replace(/\\"/g, "'"); 598 res = res.replace(/\\'/g, "'"); 599 600 // GEONExT-Hack: apply rounding once only. 601 if (res.indexOf('toFixed') < 0) { 602 // output of a value tag 603 if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) { 604 // may also be a string 605 plaintext += '+(' + res + ').toFixed(' + (this.visProp.digits) + ')'; 606 } else { 607 plaintext += '+(' + res + ')'; 608 } 609 } else { 610 plaintext += '+(' + res + ')'; 611 } 612 613 contentStr = contentStr.slice(j + 8); 614 i = contentStr.indexOf('<value>'); 615 j = contentStr.indexOf('</value>'); 616 } 617 } 618 619 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 620 plaintext = this.convertGeonext2CSS(plaintext); 621 622 // This should replace π by π 623 plaintext = plaintext.replace(/&/g, '&'); 624 plaintext = plaintext.replace(/"/g, "'"); 625 626 return plaintext; 627 }, 628 629 /** 630 * Converts the GEONExT tags <overline> and <arrow> to 631 * HTML span tags with proper CSS formating. 632 * @private 633 * @see JXG.Text.generateTerm @see JXG.Text._setText 634 */ 635 convertGeonext2CSS: function (s) { 636 if (typeof s === 'string') { 637 s = s.replace(/<overline>/g, '<span style=text-decoration:overline>'); 638 s = s.replace(/<overline>/g, '<span style=text-decoration:overline>'); 639 s = s.replace(/<\/overline>/g, '</span>'); 640 s = s.replace(/<\/overline>/g, '</span>'); 641 s = s.replace(/<arrow>/g, '<span style=text-decoration:overline>'); 642 s = s.replace(/<arrow>/g, '<span style=text-decoration:overline>'); 643 s = s.replace(/<\/arrow>/g, '</span>'); 644 s = s.replace(/<\/arrow>/g, '</span>'); 645 } 646 647 return s; 648 }, 649 650 /** 651 * Finds dependencies in a given term and notifies the parents by adding the 652 * dependent object to the found objects child elements. 653 * @param {String} content String containing dependencies for the given object. 654 * @private 655 */ 656 notifyParents: function (content) { 657 var search, 658 res = null; 659 660 // revert possible jc replacement 661 content = content.replace(/<value>/g, '<value>'); 662 content = content.replace(/<\/value>/g, '</value>'); 663 664 do { 665 search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/; 666 res = search.exec(content); 667 668 if (res !== null) { 669 GeonextParser.findDependencies(this, res[1], this.board); 670 content = content.substr(res.index); 671 content = content.replace(search, ''); 672 } 673 } while (res !== null); 674 675 return this; 676 }, 677 678 bounds: function () { 679 var c = this.coords.usrCoords; 680 681 return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2] + this.size[1], c[1] + this.size[0], c[2]]; 682 }, 683 684 /** 685 * Sets x and y coordinate of the text. 686 * @param {Number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 687 * @param {Array} coords coordinates in screen/user units 688 * @param {Array} oldcoords previous coordinates in screen/user units 689 * @returns {JXG.Text} this element 690 */ 691 setPositionDirectly: function (method, coords, oldcoords) { 692 var dc, v, 693 c = new Coords(method, coords, this.board), 694 oldc = new Coords(method, oldcoords, this.board); 695 696 if (this.relativeCoords) { 697 if (this.visProp.islabel) { 698 dc = Statistics.subtract(c.scrCoords, oldc.scrCoords); 699 this.relativeCoords.scrCoords[1] += dc[1]; 700 this.relativeCoords.scrCoords[2] += dc[2]; 701 } else { 702 dc = Statistics.subtract(c.usrCoords, oldc.usrCoords); 703 this.relativeCoords.usrCoords[1] += dc[1]; 704 this.relativeCoords.usrCoords[2] += dc[2]; 705 } 706 } else { 707 dc = Statistics.subtract(c.usrCoords, oldc.usrCoords); 708 v = [this.Z(), this.X(), this.Y()]; 709 this.X = Type.createFunction(v[1] + dc[1], this.board, ''); 710 this.Y = Type.createFunction(v[2] + dc[2], this.board, ''); 711 712 /* 713 * In case of snapToGrid===true, first the coordinates of 714 * the new position is set, then they are rounded to the grid. 715 * The resulting coordinates are set as functions X(), Y(), 716 * becasue they are set again in updateCoords(). 717 */ 718 if (this.visProp.snaptogrid) { 719 this.coords.setCoordinates(Const.COORDS_BY_USER, c.usrCoords); 720 this.snapToGrid(); 721 this.X = Type.createFunction(this.coords.usrCoords[1], this.board, ''); 722 this.Y = Type.createFunction(this.coords.usrCoords[2], this.board, ''); 723 } 724 } 725 726 return this; 727 }, 728 729 /** 730 * Alias for {@link JXG.Element#handleSnapToGrid} 731 * @returns {JXG.Text} Reference to this element 732 */ 733 snapToGrid: function () { 734 return this.handleSnapToGrid(); 735 } 736 737 }); 738 739 /** 740 * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}. 741 * @pseudo 742 * @description 743 * @name Text 744 * @augments JXG.GeometryElement 745 * @constructor 746 * @type JXG.Text 747 * 748 * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements. 749 * <p> 750 * x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed, 751 * x and y are numbers. The position is variable if x or y are functions. 752 * <p> 753 * The text to display may be given as string or as function returning a string. 754 * 755 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display 756 * 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. 757 * @see JXG.Text 758 * @example 759 * // Create a fixed text at position [0,1]. 760 * var t1 = board.create('text',[0,1,"Hello World"]); 761 * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 762 * <script type="text/javascript"> 763 * var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 764 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 765 * </script><pre> 766 * @example 767 * // Create a variable text at a variable position. 768 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 769 * var graph = board.create('text', 770 * [function(x){ return s.Value();}, 1, 771 * function(){return "The value of s is"+s.Value().toFixed(2);} 772 * ] 773 * ); 774 * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 775 * <script type="text/javascript"> 776 * var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 777 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 778 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]); 779 * </script><pre> 780 */ 781 JXG.createText = function (board, parents, attributes) { 782 var t, 783 attr = Type.copyAttributes(attributes, board.options, 'text'); 784 785 // downwards compatibility 786 attr.anchor = attr.parent || attr.anchor; 787 788 t = new JXG.Text(board, parents[parents.length - 1], parents, attr); 789 790 if (typeof parents[parents.length - 1] !== 'function') { 791 t.parents = parents; 792 } 793 794 if (Type.evaluate(attr.rotate) !== 0 && attr.display === 'internal') { 795 t.addRotation(Type.evaluate(attr.rotate)); 796 } 797 798 return t; 799 }; 800 801 JXG.registerElement('text', JXG.createText); 802 803 /** 804 * [[x,y], [w px, h px], [range] 805 */ 806 JXG.createHTMLSlider = function (board, parents, attributes) { 807 var t, 808 attr = Type.copyAttributes(attributes, board.options, 'htmlslider'), 809 par; 810 811 if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) { 812 throw new Error("JSXGraph: Can't create htmlslider with parent types '" + 813 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." + 814 "\nPossible parents are: [[x,y], [min, start, max]]"); 815 } 816 817 // backwards compatibility 818 attr.anchor = attr.parent || attr.anchor; 819 attr.fixed = attr.fixed || true; 820 821 par = [parents[0][0], parents[0][1], 822 '<form style="display:inline">' + 823 '<input type="range" /><span></span><input type="text" />' + 824 '</form>']; 825 826 t = JXG.createText(board, par, attr); 827 t.type = Type.OBJECT_TYPE_HTMLSLIDER; 828 829 t.rendNodeForm = t.rendNode.childNodes[0]; 830 t.rendNodeForm.id = t.rendNode.id + '_form'; 831 832 t.rendNodeRange = t.rendNodeForm.childNodes[0]; 833 t.rendNodeRange.id = t.rendNode.id + '_range'; 834 t.rendNodeRange.min = parents[1][0]; 835 t.rendNodeRange.max = parents[1][2]; 836 t.rendNodeRange.step = attr.step; 837 t.rendNodeRange.value = parents[1][1]; 838 839 t.rendNodeLabel = t.rendNodeForm.childNodes[1]; 840 t.rendNodeLabel.id = t.rendNode.id + '_label'; 841 842 if (attr.withlabel) { 843 t.rendNodeLabel.innerHTML = t.name + '='; 844 } 845 846 t.rendNodeOut = t.rendNodeForm.childNodes[2]; 847 t.rendNodeOut.id = t.rendNode.id + '_out'; 848 t.rendNodeOut.value = parents[1][1]; 849 850 t.rendNodeRange.style.width = attr.widthrange + 'px'; 851 t.rendNodeRange.style.verticalAlign = 'middle'; 852 t.rendNodeOut.style.width = attr.widthout + 'px'; 853 854 t._val = parents[1][1]; 855 856 if (JXG.supportsVML()) { 857 /* 858 * OnChange event is used for IE browsers 859 * The range element is supported since IE10 860 */ 861 Env.addEvent(t.rendNodeForm, 'change', function () { 862 this._val = 1.0 * this.rendNodeRange.value; 863 this.rendNodeOut.value = this.rendNodeRange.value; 864 this.board.update(); 865 }, t); 866 } else { 867 /* 868 * OnInput event is used for non-IE browsers 869 */ 870 Env.addEvent(t.rendNodeForm, 'input', function () { 871 this._val = 1.0 * this.rendNodeRange.value; 872 this.rendNodeOut.value = this.rendNodeRange.value; 873 this.board.update(); 874 }, t); 875 } 876 877 t.Value = function() { 878 return this._val; 879 }; 880 881 /* 882 if (Type.evaluate(attr.rotate) !== 0 && attr.display === 'internal') { 883 t.addRotation(Type.evaluate(attr.rotate)); 884 } 885 */ 886 887 return t; 888 }; 889 890 JXG.registerElement('htmlslider', JXG.createHTMLSlider); 891 892 893 return { 894 Text: JXG.Text, 895 createText: JXG.createText, 896 createHTMLSlider: JXG.createHTMLSlider 897 }; 898 }); 899