Get text offset from click event via Javascript? - javascript

So I have searched all over trying to figure out how to do the following and have yet to find a solution:
I need to get the text offset for a given HTML element from a click event. This means that if I have the following HTML
<p>This is a really cool paragraph</p>
and a user clicks on the first 'a' in the sentence, the text offset would be 8 seeing as the first 'a' is at index 8 if we take the 'T' in 'This' as index 0.
I need this information so that I may programmatically create a text selection based on where a user clicks. As of right now I can track which HTML elements are clicked on and thus I can create this sort of activity at a HTML-element level granularity, but I'd like to have finer control than that.
Thank you!

Copied from your comment in another answer:
I'm currently trying to simulate human behavior via Javascript which
is turning out to be a bit more difficult than I anticipated (most
likely by design).
What you want is Selenium, for web browser automation: http://seleniumhq.org/

Using Prototype:
<p id='mytext'>This is a really cool paragraph</p>
<script type='text/javascript'>
var characters = $('mytext').textContent;
var newchars = '';
for(I = 0;I < chars.length;I++) {
newchars += '<span id="char_' + I + '">' + chars[I] + '</span>';
}
$('mytext').textContent = newchars;
Event.observe($('mytext'), 'click', function(e) {
var spanID = (e.findElement('span')).getAttribute('id')
var index = spanID.split('_')[1]; // Ta-daaa!
});
</script>
Please don't do this for large blocks of text (or, preferably, at all). It creates a DOM node for every character, and can slow the browser down...

I don't think there's anything built in for that. Any solution will be a hack and unreliable.
What you can do is use the selection API to get a user selection on a page, which sounds like you can do.

Just throwing this out there as a possible hack/solution.
You could try using the mouse offsetX and offsetY when a click occurs and simulate a double click dblclick in that location, effectively selecting the text and then using the selection API to get the word they clicked. Wouldn't work to the letter, but might work to the word.

Append each letter inside a span tag and onClick event just extract the letter inside the span tag and may be keep reference with some attributes in the span tag for tracking the index of the position for each of the element.But this approach would be redundant.

Try window.getSelection() ...I'm not sure if it works in all browsers, but at least Chrome seems to create a Selection object with the offset you need.

Related

Place tags around certain text within contenteditable without moving cursor

I am working on a simple (I thought) word processor. It uses contenteditable. I have a list of words that I want to always appear highlighted.
<article contenteditable="true" class="content">
<p>Once upon a time, there were a couple of paragraphs. Some things were <b>bold</b>, and other things were <i>italic.</i></p>
<p>Then down here there was the word highlight. It should have a different color background.</p>
</article>
So basically what I need is a way to wrap a word in <span> tags. This has proven more difficult than I expected.
Here was what I tried first:
var text = document.querySelector('article.content').innerHTML
start = text.indexOf("highlight"),
end = start + "highlight".length;
text = text.splice(end, 0, "</span>");
text = text.splice(start, 0, "<span>");
document.querySelector('article.content').innerHTML = text;
It uses the splice method found here.
And it does exactly what I need it to do, with one big issue: the cursor gets moved. Because all the text is replaced, the cursor loses its place, which isn't a good thing for a text editor.
I've also tried a couple times using document.createRange, but the issue is that while given the start and end points of a range only includes visible characters, text.indexOf("highlight") gives the index including the tags and such.
A few ideas which I'm not sure how to execute:
Figure out where the cursor begins and place it there again after using the code above
Find the difference in indexes between createRange and indexOf
Maybe there's already a library with this kind of functionality that I just can't find
Thank you for your help!
Firstly, I would recommend against doing this by manipulating innerHTML. It's inefficient and error-prone (think of the case where the content contains an element with a class of "highlight", for example). Here's an example of doing this using DOM methods to manipulate the text nodes directly:
https://stackoverflow.com/a/10618517/96100
Maintaining the caret position can be achieved a number of ways. You could use a character offset-based approach, which has some disadvantages due to not considering line breaks implied by <br> and block elements but is relatively simple. Alternatively, you could use the selection save and restore module of my Rangy library, which may be overkill for your needs, but the same approach could be used.
Here is an example using the first approach:
http://jsbin.com/suwogaha/1

replace text function not working in explorer

