is it possible to have jQuery/javascript detect where a string is broken (in order to fit into CSS width constraints) so as to insert DOM elements before the beginning of a new line?
I came up with an approach, but it might be overkill for your purposes, so take this into account.
You need to create a clone of the element, empty the original, then move each word back into the original element. If the height changes at any point, there's a line-break before that word. This would be fairly simple to do using $(el).text(), but it gets more complicated if there can be other tags inside, not just text. I tried explaining how to break it down by node in this answer box, but found it easier just to create a jQuery plugin in a jsFiddle. Link here: http://jsfiddle.net/nathan/qkmse/ (Gist).
It won't handle floated elements all that well, and there are a few other situations where it'll fall over. Let me know if you'd like more options, or if it doesn't quite work for your purposes, or if you're not sure how to apply it and I'll try to help.
Here is one approach. Note: I do not see a ideal solution without using monospace fonts. The equal with characters make this task much easier.
Equal width characters
Calculate the size of one character
Calculate the size of the container
Find characters per row
Find where the row will break (ie whitespace, dashes, etc)
Get all breaking indexes.
Have a look at the jsfiddle for associated html. I have not completed this function. More checks need to be put in when calculating the breaking index. Right now it is using lastIndexOf(' '), but this ignores that the next index could be a space, or the current. Also I am not accounting for other line-breaking characters. However this should be a great starting point.
var text = $('#text').text(), // "lorem ipsum ... "
len = text.length, // total chars
width = $('#text').width(), // container width
span = $('<span />').append('a').appendTo('#sandbox'),
charWidth = span.width(), // add single character to span and test width
charsPerRow = Math.floor(width/charWidth); // total characters that can fit in one row
var breakingIndexes = [], // will contain indexes of all soft-breaks
gRowStart = 0, // global row start index
gRowEnd = charsPerRow;// global row end index
while(gRowEnd < len){
var rowEnd = text.substring(gRowStart, gRowEnd).lastIndexOf(' '); // add more checks for break conditions here
breakingIndexes.push(gRowStart + rowEnd); // add breaking index to array
gRowStart = gRowStart + rowEnd + 1; // next start is the next char
gRowEnd = gRowStart + charsPerRow; // global end inxex is start + charsperrow
}
var text2 = $('#text2').text(); // "lorem ipsum ... " now not width bound
var start = 0, newText = '';
for(var i=0; i < breakingIndexes.length; i++){
newText += text2.substring(start, breakingIndexes[i]) + '<br />'; // add hard breaks
start = breakingIndexes[i]; // update start
}
$('#text2').html(newText); // output with breaks
http://jsfiddle.net/Y5Ftn/1/
this is my script, that takes text, and then makes each line a span
CSS:
margin: 0;
padding: 0;
}
.title{
width: 300px;
background-color: rgba(233,233,233,0.5);
line-height: 20px;
}
span{
color: white;
background-color: red;
display: inline-block;
font-size: 30px;
}
a{
text-decoration: none;
color: black;
}
html
<div class="title">
SOME TEXT LONG TEXT ANDTHISISLONG AND THIS OTHER TEXT
</div>
JS
$(function(){
$(".title").find("a").each(function(){
var $this = $(this);
var originalText = $this.text();
$this.empty();
var sections = [];
$.each( originalText.split(" "), function(){
var $span = $("<span>" + this + "</span>");
$this.append($span);
var index = $span.position().top;
if( sections[index] === undefined ){
sections[index] = "";
}
sections[index] += $span.text() + " ";
});
$this.empty();
for(var i = 0; i< sections.length; i++){
if( sections[i] !== undefined ){
var spanText = $.trim(sections[i]);
$this.append("<span>" + spanText + "</span>");
}
}
});
});
You have to got jQuery included.
Alternatively you could compare width of the text block with its parent's width. If the block is at least 98% of the width of its parent, pretty sure it breaks
I don't know of any built in jQuery of javascript function to do this. You could, however, do it yourself, but it would be potentially slow if you have a lot of text.
In theory, you could make sure the height is set to auto, remove the text, and then word by word reinsert it. On a change in the height, you remove the last word, and insert your dom element. Again, this will be slow if there is a lot of text, and the better way to do this would be to not change the original element, but do it in another element which could be hidden, and then replace the original element on completion. That way, your user wouldn't see the content disappear and then come back word by word.
Another way would be to use a similar principle, and start with an empty element of the same size with height auto, and insert a new character and a space until you get a new line. From there, you can use this as an approximation with the above techinique, or you can blindly add up the length of each string until you find a new line, taking into account the width of your dom element. This technique works better with monospaced fonts though, which is where using it only as an approximation comes in.
That said, there is a way to measure text using canvas, but that may be extreme. In theory it would be, create a canvas element, get the context, set all the font properties, and then use the context.measureText() method. An example of it use can be found here.
Not sure what exactly your use case is but http://code.google.com/p/hyphenator/ may be a solution to your problem or if you dig into the hyphenator.js source code, you may be able to find the code you are looking for.
I think it would be easier to detect using a regex -- much less code while retaining efficiency.
Here's something that worked for me:
if ((/(\r\n|\n|\r)/.test($(this).val()))) {
alert('there are line breaks here')
}
I'm not sure if this will work with your broken strings, but it works for detecting line-breks with jQuery.
Related
I'm looking for a way to fill up a div or a paragraph width, inside a razor page, with a dynamic string. for example, let's say I have the sentence "I love cookies very much" and I have a a div which have a width and a height, I put "I love cookies" at the beginning and "much" at the end, And in the middle I want to fill the div with "very"s without wrapping or overflow.
<div class="col-md-6">
I love cookies
<script>js_magic()</script>
much!
</div>
desired output:
I love cookies very very very very very very very very much!
as if, the word "very" should repeat for as long as it have enough width.
In C# i, sort of, know how to do it, I use graphics and font, stringlength etc... but js and jQuery always look like a big mess to me.
in this case you need to know the drawing size of very by adding at least one and then measuring it's width and calculate how many you can insert to fill the width. Working JSBin
also add this style to your container white-space: nowrap; to prevent unneeded wrapping
UPDATE: Comments added
//this function takes 2 strings and inserts str2 into str at a specific position and returns the result
function insertString(str,ind,str2){
return str.slice(0, ind) + str2 + str.slice(ind);
}
function fillInVery(){
// capture the text content from HTML container
var s = $("#container").text();
// the text to be inserted multiple times (wrapped in order to be selected)
var very = "<span class='very'>very </span>";
console.log(s);
// insert one VERY string to the text
s = insertString(s,15,very);
// replace the old string with the new one to be rendered
$("#container").html(s);
console.log(s);
// get the width of the inserted and rendered VERY word
var veryw = $('.very:first').width();
var contw = $('#container').width();
var contpw = $('#container').parent().width();
// calculate the difference (space) between the container and its parent and divide it over the width of one VERY to get how many can fit in the remaining space
var countInsert = Math.floor((contpw - contw)/veryw);
// add VERY word to fill the space
for(var i =0 ; i< countInsert; i++){
s = insertString(s,15,very);
}
// replace the text and render it
$("#container").html(s);
}
fillInVery();
I'm trying to get text to wrap along an angle. This illustrates what I'm trying to do better than I can describe it:
http://bdub.ca/angle.png http://bdub.ca/angle.png
I found a solution here but it uses a heck of a lot of empty floated divs to create the effect. I will need to do this a bunch of times on a page so it would be better to have a solution that is lighter in weight. JavaScript is okay if it's something that I can just run on page load to spare the DOM from an overload of extra elements.
My brainstorming for a JS solution got as far as trying to figure out how to wrap each line in a span and set the left margins of the spans successively larger. The caveat is that the text is a paragraph that will auto wrap to fit in the container - I unfortunately can't ask my client to insert line breaks from Wordpress - so to wrap each line in a span would involve somehow detecting the automatic line breaks using javascript.
Any suggestions?
It's very tricky in that you can't legitimately (w3c standards; monitor screen resolution size and privacy) detect the actual width of characters. You could set the font-size to a specific width and insert a line-break yourself when it comes close to the width. (in b4 monitor)
so css: .paraIncrement { font-size: 12pt; }
I'm a little rusty with javascript so let's psudo-code this one:
outputstr[] = array();
int index = 0;
int startmax = 80;
int innerCount = 0;
for (int i = 0; paragraph.length; i++) {
outputstr[index] += paragraph[i];
innercount++;
if (innercount == startmax) {
startmax -= 5; // how much you want the indent to shrink progressively.
innercount = 0;
index++;
}
}
for (int i = 0; i < output.length(); i++) {
echo '<span style="margin-left: '+(indentValue*i)+';">'+output[i]+'</span>';
}
This section expects the maximum length of the start to be 80 characters long, decrementing by 5 each time. Also if you want to be sure it doesn't break early, ensure the the css.overflow is set to hidden/visible.
Having an blank image (jagged like steps) you want inline on the left of the paragraph should do it.
How can one tell, using Javascript (jQuery will work), if a particular X-Y coordinate on an HTML page is vertically between two lines of text? The lines may be in the middle of a long paragraph, inside a lengthy line-item tag, in a span, between two tags, etc. I have no way of controlling the HTML or the X-Y point, but I need to know if the X-Y point is in the middle of a line of text, or if it is in-between two lines of text; and it needs to be pretty efficient.
Please ask any questions you may have if I have not been clear enough.
Many thanks.
You can call .getBoundingClientRect() on a text range. You will need to write separate code for IE vs Non-IE browsers to get the text ranges.
This should be relatively easy in IE, thanks to textRange.moveToPoint(x, y). For other browsers you'll have to do something like do a binary search on the elements in the DOM, calling .getBoundingClientRect() on the elements, until you find the element that contains your text. Then create a range that contains the text of that element and do a binary search on the range to find whether your point overlaps any text.
All of this will be greatly complicated if you have absolutely positioned elements with text overlapping other elements.
Having dealt with text ranges, I don't think you can technically put anything "between" two lines of text on the same HTML node. Even if you use line height, every pixel belongs to one of the lines (even though it's visually space between them).
I'll throw out a few options which might help.
The simplest answer is probably just to use the line height:
get dom element that was clicked (event.relatedTarget in jQuery?)
determine its offset relative to the page (i.e. where the top of that element is)
determine the point that was clicked (x,y coords of the mouse event)
compare the two using the line-height of text in the row
This would look something like this:
function getLines(topOfElement, clickPoint, lineHeight) {
return Math.floor( (clickPoint - topOfElement)/lineHeight );
}
var topOfElement = $(element).offset().top; //must be position: relative|absolute
var clickedPoint = event.clientY; //might be pageY?
var lineHeight = parseFloat($(element).css('line-height')); //probably need to set this in px using css or it might be null
var textHeight = parseInt($(element).css('font-size')); //probably need to set this in px using css or it might be null
var prevLineNumber = getLines(topOfElement, clickedPoint, lineHeight);
// the previous line ends (in theory) at the bottom of the text (textHeight)
// you might need to adjust this definition to your needs
var prevLineBottom = prevLineNumber*lineHeight+topOfElement+textHeight;
// the next line begins (in theory) at the top of its line
// you might need to adjust this definition to your needs
var nextLineTop = (prevLineNumber+1)+lineHeight;
if( clickedPoint >= nextLineTop ) {
alert('clicked on row '+(prevLineNumber+1));
}
else if( clickedPoint <= prevLineBottom ) {
alert('clicked on row '+prevLineNumber);
}
else {
alert('clicked between rows '+prevLineNumber+' and '+(prevLineNumber+1));
}
If you want to see if the click happened between two html nodes, you can do that with Rangy, as well as some fancy selection and range calculations.
You could use it for things like determining the exact length of the text before and after the seletion. This is only useful if you want to see where in the text they clicked.
function getTextAtClick() {
var result = {nodeClicked: null, textBefore: '', textAfter: '', valid: false};
//get a selection object (even though the selection is technically zero length)
var sel = rangy.getSelection();
//you would probably want to discard any selection not zero length (i.e actual selection of text instead of a click)
// if not, you'd need to decide what it means to select across multiple dom nodes :(
if( sel.toString().length > 0 ) { return result; }
// get the point where the click occurred
var range = sel.getRangeAt(0);
result.valid = true;
// determine text in our dom element up to the click point
var before = rangy.createRange();
before.setStart(range.startContainer, 0);
before.setEnd(range.startContainer, range.startOffset);
result.textBefore = before.toString();
// determine text in our dom element after the click point
var after = rangy.createRange();
after.setStart(range.startContainer, range.startOffset+1);
after.setEndAfter(range.startContainer);
result.textAfter = after.toString();
return result;
}
I have a form where I use the following system: the fields and their labels are float: left and an extended comment explaining how to fill in the field appears far to the right, positioned with a wide left margin.
Following a suggestion in an Eric Meyer book, I use an hr to align the two: I put an hr styled with:
.lineup { clear:both; visibility: hidden}
Then I use Javascript to make the comment display when I want it.
This works great, except (for some weird problem in Safari and) when the comment is really long, when it "pushes down" the other form content as it appears.
So, I said, I can write a Javascript function to run on page build, to delete the hr's (remembering their offsetTop's) and move all the descriptions to somewhere near where the hr's were.
But I can't get it to remove the hr's.
Finally the code:
var hrListY = new Array(); // Y-coordinates of HR "lineup" elements
// Moves all descriptions so their middle is where the top used to be, and
// removes the <hr>s previously used to position them.
function relayoutDescriptions() {
var hrs = document.getElementsByTagName("hr");
alert('hrs.length = ' + hrs.length);
var i;
for (i = 0; i < hrs.length; i++) {
var hr = hrs[i];
if (hr.className == 'lineup') {
hrListY.push(hr.offsetTop);
alert('Got an HR element: Y = ' + hr.offsetTop + ' parentNode class = "' + hr.parentNode.className + '"');
hr.parentNode.removeChild(hr);
}
}
// Now we have a list of Y-coordinates of HR elements, hrListY. We use it
// to adjust the offsetTop's of the -desc divs. For each one, we adjust the
// offsetTop so the center of the div is at the height where the HR was.
}
That's all I have of it so far. It gives me reasonable ascending numbers for offsetTop, and a plausible parent node class, but the resulting layout clearly shows, and firebug confirms, that the hr's are still there.
Help?
P.S.
If there's an easy way to do this with JQuery, I'm amenable to that, but I'd REALLY like to know what the $##&*% is going on here.
Thanks!
The node list returned by getElementsByTagName is live, which means that when you remove one of its elements from the DOM, the things to the right move left, so you're only removing every second item.
From http://www.w3.org/TR/DOM-Level-2-Core/core.html
NodeList and NamedNodeMap objects in the DOM are live; that is, changes to the underlying document structure are reflected in all relevant NodeList and NamedNodeMap objects.
You can see that this is so by moving the alert('hrs.length = ' + hrs.length); inside your loop. It will alert a different number each time through.
To fix this, you can copy the list
var myNodeList = document.getElementsByTagName('HR');
myNodeList = Array.prototype.slice.call(myNodeList);
or you can iterate right to left
var myNodeList = document.getElementsByTagName('HR');
for (var i = myNodeList.length; --i >= 0;) {
...
}
so that when you remove an item, there is nothing to the right that shifts left messing up your indexing.
I have a long text and I'd like to offer the user a reading help: The current line should be highlighted. To make it easier, I'll just use the Y coordinate of the mouse (this way, the mouse pointer isn't going to get in the way). I have a big DIV with the id content which fills the whole width and a small DIV with the class content for the text (see here for an example).
I'm using jQuery 1.4. How can I highlight the line of text that is closest to the current mouse position?
Not sure if jQuery will help you out much here, but you could take a look at the element.getClientRects method, documented on MSDN and MDC. More specifically, this example at MSDN is sort of similar to what you want to achieve, highlighting lines using a cleverly z-indexed div element that goes behind the text at the co-ordinates returned by getClientRects().
You should be able to achieve the same thing by looping through the TextRectangle objects returned in the document's onmousemove and checking to see if the y value of the mouse cursor is > the top and < the bottom of each rectangle and moving the cleverly z-indexed div to the same position/height.
All the current major browsers support getClientRects().
http://jsbin.com/avuku/15
UPDATED - working in Chrome, IE6/7/8, Firefox, Opera, Safari. The initial problems I had in the other browsers were related to the DIV needing to be display: inline.
UPDATED AGAIN - I had to refer to this answer for some newer questions, so I took the time to update it to recalc the lines on window resize. It looks like others have been playing around too, it's now on revision 15.
I don't see how you could feasibly do this without explicitly-wrapped text (i.e., newlines or <br> elements).
To the best of my knowledge, there's no way for the DOM to discover where a specific piece of text has wrapped, character-wise nor pixel-wise - including what I know of the Range API - not to mention the dynamic nature text can assume, such as with the text-zooming feature of browsers.
But if you could somehow manage to generate/inject explicit line-endings, then I think I have a solution for you.
EDIT
Thanks to the awesome information in Pekka's answer, I've cobbled together a functional prototype, but it has a significant caveat - works with plain-text content only. Any HTML present the body of the element will be stripped.
jQuery.fn.wrapLines = function( openTag, closeTag )
{
var dummy = this.clone().css({
top: -9999,
left: -9999,
position: 'absolute',
width: this.width()
}).appendTo(this.parent())
, text = dummy.text().match(/\S+\s+/g);
var words = text.length
, lastTopOffset = 0
, lines = []
, lineText = ''
;
for ( var i = 0; i < words; ++i )
{
dummy.html(
text.slice(0,i).join('') +
text[i].replace(/(\S)/, '$1<span/>') +
text.slice(i+1).join('')
);
var topOffset = jQuery( 'span', dummy ).offset().top;
if ( topOffset !== lastTopOffset && i != 0 )
{
lines.push( lineText );
lineText = text[i];
} else {
lineText += text[i];
}
lastTopOffset = topOffset;
}
lines.push( lineText );
this.html( openTag + lines.join( closeTag + openTag ) + closeTag );
};
$(function()
{
$('p').wrapLines( '<span class="line">', '</span>' );
});
span.line {
display: inline;
}
span.line:hover {
background-color: lightblue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p style="max-width:400px">
one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three
</p>
The best approach that comes to mind is splitting each line into a <span> or <div> element that has a :hover CSS class with the "highlight" setting set:
span.line:hover { background-color: lightblue; }
That would be the least expensive solution, as the browser is going to take care of all the highlighting itself. If you want fancy effects, you can still achieve that by adding mouseover and mouseout events to every line.
The tough part, of course, is splitting the content into lines at the browser's line break. You need to do that dynamically so the lines actually reflect the positions at which the browser breaks the text.
Maybe the accepted answer to this question is a step into the right direction:
Getting a specific line using jQuery
How it works:
It goes through the entire element (actually, a clone of the element) inserting a element within each word. The span's top-offset is cached - when this offset changes we can assume we're on a new line.