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