Is particular x-y coordinate in between lines of text? - javascript

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;
}

Related

how to fill a screen width/height in razor page with string

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();

Change background on hover of a draggable div

I have a small draggable division (black) and many nodes with different IDs beside it
I need a hovering effect when I drag the black element on the nodes. What I am doing in allowDrop function is:
var dragObj;
function drag(ev){
dragObj = ev;
}
function allowDrop(ev){
ev.preventDefault();
var Dragged = dragObj;
var Hovered = ev;
var leftIndent = Dragged.layerX;
var hoveredID = Hovered.target.id.toString().split('_');
var nodesOnLeft = Math.floor(leftIndent/12);
var startingNode = hoveredID[0]-nodesOnLeft;
for (i=1;i<=Math.floor(draggableElementLength/12);i++){
var toApplyCssID = startingNode.toString() + '_1';
var toApplyCss = document.getElementById(toApplyCssID);
$('#'+toApplyCssID).css('background-color','lightgreen');
}
}
basically I am using the layerX property to find out the distance between my mouse pointer and draggable division's border and adjusting that to calculate number of nodes where I have to apply new CSS and applying that by finding the ID of node.
This is working but its making the process very slow as it involves many calculations and its not the hover effect as its not going away when I am removing the draggable division.
Is there any other way to achieve this or do I need to make code changes in existing code.
thanks
Without the HTML and the rest of the script, I can only point you in the direction you should be taking in this kind of scenario:
Don't constantly repeat calculations (that do not change) in a loop, store them outside the function:
Use document.getElementById(toApplyCssID) for each node and store the
elements in an array
Get the distance from the mouse position to the required edge or
edges of the div (leftIndent) on initial drag/mousedown and store
that in a variable
Store Math.floor(draggableElementLength/12) in another variable (you
haven't shown where this originates, but it doesn't seem to change in
the function...)
Apply the CSS to the relavent elements (from the array of elements)
using the other stored values to select them
Remove the CSS on those that had it applied earlier
This may not be the ultimate solution, but these are some of the things you can look at to speed up what you (may) have currently.

Convert textarea row/column to screen x/y coordinates

I have a textarea element on my webpage that will contain some plain text (alphanumerics, newlines, tabs, spaces). I know how to get the row and column of my cursor. But, I need a way to convert those into screen coordinates so I could display a floating div at that exact location of the cursor. I see two ways
Method 1: Take the offset of the textarea from (0,0) on screen. For x-coord, iterate through each character on that line and multiply the number of characters with the width of each character (tabs vs actual characters would be counted differently) and add it to the original offset. For y-coord, take the number of rows times the height of each row and add it to the original offset. But, how do I compute the character width and row height?
Method 2: Find a javascript library. Does anybody know of an existing javascript library/framework such as jQuery that would do this? I have looked and can't seem to find anything.
After the onchange event is fired, check for mouseMove events and then get the coordinates:
var waiting;
function text_changed(event) { //attach this to onChange
waiting = true;
setTimeout('waiting = false',1000);
}
function mouse_moved(event) { //attach this to mouseMove
if (waiting) {
display_div(event.pageX,event.pageY);
waiting = false;
}
}
function display_div(x,y) {
$('#floating_div').css('left',x).css('top',y).css('display','block');
}
if you have to, replace the jQuery with regular DOM scripting:
function display_div(x,y) {
var elmt = document.getElementById('floating_div');
elmnt.setAttribute('style','left:'+x+';top:'+y+';display:block');
}

What's wrong with my attempt to remove an element with JavaScript?

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.

How can I highlight the line of text that is closest to the mouse?

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.

Categories