15

I have a jQuery function which toggles the visibility of the contents of a fieldset when its legend is clicked, leaving just the fieldset border (if there is one) and the legend showing:

$('legend.togvis').click(function() {
    $(this).siblings().toggle();
    return false;
});

It works great unless the fieldset contains text nodes.

<fieldset><legend class='togvis'>Click Me</legend>
  <p>I toggle when the legend is clicked.</p>
  But I'm a recalcitrant text node and refuse to toggle.
</fieldset>

In an effort to toggle the text nodes too I tried this:

$('legend.togvis').click(function() {
    $(this).parent().contents().not('legend').toggle();
    return false;
});

which works the same as the first function. And this:

$('legend.togvis').click(function() {
    $(this).parent().contents(":not(legend)").toggle();
    return false;
});

which throws the error

Message: 'style.display' is null or not an object
Line: 5557
Char: 3
Code: 0
URI: http://code.jquery.com/jquery-1.4.4.js

Any thoughts on how to get the entire contents of a fieldset (minus the legend) to toggle when the legend is clicked?

ETA Solution, with many thanks to Eibx

$('legend.togvis').click(function() { 
    $(this).parent().contents().filter(
        function() {  return this.nodeType == 3; }).wrap('<span></span>');//wrap any stray text nodes
    $(this).siblings().toggle(); 
}); 
dnagirl
  • 20,196
  • 13
  • 80
  • 123
  • it appears that the issue is that text nodes don't/can't have a style. Since `.toggle()` works by affecting the css of the node, it can't affect text nodes. Perhaps I need to store the contents of the fieldset in `.data()` and then remove children? – dnagirl Jan 11 '11 at 14:38

7 Answers7

16

You simply cannot make text disappear in HTML, JavaScript or CSS. It needs to be contained in a HTML element that will have display:none; applied or something similar.

The following code will wrap everything into a div, and move your legend to the same level as the div, and make the div show/hide when you click on the legend.

$("fieldset legend").click(function() {
  if ($(this).parent().children().length == 2)
    $(this).parent().find("div").toggle();
  else
  {
    $(this).parent().wrapInner("<div>");
    $(this).appendTo($(this).parent().parent());
    $(this).parent().find("div").toggle();
  }
});
はると
  • 1,489
  • 8
  • 20
  • this is a really nice solution because the markup gets added by the function that needs it. – dnagirl Jan 11 '11 at 14:59
  • Thanks - but I would recommend, that you move some of the code to $(document).ready() since this will check for element on each click. Would be smart to make sure that all legends have the right children. Something in the lines of: http://jsbin.com/eleva3/2/edit – はると Jan 11 '11 at 15:05
  • Unfortunately, it's causing some layout problems. You're sending me in a really good direction though. – dnagirl Jan 11 '11 at 15:08
  • Sadly this is the closest I can come, with out messing with your design: http://jsbin.com/eleva3/3/edit - But there a bunch of down falls, I don't recommend you using it. – はると Jan 11 '11 at 15:20
  • I figured it out (solution added to question now). Giving you credit for the solution, since I wouldn't have got it without you. – dnagirl Jan 11 '11 at 15:27
  • Just as a heads up to all who use this function. Under IE8 (and possibly earlier) rendering a DIV element above the LEGEND element will cause it to actually appear above the legend element. You will need to adjust the code for your uses such that the div element appears below the legend element (can't call wrap inner on all the content) – Sean Anderson Oct 26 '12 at 01:04
  • This is pretty old but I thought it was worth mentioning that I found it helpful to use children instead of find in addition to using prependTo instead of appendTo. – drs9222 Sep 24 '14 at 15:06
11

Since text nodes are not elements, there is no way to toggle them.

As a last resort, if you cannot wrap them with an element, you can remove everything except the legend and add the elements and the text nodes back later:

$(document).ready(function() {
    $('legend.togvis').click(function() {
        var $this = $(this);
        var parent = $this.parent();
        var contents = parent.contents().not(this);
        if (contents.length > 0) {
            $this.data("contents", contents.remove());
        } else {
            $this.data("contents").appendTo(parent);
        }
        return false;
    });
});

You can test that solution here.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • This should worked for me. I wanted to be able to toggle a fieldset with report filtering controls so I had a couple of drop downs and a couple of text boxes and their accompanying labels. – Nick Harrison Dec 22 '11 at 15:12
10

Glorious Pure HTML/CSS Solution

This can be done purely with HTML and CSS, now in the year 2019.

#toggle + #content {
    display: none;
}

#toggle:checked + #content {
    display:block;
}

legend {
    width: 85%;
    border: 1px solid black;
    padding: 3px 6px;
    cursor: pointer;
}

legend label {
    width: 100%;
    display: inline-block;
    cursor: inherit;
}

legend label::after {
    content: "▼";
    float: right;
}
<body>
<form>
<fieldset>
    <legend><label for="toggle">Test</label></legend>
    <!-- This input has no name, so it's not submitted with the form -->
    <input type="checkbox" id="toggle" checked="" hidden=""/>
    <div id="content">
        <label>Some text field<input type="text" name="some-text-field"/></label><br/>
        <label>Some color input<input type="color" name="some-color-field"/></label>
    </div>
