29

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?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Gazzer
  • 4,524
  • 10
  • 39
  • 49

6 Answers6

26

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.

guy mograbi
  • 27,391
  • 16
  • 83
  • 122
  • 1
    The assumption that text is in the leaf nodes seems problematic - e.g. if you have text or text then those parts will be in leaf nodes but not the rest of the surrounding non-bolded or non-emphasized text. – Dave Hilditch May 25 '16 at 13:50
  • but I show how you can get around it easily. There is another way.. You could clone the entire HTML, and simply remove the hidden parts and then do 'getText' on all. – guy mograbi May 27 '16 at 04:06
  • @DaveHilditch added an example that also resolves your situation. – guy mograbi May 27 '16 at 04:37
  • I've had some strange behavior with the final JS function you post, are you sure that preservers all the JS events on all the DOM elements correctly? – David Rogers Jul 26 '16 at 20:12
  • @Dave13s not sure I follow the question. I published a plunker. if you encountered a problem - can you please reproduce it on plunker? it will be easier to address. – guy mograbi Jul 27 '16 at 08:59
  • @Dave13s in case you need to preserve bindings there are several ways to resolve that. I personally like using `on`. do you know that? – guy mograbi Jul 27 '16 at 16:24
  • Your answer is great. but text next to the hidden element is not included exg. `text` , why is that? – Abdullah Salma Nov 24 '17 at 13:27
  • @AbdullahSalma please give link to a plunker and I will try to find the problem. – guy mograbi Nov 25 '17 at 05:43
17

Use the :visible selector of jQuery

In your case I think you want to do:

$('#output').children(":visible").text() 
TylerH
  • 20,799
  • 66
  • 75
  • 101
marcgg
  • 65,020
  • 52
  • 178
  • 231
  • btw, according to OP question, only one of #output's child is hidden, is that doable with that way? When I test `$('#output:visible').text()` its still showing "Watashi I", but OP wants only "Watashi", isn't it? – YOU Dec 04 '09 at 10:56
  • @s.mark: You must be right. I edited my answer, I think it should work. If not try *:visible or something like that. You could also test .css("display")!="none" – marcgg Dec 04 '09 at 11:21
  • I think this is the wrong approach because it consists of re-implementing already existing browser functionality (copy/paste). – Kaze no Koe Dec 04 '09 at 11:29
  • 1
    @smark: good! It's clearly not the best solution. The best one would be to only load into the page the language wanted, but this will fix the OP's problem right away – marcgg Dec 04 '09 at 13:25
  • hey Marcgg, this seems to work almost perfectly. It does indeed copy the visible text but if I change the visible text via the 'rotate' button then a copy action still only gets the original visible text - not the updated one (this is easier to see if you go to the actual site and click the rotate icon to visualise how the text changes). So I guess it becomes a bit more complicated if the visible text changes. (btw, I wanted to 'up' your suggestion but I'm new here and it won't allow me!) – Gazzer Dec 04 '09 at 19:10
  • I got this to work. The above issue was connected to something else. – Gazzer Dec 27 '09 at 05:08
11

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'));
aehlke
  • 15,225
  • 5
  • 36
  • 45
VanDir
  • 1,980
  • 3
  • 23
  • 41
  • 1
    The `$('#output *:not(:has(*)):visible').text()` jQuery answer missed some text for some reason. This one picked up all and only what was visible (except for generated content in a ::before pseudo-element, which I didn't worry too much about). – jinglesthula Feb 25 '19 at 20:04
  • Validate your input! If you query for the input element, it could be null Great answer. – Jack Steam Jan 17 '21 at 21:38
  • This answer worked best for me. – Chris Hayes Aug 22 '23 at 19:23
2

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()
1
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.

Community
  • 1
  • 1
David Rogers
  • 2,601
  • 4
  • 39
  • 84
  • I found this code a useful start. But wouldn't you want to replace moreResultText += lookup($(value), text); with moreResultText += lookup($(value), ''); If not you'll repeat the original text value. – Robert Nov 18 '16 at 19:20
  • I'm glad you find it useful :), I feel that this answer gives a better/more complete albeit not perfect way to accomplish the requested functionality vs the other higher voted answers. As for the code, I have not observed that behavior, it's DFS so it should add each lower level's text to the string, eventually passing that up to the parent caller until you have a large string that contains all text. Do you have an example where it's not working, maybe a [JSFiddle](https://jsfiddle.net/)? – David Rogers Nov 18 '16 at 22:32
0

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.

Kaze no Koe
  • 3,254
  • 1
  • 21
  • 22
  • hey 'voice of the wind' :-) I'd prefer to find a solution that doesn't involve removing elements at the moment as that would require a major rewrite of the coed. – Gazzer Dec 04 '09 at 19:11
  • I guess you'll have to go with some hide & seek magicks then :) I'd remove them dynamically with JS in the function bound to the interface element that toggles the button, but if that's too much work I don't have anything else to propose :( – Kaze no Koe Dec 07 '09 at 14:35