This is a followup question to Removing <span> tag while leaving content intact, with just javascript
If I use spans to highlight text in a page, it breaks up the content into new nodes. And then, when I remove the highlight spans using replaceChild, the nodes remain separated. I would like to have the original text merged back into a single text node, instead of three text nodes - the text before the highlighting started, the text that was previously highlighted, and the text after the highlighting ended. Is this possible to do?
You could try something like
containerElement.innerHTML = containerElement.textContent;
Not sure that will work on IE prior to 9 though because of textContent.
Similar to Jim's suggestion but accommodates IE:
containerElement.innerHTML = containerElement.textContent || containerElement.innerText;
Or a much longer version:
var text = containerElement.textContent || containerElement.innerText;
while (containerElement.firstChild) {
containerElement.removeChild(containerElement.firstChild);
}
containerElement.appendChild(document.createTextNode(text));
I think the first is simpler.
Related
Context
I have a website where users can write articles. After creating the article, other users should be able to go in and highlight any text within the article and make a comment on it (similar to the system on Medium). I do this by saving the user's highlight to a database and then checking the highlight against the article when it loads. However, the author of the article can bold and italicize text, which ruins the system because the <strong> and <i> tags get in the way. For example, if a user's article consisted of the following content:
<p>This is my article. <strong>Bolded text here.</strong></p>
and another user wanted to come in and highlight my article. Bolded text, that text would be saved to the database. Then, I insert the highlight into the article (which is just applying a span to the highlighted text) by using this code to replace the article's HTML with the highlight:
let $text = $("#articleContents");
let textCurrent = $text.html().trim();
let textToHighlight = text.trim();
let ifTextExists = textCurrent.indexOf(textToHighlight) > -1;
if (ifTextExists) {
textCurrent = textCurrent.replace(textToHighlight, "<span class='highlights'>" + textToHighlight + "</span>");
$text.html(textCurrent);
}
So, because I'm checking the article's HTML to see if a highlight matches, when the user wants to highlight the string my article. Bolded text, the content it is checked against is article. <strong>Bolded text</strong>, so the text is not highlighted (the class is not applied) because the tags get in the way. With Medium's system, any string can be highlighted regardless of whether it is bolded or not.
Question
How can I alter my code to disregard the non-text nodes and highlight the whole string, whether it's wrapped in tags or not?
Things I Have Tried
I have tried using .text() instead of .html(); the problem with that is that the HTML tags are removed from the article, which I need to be able to keep the structure of the article (for example, removing the div tags means
moving the text to a new line by pressing enter doesn't work).
Use $text.text() instead of $text.html() so it ignores tags.
You don't need JQuery for that.
JavaScript solution: element.innerText
const elem = document.querySelector('.elementSelector')
elem.innerText;
JQuery solution: $(elem).text();
$('.elementSelector').text();
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
I am using execCommand with javascript to insert text into editable iframes like this:
element.execCommand("insertHTML",false,"some text");
Anyone know how to insert that text instead of the first character to the left of the cursor? So the same effect as pressing a backspace before doing the above?
It seems that there's no easy way to send keystrokes to editable iframe, so you'll probably need to find some sort of workaround. Easiest way to do that would be to get the contents from iframe, manipulate them and then put them back to iframe.
E.g.:
Select all text in iframe with
var selection = element.execCommand("selectAll");
to remove last character - slice selection
selection = selection.baseNode.data.slice(0, -1)
delete all content
element.execCommand("Delete")
append sliced selection + your new text
element.execCommand("insertHTML",false,selection);
element.execCommand("insertHTML",false,"some text");
References:
http://msdn.microsoft.com/en-us/library/ie/ms533049(v=vs.85).aspx
https://developer.mozilla.org/en/Rich-Text_Editing_in_Mozilla
P.S. I'm note very familiar with editable iframe or selection objects, so if you have any html of special characters in your text it might be much more complicated than this. Also you might need to tweak it for different browsers.
I am looking to create a javascript/jquery function to wrap a piece of highlighted text from a textarea in strong tags - similar to the WYSIWYG editor here.
Is this possible and if so can you point me in the right direction.
EDIT:
OK so here's a hopefully clearer description of what I want...
I have a textbox on my page which I can type in.
I then want to be able to highlight a part of this text and wrap the highlighted part in <strong> tags
So if the text box had the words one two three and I highlighted the word "two", I want to be able to wrap that word in the strong tags - so becoming one <strong>two</strong> three
Hope this is clearer... I know there are plugins out there but I don't need the full WYSIWYG functionality.
My Rangy inputs (terrible name, I know) jQuery plug-in does this.
Example code:
$("#foo").surroundSelectedText("<strong>", "</strong>");
jsFiddle: http://jsfiddle.net/aGJDa/
I love Rangy! Use it often! But I didn't want to include the whole thing just for this little application, so I did it using document.execCommand to wrap the selected text, then used the href (third parameter of the CreateLink execCommand) to find the element, wrap it with what I wanted, and then remove the link:
document.execCommand('CreateLink', false, 'uniqueid');
var sel = $('a[href="uniqueid"]');
sel.wrap('<strong />')
sel.contents().unwrap();
document.execCommand is supported by all major browsers so you should be safe hacking it this way. In the browsers I've tested, the browser itself will close and open tags for you, so if you're selecting from the middle of one html tag to the middle of another, it should nest the tags correctly.
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.