</fieldset>
</form>
</body>

Javascript Version

While the Pure HTML/CSS Solution is clearly glorious, there's nothing wrong with using Javascript to program interactivity on a page. But I've noticed all other answers use JQuery, which is totally unnecessary in the year 2019.

window.addEventListener('load', () => {
    const collapser = document.getElementById('collapser');
    const collapsable = document.getElementById('collapsable');

    function collapse(event) {
        if (event) {
            event.stopPropagation();
        }

        collapsable.hidden = !collapsable.hidden;
    }

    collapser.addEventListener('click', collapse);
});
legend {
    width: 85%;
    border: 1px solid black;
    padding: 3px 6px;
    cursor: pointer;
    display: inline-block;
}

legend::after {
    content: "▼";
    float: right;
}
<body>
<form>
<fieldset>
    <legend id="collapser">Test</legend>
    <div id="collapsable">
        <label>Some text field<input type="text" name="some-text-field"/></label><br/>
        <label>Some color input<input type="color" name="some-color-field"/></label>
    </div>
</fieldset>
</form>
</body>
ocket8888
  • 1,060
  • 12
  • 31
  • I didn't feel like animating a transition on the arrow, but that's also totally possible both with and without Javascript (though in this case it's harder without it). You could also use `
    `/`` to similar effect if your supported browsers are all HTML5-compliant.
    – ocket8888 Oct 31 '19 at 17:16
  • Safari does not support the "hidden" attribute. – Donald Winston Oct 26 '22 at 10:07
  • [Safari absolutely supports the "hidden" global attribute](https://caniuse.com/?search=hidden). It doesn't support the `until-found` keyword value of that attribute, but I'm not using that here. – ocket8888 Nov 02 '22 at 21:19
6

Use a div tag to separate the contents, something like:

<fieldset>
  <legend class='togvis'>Click Me</legend>
  <div class="contents">
  <p>I toggle when the legend is clicked.</p>
  But I'm a recalcitrant text node and refuse to toggle.
  </div>
</fieldset>

Then you only need to toggle the div.

$('legend.togvis').click(function() {
    $(this).closest("contents").toggle();
    return false;
});

Update only in case you cannot edit the HTML you can do the following but it will only work if all texts are within at least one tag:

$('legend.togvis').click(function() {
    $(this).parents("fieldset").find("*:not('legend')").toggle();
    return false;
});

Online demo here: http://jsfiddle.net/yv6zB/1/

amosrivera
  • 26,114
  • 9
  • 67
  • 76
  • he might not be able to do that – hunter Jan 11 '11 at 14:23
  • your updated code does not affect text nodes. It just does what my original function does. I had considered an internal div as a toggle hook, but adding non-semantic mark-up just to get javascript functionality offends my sensibilities ;). – dnagirl Jan 11 '11 at 14:35
  • jejeje, understood, but then how is semantically correct that a text node is floating around? should not this "But I'm a recalcitrant text node and refuse to toggle." go inside paragraph tags? – amosrivera Jan 11 '11 at 14:37
1

May be you simply enclose your text into the sibilig tag:

<fieldset><legend class='togvis'>Click Me</legend>
  <div><p>I toggle when the legend is clicked.</p>
  But I'm a recalcitrant text node and refuse to toggle.</div>
</fieldset>
Ivan Buttinoni
  • 4,110
  • 1
  • 24
  • 44
0

I liked the accepted answer, but did not find it robust enough for my purposes. Just checking the length of the number of child elements does not support many scenarios. As such, I would consider using an implementation similar to:

$("fieldset legend").click(function () {
    var fieldset = $(this).parent();
    var isWrappedInDiv = $(fieldset.children()[0]).is('div');

    if (isWrappedInDiv) {
        fieldset.find("div").slideToggle();
    } else {
        fieldset.wrapInner("<div>");
        $(this).appendTo($(this).parent().parent());
        fieldset.find("div").slideToggle();
    }
});
Sean Anderson
  • 27,963
  • 30
  • 126
  • 237
0

Modification of ocket8888's js answer to work with classes

window.addEventListener('load', () => {
    function collapse(event) {
        if (event) {
            event.stopPropagation();
        }

        var targetdiv = event.target.parentElement.getElementsByClassName("collapsable")[0];
        targetdiv.hidden = !targetdiv.hidden;
    }

    var collapser = document.getElementsByClassName('collapser');
    Array.from(collapser).forEach((collapse_tag) => {
        collapse_tag.addEventListener('click', collapse);
    });
});
.collapsable {
     border: 1px solid #F00;
}
legend {
    width: 85%;
    border: 1px solid #0F0;
    padding: 3px 6px;
    cursor: pointer;
    display: inline-block;
}

legend::after {
    content: "▼";
    float: right;
}
<body>
<form>
Above
<fieldset>
    <legend class="collapser">Test</legend>
    <div class="collapsable">
        <label>Some text field<input type="text" name="some-text-field"/></label><br/>
        <label>Some color input<input type="color" name="some-color-field"/></label>
    </div>
</fieldset>
Below
</form>
</body>
mikeytown2
  • 1,744
  • 24
  • 37