I have a js replace function to replace text next to two radio buttons on a pre set form.
Script is as follows.
document.body.innerHTML=document.body.innerHTML.replace("Payment by <b>Moneybookers</b>
e-wallet<br>","");
document.body.innerHTML=document.body.innerHTML.replace("Maestro, Visa and other credit/debit cards by <b>Moneybookers</b>","Pago con Diners Club, Mastercard o Visa");}onload=x;
The script works fine in Chrome and Firefox, however, the script is not actioned in Explorer.
I believe it has something to do with there being , / - within the text I am replacing? When I use the function to replace text with no , / - in the text - it works fine in explorer, however, for example when I try to replace text.. - "Maestro, Visa and other credit/debit cards by Moneybookers" this does not work in explorer.. I'm assuming because of the coma and forward slash. Honestly I've tried everything but just can not get this to work. Any help would be greatly appreciated!!
Not sure whether it's related (I'm a Mac user without IE) but you shouldn't use multiline strings. Use \n instead.
What is returned by innerHTML varies from one browser to an other, because there is no standard about it (the content will be the same, but the way it's displayed can be different). Doing replace like that is likely to fail on some browser. You should just take an other approach to do your replace.
A better approach would be to wrap the text you want to replace with a span, this way you can more easily target the content you want to replace.
<span id="thatFirstThing">Payment by <b>Moneybookers</b>e-wallet<br></span>
An after you can do
document.getElementById("thatFirstThing").innerHTML = "";
P.S.: Doing innerHTML replace on the body also has a huge side-effect. Since you are replacing the content of your hole page. All the event handler that where bind on your page will disappear.
Edit: If you can't modify the HTML page, it's a little bit more tricky, because the DOM is not well adapted to do such thing. What you could do is to target parent element by navigating through the DOM with document.getElementById and childNodes. And once you have your parent element just write the new content you want, without doing replace.
In the end it would look something like this :
document.getElementById("someSection").childNodes[0].childNodes[1].childNodes[0].innerHTML = "";

getting selected text & text indexes in a <pre> tag, where the offset is the start of the pre

So i have a pre tag like so:
<pre> Some content, more content. <span>Coloured content</span>. Some more content</pre>
What i want to do is setup an event using javascript or jquery that binds a mouseup event. When the user selects text, i want to get the indexes offset from the start of the pre, so it ignores the span tags per say. So if someone selects the text after the span tag, it knows to offset from the pre opening.
Is there a way I can do this? It looks like window.getSelection starts it off after the span tag.
Given this HTML
<pre>0<span>1</span>23<span>4<span>56<span><span>7</span></span>8</span></span></pre>
you want to get the first selected digit as output/offset, right?
The basic idea is to navigate to the left in the DOM tree until there is no more node with the same parent. Then climb up to finally reach the pre tag. Whilst navigating through the tree towards the upper left, all characters of the visited elements are counted and added to the final result.
$('pre').on('mouseup', function(){
var selection = window.getSelection();
// Get the offset within the container that holds all of the selection
var offset = selection.anchorOffset;
// A reference to the currently examined node
var currentNode = selection.anchorNode;
// Wander around until we hit the pre
while(currentNode!==this){
if(currentNode.previousSibling){
// If there is a node left of us (with the same parent) navigate to the left and sum up the text length in the left node.
// There is no need to check the children (possibly existent) since these would be included in text's return value
offset = offset + $(currentNode.previousSibling).text().length;
// Navigate to the left node of the current
currentNode = currentNode.previousSibling;
} else {
// There is no left node so climb up towards the pre tag
currentNode = currentNode.parentNode;
}
}
// Print the final result
console.log(offset);
});
The script should output the required number. So if you are selecting 78 you'd get 7 as output.
I did only test this code in Firefox. Other browsers should work as well if they implement HTML Editing API. IE does not support it until version 9. The same applies for getSelection (see MSDN).
This kind of thing gets very complicated, especially when you need to worry about cross-browser implementations (*cough* IE *cough*). I therefore strongly recommend Rangy, a "cross-browser JavaScript range and selection library." I've used it a bit myself and found it to work perfectly.
The library's author, Tim Down, has answered a lot of questions on SO about range and selection issues, and you'll see how complicated they get :)

HTML: Is there any way to determine if an element is behind another element?

We have an application with a search popup. You search for a term, and it finds them in the page and add a class to the word which in turn highlights the text. Occasionally the found instance is in an area of the page that is behind the search popup. If that is the case, I'd like tone the opacity down of the search popup so the user can see the instance behind it. Is there any simple way to do this? Any tips or tricks are appreciated. One requirement is that the search popup stay open after a search, so we can't just close/hide it.
You can use getBoundingClientRect() on the found text and the popup and compare the dimensions.
MSDN
MDN range, element
You can act on text ranges directly, but it sounds like you have an element wrapping the found text in order to apply the highlight, so I'd just use that element. I think you can just pass your found text and your popup to this function to test if they overlap:
function isOverlapping (a, b)
{
var rectA = a.getBoundingClientRect();
var rectB = b.getBoundingClientRect();
return rectA.top < rectB.bottom && rectA.bottom > rectB.top &&
rectA.left < rectB.right && rectA.right > rectB.left;
}
Edit: I must have way too much time on my hands, because here's a fun little demo: http://jsfiddle.net/gilly3/qUFLJ/4/
Yes, using JavaScript it would be easy. Since you know if the popup is open, i assume that you know it's width and position. Just create a bounding box and check if your element is within this box. getBoundingClientRect is useful for this. If you're using jQuery, .offset can also help you.
Let me know if you need an example.

How do I get just the visible text with jQuery (or Javascript)?

