window.onload = function () { var paper = Raphael("holder"); //var curve = paper.ellipse(100, 100, 1, 1).attr({"stroke-width": 0, fill: Color.red}); var text = "Betty Botter bought some butter but, she said, the butter's bitter. If I put it in my batter, it will make my batter bitter. But a bit of better butter will make my batter better. So, she bought a bit of butter, better than her bitter butter, and she put it in her batter, and the batter was not bitter. It was better Betty Botter bought a bit better butter."; var font = {font: "11px Arial", "font-style":"italic", opacity: 1, "fill": LABEL_COLOR, stroke: LABEL_COLOR, "stroke-width":.3}; var font = {font: "11px Arial", opacity: 1, "fill": LABEL_COLOR}; var boxWidth = 100 var AttributedStringIterator = function(text){ //this.text = this.rtrim(this.ltrim(text)); text = text.replace(/(\s)+/, " "); this.text = this.rtrim(text); /* if (beginIndex < 0 || beginIndex > endIndex || endIndex > length()) { throw new IllegalArgumentException("Invalid substring range"); } */ this.beginIndex = 0; this.endIndex = this.text.length; this.currentIndex = this.beginIndex; //console.group("[AttributedStringIterator]"); var i = 0; var string = this.text; var fullPos = 0; //console.log("string: \"" + string + "\", length: " + string.length); this.startWordOffsets = []; this.startWordOffsets.push(fullPos); // TODO: remove i 1000 while (i<1000) { var pos = string.search(/[ \t\n\f-\.\,]/); if (pos == -1) break; // whitespace start fullPos += pos; string = string.substr(pos); ////console.log("fullPos: " + fullPos + ", pos: " + pos + ", string: ", string); // remove whitespaces var pos = string.search(/[^ \t\n\f-\.\,]/); if (pos == -1) break; // whitespace end fullPos += pos; string = string.substr(pos); ////console.log("fullPos: " + fullPos); this.startWordOffsets.push(fullPos); i++; } //console.log("startWordOffsets: ", this.startWordOffsets); //console.groupEnd(); }; AttributedStringIterator.prototype = { getEndIndex: function(pos){ if (typeof(pos) == "undefined") return this.endIndex; var string = this.text.substr(pos, this.endIndex - pos); var posEndOfLine = string.search(/[\n]/); if (posEndOfLine == -1) return this.endIndex; else return pos + posEndOfLine; }, getBeginIndex: function(){ return this.beginIndex; }, isWhitespace: function(pos){ var str = this.text[pos]; var whitespaceChars = " \t\n\f"; return (whitespaceChars.indexOf(str) != -1); }, isNewLine: function(pos){ var str = this.text[pos]; var whitespaceChars = "\n"; return (whitespaceChars.indexOf(str) != -1); }, preceding: function(pos){ //console.group("[AttributedStringIterator.preceding]"); for(var i in this.startWordOffsets) { var startWordOffset = this.startWordOffsets[i]; if (pos < startWordOffset && i>0) { //console.log("startWordOffset: " + this.startWordOffsets[i-1]); //console.groupEnd(); return this.startWordOffsets[i-1]; } } //console.log("pos: " + pos); //console.groupEnd(); return this.startWordOffsets[i]; }, following: function(pos){ //console.group("[AttributedStringIterator.following]"); for(var i in this.startWordOffsets) { var startWordOffset = this.startWordOffsets[i]; if (pos < startWordOffset && i>0) { //console.log("startWordOffset: " + this.startWordOffsets[i]); //console.groupEnd(); return this.startWordOffsets[i]; } } //console.log("pos: " + pos); //console.groupEnd(); return this.startWordOffsets[i]; }, ltrim: function(str){ var patt2=/^\s+/g; return str.replace(patt2, ""); }, rtrim: function(str){ var patt2=/\s+$/g; return str.replace(patt2, ""); }, getLayout: function(start, limit){ return this.text.substr(start, limit - start); }, getCharAtPos: function(pos) { return this.text[pos]; } }; /* var TextMeasurer = function(paper, text, fontAttrs){ this.text = text; this.paper = paper; this.fontAttrs = fontAttrs; this.fStart = this.text.getBeginIndex(); }; TextMeasurer.prototype = { getLineBreakIndex: function(start, maxAdvance){ var localStart = start - this.fStart; }, getLayout: function(){ } } */ var LineBreakMeasurer = function(paper, text, fontAttrs){ this.paper = paper; this.text = new AttributedStringIterator(text); this.fontAttrs = fontAttrs; if (this.text.getEndIndex() - this.text.getBeginIndex() < 1) { throw {message: "Text must contain at least one character.", code: "IllegalArgumentException"}; } //this.measurer = new TextMeasurer(paper, this.text, this.fontAttrs); this.limit = this.text.getEndIndex(); this.pos = this.start = this.text.getBeginIndex(); this.rafaelTextObject = this.paper.text(100, 100, this.text.text).attr(fontAttrs).attr("text-anchor", "start"); this.svgTextObject = this.rafaelTextObject[0]; }; LineBreakMeasurer.prototype = { nextOffset: function(wrappingWidth, offsetLimit, requireNextWord) { //console.group("[nextOffset]"); var nextOffset = this.pos; if (this.pos < this.limit) { if (offsetLimit <= this.pos) { throw {message: "offsetLimit must be after current position", code: "IllegalArgumentException"}; } var charAtMaxAdvance = this.getLineBreakIndex(this.pos, wrappingWidth); //charAtMaxAdvance --; //console.log("charAtMaxAdvance:", charAtMaxAdvance, ", [" + this.text.getCharAtPos(charAtMaxAdvance) + "]"); if (charAtMaxAdvance == this.limit) { nextOffset = this.limit; //console.log("charAtMaxAdvance == this.limit"); } else if (this.text.isNewLine(charAtMaxAdvance)) { console.log("isNewLine"); nextOffset = charAtMaxAdvance+1; } else if (this.text.isWhitespace(charAtMaxAdvance)) { // TODO: find next noSpaceChar //return nextOffset; nextOffset = this.text.following(charAtMaxAdvance); } else { // Break is in a word; back up to previous break. /* var testPos = charAtMaxAdvance + 1; if (testPos == this.limit) { console.error("hbz..."); } else { nextOffset = this.text.preceding(charAtMaxAdvance); } */ nextOffset = this.text.preceding(charAtMaxAdvance); if (nextOffset <= this.pos) { nextOffset = Math.max(this.pos+1, charAtMaxAdvance); } } } if (nextOffset > offsetLimit) { nextOffset = offsetLimit; } //console.log("nextOffset: " + nextOffset); //console.groupEnd(); return nextOffset; }, nextLayout: function(wrappingWidth) { //console.groupCollapsed("[nextLayout]"); if (this.pos < this.limit) { var requireNextWord = false; var layoutLimit = this.nextOffset(wrappingWidth, this.limit, requireNextWord); //console.log("layoutLimit:", layoutLimit); if (layoutLimit == this.pos) { //console.groupEnd(); return null; } var result = this.text.getLayout(this.pos, layoutLimit); //console.log("layout: \"" + result + "\""); // remove end of line //var posEndOfLine = this.text.getEndIndex(this.pos); //if (posEndOfLine < result.length) // result = result.substr(0, posEndOfLine); this.pos = layoutLimit; //console.groupEnd(); return result; } else { //console.groupEnd(); return null; } }, getLineBreakIndex: function(pos, wrappingWidth) { //console.group("[getLineBreakIndex]"); //console.log("pos:"+pos + ", text: \""+ this.text.text.replace(/\n/g, "_").substr(pos, 1) + "\""); var bb = this.rafaelTextObject.getBBox(); var charNum = -1; try { var svgPoint = this.svgTextObject.getStartPositionOfChar(pos); //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.blue}); svgPoint.x = svgPoint.x + wrappingWidth; //svgPoint.y = bb.y; //console.log("svgPoint:", svgPoint); //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.red}); charNum = this.svgTextObject.getCharNumAtPosition(svgPoint); } catch (e){ console.warn("getStartPositionOfChar error, pos:" + pos); /* var testPos = pos + 1; if (testPos < this.limit) { return testPos } */ } //console.log("charNum:", charNum); if (charNum == -1) { //console.groupEnd(); return this.text.getEndIndex(pos); } else { // When case there is new line between pos and charnum then use this new line var newLineIndex = this.text.getEndIndex(pos); if (newLineIndex < charNum ) { console.log("newLineIndex <= charNum, newLineIndex:"+newLineIndex+", charNum:"+charNum, "\"" + this.text.text.substr(newLineIndex+1).replace(/\n/g, "↵") + "\""); //console.groupEnd(); return newLineIndex; } //var charAtMaxAdvance = this.text.text.substring(charNum, charNum + 1); var charAtMaxAdvance = this.text.getCharAtPos(charNum); //console.log("!!charAtMaxAdvance: " + charAtMaxAdvance); //console.groupEnd(); return charNum; } }, getPosition: function() { return this.pos; } }; // ****** function drawMultilineText(text, x, y, boxWidth, boxHeight, options) { var TEXT_PADDING = 3; var width = boxWidth - (2 * TEXT_PADDING); if (boxHeight) var height = boxHeight - (2 * TEXT_PADDING); var layouts = []; var measurer = new LineBreakMeasurer(paper, text, font); var lineHeight = measurer.rafaelTextObject.getBBox().height; console.log("text: ", text.replace(/\n/g, "↵")); if (height) { var availableLinesCount = parseInt(height/lineHeight); console.log("availableLinesCount: " + availableLinesCount); } var i = 1; while (measurer.getPosition() < measurer.text.getEndIndex()) { var layout = measurer.nextLayout(width); //console.log("LAYOUT: " + layout + ", getPosition: " + measurer.getPosition()); if (layout != null) { if (!availableLinesCount || i < availableLinesCount) { layouts.push(layout); } else { layouts.push(fitTextToWidth(layout + "...", boxWidth)); break; } } i++; }; console.log(layouts); measurer.rafaelTextObject.attr({"text": layouts.join("\n")}); //measurer.rafaelTextObject.attr({"text-anchor": "end"}); //measurer.rafaelTextObject.attr({"text-anchor": "middle"}); if (options) measurer.rafaelTextObject.attr({"text-anchor": options["text-anchor"]}); var bb = measurer.rafaelTextObject.getBBox(); //measurer.rafaelTextObject.attr({"x": x + boxWidth/2}); if (options["vertical-align"] == "top") measurer.rafaelTextObject.attr({"y": y + bb.height/2 + TEXT_PADDING}); else measurer.rafaelTextObject.attr({"y": y + height/2}); //var bb = measurer.rafaelTextObject.getBBox(); if (measurer.rafaelTextObject.attr("text-anchor") == "middle" ) measurer.rafaelTextObject.attr("x", x + boxWidth/2 + TEXT_PADDING/2); else if (measurer.rafaelTextObject.attr("text-anchor") == "end" ) measurer.rafaelTextObject.attr("x", x + boxWidth + TEXT_PADDING/2); else measurer.rafaelTextObject.attr("x", x + boxWidth/2 - bb.width/2 + TEXT_PADDING/2); var boxStyle = {stroke: Color.LightSteelBlue2, "stroke-width": 1.0, "stroke-dasharray": "- "}; /* var box = paper.rect(x+.0 + boxWidth/2 - bb.width/2+ TEXT_PADDING/2, y + .5 + boxHeight/2 - bb.height/2, width, height).attr(boxStyle); box.attr("height", bb.height); */ //var box = paper.rect(bb.x - .5 + bb.width/2 + TEXT_PADDING, bb.y + bb.height/2, bb.width, bb.height).attr(boxStyle); var textAreaCX = x + boxWidth/2; var textAreaCY = y + height/2; var dotLeftTop = paper.ellipse(x, y, 3, 3).attr({"stroke-width": 0, fill: Color.LightSteelBlue, stroke: "none"}); var dotCenter = paper.ellipse(textAreaCX, textAreaCY, 3, 3).attr({fill: Color.LightSteelBlue2, stroke: "none"}); /* // real bbox var bb = measurer.rafaelTextObject.getBBox(); var rect = paper.rect(bb.x+.5, bb.y + .5, bb.width, bb.height).attr({"stroke-width": 1}); */ var boxStyle = {stroke: Color.LightSteelBlue2, "stroke-width": 1.0, "stroke-dasharray": "- "}; var rect = paper.rect(x+.5, y + .5, boxWidth, boxHeight).attr(boxStyle); } /* for (var i=0; i<1; i++) { var t = text; //var t = "Высококвалифицирова"; var text = paper.text(300, 100, t).attr(font).attr("text-anchor", "start"); var bbText = text.getBBox(); paper.rect(300+.5, 100 + .5, bbText.width, bbText.height).attr({"stroke-width": 1}); console.log("t: ", t.replace(/\n/g, "↵")); while (measurer.getPosition() < measurer.text.getEndIndex()) { var layout = measurer.nextLayout(width); //console.log("LAYOUT: " + layout + ", getPosition: " + measurer.getPosition()); if (layout != null) layouts.push(layout); }; measurer.rafaelTextObject.attr("text", layouts.join("\n")); var bb = measurer.rafaelTextObject.getBBox(); var rect = paper.rect(bb.x+.5, bb.y + .5, bb.width, bb.height).attr({"stroke-width": 1}); lay.push(layouts); console.log(layouts); } */ var fitTextToWidth = function(original, width) { var text = original; // TODO: move attr on parameters var attr = {font: "11px Arial", opacity: 0}; // remove length for "..." var dots = paper.text(0, 0, "...").attr(attr).hide(); var dotsBB = dots.getBBox(); var maxWidth = width - dotsBB.width; var textElement = paper.text(0, 0, text).attr(attr).hide(); var bb = textElement.getBBox(); // it's a little bit incorrect with "..." while (bb.width > maxWidth && text.length > 0) { text = text.substring(0, text.length - 1); textElement.attr({"text": text}); bb = textElement.getBBox(); } // remove element from paper textElement.remove(); if (text != original) { text = text + "..."; } return text; } var x=100, y=90, height=20; var options = {"text-anchor": "middle", "boxHeight": 150, "vertical-align": "top"}; var options = {"boxHeight": 150, "vertical-align": "top"}; drawMultilineText(text, x, y, 150, 100, options); };