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