I have website that converts Japanese Kanji into Romaji (roman letters):
and the output shows and hides with CSS what the user needs to see depending on their input criteria. For example:
<div id="output"><span class="roman">watashi</span> <span class="english">I</span></div>
The interface allows the user to flip between and output of watashi or I depending on what they want to see. The CSS hides one or the other using jQuery and a toggle button. (the hiding mechanism involves simple adding a class to the body and letting CSS do its thing).
The problem is that when users copy/paste the text into Word it copies everything. So I decided to use a system to copy paste the text using JavaScript and jQuery, but the problem repeats itself:
$('#output').text() outputs watashi I even if I is invisible on the page itself rather than watashi. Is there any way to get just the visible text?
the other solutions did not give me what I needed.
Short Answer
my answer is :
$('#output *:not(:has(*)):visible').text()
plunkr
TL;DR
The problem with marcgg's solution
You should not ask the text of all element under some root element..
why? - it will repeat output and ignore hidden flag
lets look at a simple example
<div id="output" class="my-root">
<div class="some-div">
<span class="first" style="display:none"> hidden text </span>
<span class="second" > visible text </span>
</div>
<div>
now if I do $('#output').children(":visible").text()
I will get .some-div and .second..
when in fact .some-div is of no concern to me..
when I ask for text() on those elements, .some-div will return the hidden text as well..
so technically marcgg's solution is wrong IMHO...
The reason for my answer
Now, in order to properly answer the question, we have to make an assumption. One that, for me, seems reasonable enough.
The assumption is that text only appears in leaf elements..
So we won't see something like this:
<div id="output" class="my-root">
<div class="some-div">
<span class="first" style="display:none"> hidden text </span>
<span class="second" > visible text </span>
</div>
some text here..
<div>
Why does this assumption seem reasonable to me? two reasons:
Because it is hard to maintain a page that is constructed this way - and with time people with experience learn that and avoid it.
It is easy to convert your html to such a structure. just wrap parents' text with spans. So even if this assumption does not exist right now, it is easy to get there.
With that assumption, what you want to do is request all leaf elements (elements without children) , filter out the visible, and ask for their text..
$('#output *:not(:has(*)):visible').text()
This should generate the correct result.
Gotta have text outside leaf element?
the comments suggest sometimes you just got to have text outside leaf element
<div> This is some <strong style="display:none"> text </strong> </div>
As you can see, you have <strong> as a leaf and it is common to have text outside it like in this example.
You could go around it with the workaround I suggest above.. but what if you can't?
You can clone the dom and then remove all hidden elements.
The problem here is that in order for :visible selector or :hidden selectors to work, I must have the dom element on the document (which means actually visible to the user).
And so, this method comes with some side effects, so be careful.
Here is an example
for this html
<div id="output" class="my-root">
<span>
some text <strong style="display:none">here.. </strong>
</span>
</div>
This javascript works
$(function(){
var outputClone = $('#output').clone();
$('#output :hidden').remove();
console.log($('#output').text()); // only visible text
$('#output').replaceWith(outputClone);
console.log($('#output').text()); // show original state achieved.
})
see plunker here
as mentioned - side effects may appear like a momentary flicker, or some initialization script that should run.. some may be avoided with some original thinking (div with size 1px/1px to contain the clone alongside original content?) depending on your scenario.
Use the :visible selector of jQuery
In your case I think you want to do:
$('#output').children(":visible").text()
Try this in modern browsers (here 'element' is a non-JQuery DOM object):
function getVisibleText(element) {
window.getSelection().removeAllRanges();
let range = document.createRange();
range.selectNode(element);
window.getSelection().addRange(range);
let visibleText = window.getSelection().toString().trim();
window.getSelection().removeAllRanges();
return visibleText;
}
then:
getVisibleText(document.getElementById('output'));
Guy has the correct answer.
However, I was dealing with a "this" object, so to get his answer to work you need to use the following syntax...
$('*:not(:has(*)):visible', this).text()
var lookup = function(element, text) {
//DFS Recursive way of finding text on each level
//Visible only works on elements that take up space(i.e. not fixed position elements)
var results = element.children(':visible');
//Look at the text at each level with the children removed
var newText = '';
results.each(function(index, value) {
newText += $(value).clone()
.children()
.remove()
.end()
.text();
});
var moreResultText = '';
results.each(function(index, value) {
moreResultText += lookup($(value), text);
})
if (results.length > 0) {
return text + newText + moreResultText;
} else {
return text;
}
};
lookup($('#output'), ''));
Most of the other functions fall apart when run on large sections of a page, this should be a more accurate way to determine what is actually displayed to the user, without corrupting the page, and without returning text that is not visible to the user.
Be careful of course, this does not preserve any sense of formatting, and the spacing of the output may not be correct between elements. Also, it probably does not correctly order the returned text, in these aspects its usages will be limited. Another consideration is the real definition of visible is a little hard to nail down, but for this example I accept that ":visible" works for most common cases.
I use it to check if a page contains visible text(just run it on the body element), but it would probably work for this example too.
Instead of hiding a span, remove the span element and keep a reference to it. When the user clicks on the toggle button, remove the other one and insert the one you kept a reference to. The user won't be able to select something that isn't in the DOM anymore.

Categories