Nesting optgroups in a dropdownlist/select
Asked Answered
I

13

93

I have created a customer c# DropDownList control that can render out it's contents are optgroups (Not from scratch, I edited some code found on the internet, although I do understand exactly what it's doing), and it works fine.

However, I have now come across a situation where I need to have two levels of indentation in my dropdown, i.e.

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <optgroup label="Level Two">
      <option> A.B.1 </option>
    </optgroup>
    <option> A.2 </option>
  </optgroup>
</select>

However, in the example snippet above, it is rendering as if Level Two was at the same amount of indentation as Level One.

Is there a way to produce the nested optgroup behavior I am looking for?

Infinite answered 24/6, 2009 at 11:16 Comment(1)
Added this issue as a proposal to the HTMLWG: github.com/whatwg/html/issues/5789Packhorse
I
56

Ok, if anyone ever reads this: the best option is to add four &nbsp;s at each extra level of indentation, it would seem!

so:

<select>
 <optgroup label="Level One">
  <option> A.1 </option>
  <optgroup label="&nbsp;&nbsp;&nbsp;&nbsp;Level Two">
   <option>&nbsp;&nbsp;&nbsp;&nbsp; A.B.1 </option>
  </optgroup>
  <option> A.2 </option>
 </optgroup>
</select>
Infinite answered 24/6, 2009 at 11:29 Comment(8)
Another reason this is not ideal is a selected option with a lot of indent looks unusual when the select is not active because of all the paddingConcertina
Inspect the DOM... the optgroups are not actually nested. At least in Firefox. It closes the first optgroup when it sees the 2nd.Junno
I ran across the same issue in chrmoe as @Mark has mentioned. The optgroups does not actually get nested. #25726835Brumby
Please, use paddings instead &nbsp;. Thats really an ugly solution when you have other answers with padding style.Tummy
@Tummy Unless someone fixed padding in WebKit browsers (they may have, I haven't looked) it's unfortunately not a decent solution. text-indent may be possible, from a comment on another answer, but I've not looked at this in years and so will leave this answer marked correct until someone provides a better cross-browser solution.Infinite
A better option is &ensp; (&#8194;) These don't render when the dropbox is in the closed state, but do when it's open. And they work they keyboard navigation, at least in the current chrome version. Tip from @evilkos on #1147289Singlebreasted
And no, padding/margin still don't apply to <option> elements. As far as I'm aware, this is according to the specification, and is not a bug.Singlebreasted
Geoffrey's comment up above is way outdated. Obviously a browser can be coded to ignore space when typing in a select dropdown -- that is exactly what Chrome is doing right now as I test this.Pic
C
77

The HTML spec here is really broken. It should allow nested optgroups and recommend user agents render them as nested menus. Instead, only one optgroup level is allowed. However, they do have to say the following on the subject:

Note. Implementors are advised that future versions of HTML may extend the grouping mechanism to allow for nested groups (i.e., OPTGROUP elements may nest). This will allow authors to represent a richer hierarchy of choices.

And user agents could start using submenus to render optgoups instead of displaying titles before the first option element in an optgroup as they do now.

Clasp answered 5/8, 2010 at 11:59 Comment(3)
That's true, you cannot create a multiple level nested tree with option groups, neither with html 5, also the note is false.Nabors
Anyone know if they plan to visit this in HTML5? Seems like a massive oversight.Hexosan
According to the HTML5 spec (dev.w3.org/html5/markup/optgroup.html#optgroup) the only permitted parent of optgroup is select, which suggest that no, this is not supported in HTML5.China
I
56

Ok, if anyone ever reads this: the best option is to add four &nbsp;s at each extra level of indentation, it would seem!

so:

<select>
 <optgroup label="Level One">
  <option> A.1 </option>
  <optgroup label="&nbsp;&nbsp;&nbsp;&nbsp;Level Two">
   <option>&nbsp;&nbsp;&nbsp;&nbsp; A.B.1 </option>
  </optgroup>
  <option> A.2 </option>
 </optgroup>
</select>
Infinite answered 24/6, 2009 at 11:29 Comment(8)
Another reason this is not ideal is a selected option with a lot of indent looks unusual when the select is not active because of all the paddingConcertina
Inspect the DOM... the optgroups are not actually nested. At least in Firefox. It closes the first optgroup when it sees the 2nd.Junno
I ran across the same issue in chrmoe as @Mark has mentioned. The optgroups does not actually get nested. #25726835Brumby
Please, use paddings instead &nbsp;. Thats really an ugly solution when you have other answers with padding style.Tummy
@Tummy Unless someone fixed padding in WebKit browsers (they may have, I haven't looked) it's unfortunately not a decent solution. text-indent may be possible, from a comment on another answer, but I've not looked at this in years and so will leave this answer marked correct until someone provides a better cross-browser solution.Infinite
A better option is &ensp; (&#8194;) These don't render when the dropbox is in the closed state, but do when it's open. And they work they keyboard navigation, at least in the current chrome version. Tip from @evilkos on #1147289Singlebreasted
And no, padding/margin still don't apply to <option> elements. As far as I'm aware, this is according to the specification, and is not a bug.Singlebreasted
Geoffrey's comment up above is way outdated. Obviously a browser can be coded to ignore space when typing in a select dropdown -- that is exactly what Chrome is doing right now as I test this.Pic
K
56

This is just fine but if you add option which is not in optgroup it gets buggy.

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <optgroup label="&nbsp;&nbsp;&nbsp;&nbsp;Level Two">
      <option>&nbsp;&nbsp;&nbsp;&nbsp; A.B.1 </option>
    </optgroup>
    <option> A.2 </option>
  </optgroup>
  <option> A </option>
</select>

Would be much better if you used css and close optgroup right away :

<select>
  <optgroup label="Level One"></optgroup>
  <option style="padding-left:15px"> A.1 </option>
  <optgroup label="Level Two" style="padding-left:15px"></optgroup>
  <option style="padding-left:30px"> A.B.1 </option>
  <option style="padding-left:15px"> A.2 </option>
  <option> A </option>
</select>
Kp answered 4/10, 2010 at 11:52 Comment(4)
Perfect, much easier since you can do style="padding-left:'. (15 * $level). 'px" in a loop over a tree of items.Unit
unfortunately this does not work in safari, since padding in <option> is not renderedNez
CSS to the rescue: you can remove the padding-left and use text-indent instead, adding the same amount to your select box width (source: #2967355)Nellienellir
That's not buggy, it's defined that way. Option A is not part of a group.Riti
A
18

<style>
  .NestedSelect{display: inline-block; height: 100px; border: 1px Black solid; overflow-y: scroll;}
  .NestedSelect label{display: block; cursor: pointer;}
  .NestedSelect label:hover{background-color: #0092ff; color: White;}
  .NestedSelect input[type="radio"]{display: none;}
  .NestedSelect input[type="radio"] + span{display: block; padding-left: 0px; padding-right: 5px;}
  .NestedSelect input[type="radio"]:checked + span{background-color: Black; color: White;}
  .NestedSelect div{margin-left: 15px; border-left: 1px Black solid;}
  .NestedSelect label > span:before{content: '- ';}
</style>

<div class="NestedSelect">
  <label><input type="radio" name="MySelectInputName"><span>Fruit</span></label>
  <div>
    <label><input type="radio" name="MySelectInputName"><span>Apple</span></label>
    <label><input type="radio" name="MySelectInputName"><span>Banana</span></label>
    <label><input type="radio" name="MySelectInputName"><span>Orange</span></label>
  </div>

  <label><input type="radio" name="MySelectInputName"><span>Drink</span></label>
  <div>
    <label><input type="radio" name="MySelectInputName"><span>Water</span></label>

    <label><input type="radio" name="MySelectInputName"><span>Soft</span></label>
    <div>
      <label><input type="radio" name="MySelectInputName"><span>Cola</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Soda</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Lemonade</span></label>
    </div>

    <label><input type="radio" name="MySelectInputName"><span>Hard</span></label>
    <div>
      <label><input type="radio" name="MySelectInputName"><span>Bear</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Whisky</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Vodka</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Gin</span></label>
    </div>
  </div>
</div>
Alacrity answered 24/5, 2015 at 2:35 Comment(1)
This will automatically work for UNLIMITED nesting level without adding/modifying any CSS/Style attribute.Alacrity
M
6

I really like the Broken Arrow's solution above in this post. I have just improved/changed it a bit so that what was called labels can be toggled and are not considered options. I have used a small piece of jQuery, but this could be done without jQuery.

I have replaced intermediate labels (no leaf labels) with links, which call a function on click. This function is in charge of toggling the next div of the clicked link, so that it expands/collapses the options. This avoids the possibility of selecting an intermediate element in the hierarchy, which usually is something desired. Making a variant that allows to select intermediate elements should be easy.

This is the modified html:

<div class="NestedSelect">
    <a onclick="toggleDiv(this)">Fruit</a>
    <div>
        <label>
            <input type="radio" name="MySelectInputName"><span>Apple</span></label>
        <label>
            <input type="radio" name="MySelectInputName"><span>Banana</span></label>
        <label>
            <input type="radio" name="MySelectInputName"><span>Orange</span></label>
    </div>

    <a onclick="toggleDiv(this)">Drink</a>
    <div>
        <label>
            <input type="radio" name="MySelectInputName"><span>Water</span></label>

        <a onclick="toggleDiv(this)">Soft</a>
        <div>
            <label>
                <input type="radio" name="MySelectInputName"><span>Cola</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Soda</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Lemonade</span></label>
        </div>

        <a onclick="toggleDiv(this)">Hard</a>
        <div>
            <label>
                <input type="radio" name="MySelectInputName"><span>Bear</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Whisky</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Vodka</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Gin</span></label>
        </div>
    </div>
</div>

A small javascript/jQuery function:

function toggleDiv(element) {
    $(element).next('div').toggle('medium');
}

And the css:

.NestedSelect {
    display: inline-block;
    height: 100%;
    border: 1px Black solid;
    overflow-y: scroll;
}

.NestedSelect a:hover, .NestedSelect span:hover  {
    background-color: #0092ff;
    color: White;
    cursor: pointer;
}

.NestedSelect input[type="radio"] {
    display: none;
}

.NestedSelect input[type="radio"] + span {
    display: block;
    padding-left: 0px;
    padding-right: 5px;
}

.NestedSelect input[type="radio"]:checked + span {
    background-color: Black;
    color: White;
}

.NestedSelect div {
    display: none;
    margin-left: 15px;
    border-left: 1px black
    solid;
}

.NestedSelect label > span:before, .NestedSelect a:before{
    content: '- ';
}

.NestedSelect a {
    display: block;
}

Running sample in JSFiddle

Manicdepressive answered 7/12, 2016 at 17:41 Comment(0)
U
5

I think if you have something that structured and complex, you might consider something other than a single drop-down box.

Universalize answered 24/6, 2009 at 11:36 Comment(1)
Not my design, I just implement things, although I agree that it's reasonably insane. Part of the joy of being brought in to work on a pre-agreed design.Infinite
B
5

I know this was quite a while ago, however I have a little extra to add:

This is not possible in HTML5 or any previous specs, nor is it proposed in HTML5.1 yet. I have made a request to the public-html-comments mailing list, but we'll see if anything comes of it.

Regardless, whilst this is not possible using <select> yet, you can achieve a similar effect with the following HTML, plus some CSS for prettiness:

<ul>
  <li>
	<input type="radio" name="location" value="0" id="loc_0" />
	<label for="loc_0">United States</label>
	<ul>
	  <li>
		Northeast
        <ul>
	      <li>
			<input type="radio" name="location" value="1" id="loc_1" />
			<label for="loc_1">New Hampshire</label>
		  </li>
          <li>
			<input type="radio" name="location" value="2" id="loc_2" />
			<label for="loc_2">Vermont</label>
		  </li>
          <li>
			 <input type="radio" name="location" value="3" id="loc_3" />
			 <label for="loc_3">Maine</label>
		  </li>
		 </ul>
	   </li>
       <li>
		   Southeast
           <ul>
			 <li>
				<input type="radio" name="location" value="4" id="loc_4" />
				<label for="loc_4">Georgia</label>
			 </li>
             <li>
				<input type="radio" name="location" value="5" id="loc_5" />
				<label for="loc_5">Alabama</label>
			 </li>
		   </ul>
		</li>
	  </ul>
    </li>
    <li>
	   <input type="radio" name="location" value="6" id="loc_6" />
	   <label for="loc_6">Canada</label>
       <ul>
		  <li>
			 <input type="radio" name="location" value="7" id="loc_7" />
			 <label for="loc_7">Ontario</label>
		  </li>
          <li>
			  <input type="radio" name="location" value="8" id="loc_8" />
		      <label for="loc_8">Quebec</label>
		  </li>
          <li>
			   <input type="radio" name="location" value="9" id="loc_9" />
			   <label for="loc_9">Manitoba</label>
		  </li>
		</ul>
	 </li>
  </ul>

As an extra added benefit, this also means you can allow selection of the <optgroups> themselves. This might be useful if you had, for example, nested categories where the categories go into heavy detail and you want to allow users to select higher up in the hierarchy.

This will all work without JavaScript, however you might wish to add some to hide the radio buttons and then change the background color of the selected item or something.

Bear in mind, this is far from a perfect solution, but if you absolutely need a nested select with reasonable cross-browser compatibility, this is probably as close as you're going to get.

Burcham answered 9/6, 2013 at 8:21 Comment(4)
Can you add some sample CSS prettiness for those of us who do not know CSS as well as maybe we should?Unyielding
This is nothing more than a nested list. This completely misses the point of the question, which is to have some kind of collapsible list.Grenadine
@Grenadine Maybe you should read the question again. At no point does he state that the sections need to be collapsible. The specific problem he was looking to solve was a method of having nested <optgroup>s which indent. Obviously one can make HTML render like a dropdown with some very simple CSS and/or JS, but OP was not looking for help with that.Burcham
This is a decent answer for how to present things visually, but a list tag is not the equivalent of a select. You would need more CSS and Javascript code to make it behave like one.Riti
C
4

I needed clean and lightweight solution (so no jQuery and alike), which will look exactly like plain HTML, would also continue working when only plain HTML is preset (so javascript will only enhance it), and which will allow searching by starting letters (including national UTF-8 letters) if possible where it does not add extra weight. It also must work fast on very slow browsers (think rPi - so preferably no javascript executing after page load).

In firefox it uses CSS identing and thus allow searching by letters, and in other browsers it will use &nbsp; prepending (but there it does not support quick search by letters). Anyway, I'm quite happy with results.

You can try it in action here

It goes like this:

CSS:

.i0 { }
.i1 { margin-left: 1em; }
.i2 { margin-left: 2em; }
.i3 { margin-left: 3em; }
.i4 { margin-left: 4em; }
.i5 { margin-left: 5em; }

HTML (class "i1", "i2" etc denote identation level):

<form action="/filter/" method="get">
<select name="gdje" id="gdje">
<option value=1 class="i0">Svugdje</option>
<option value=177 class="i1">Bosna i Hercegovina</option>
<option value=190 class="i2">Babin Do</option>  
<option value=258 class="i2">Banja Luka</option>
<option value=181 class="i2">Tuzla</option>
<option value=307 class="i1">Crna Gora</option>
<option value=308 class="i2">Podgorica</option>
<option value=2 SELECTED class="i1">Hrvatska</option>
<option value=5 class="i2">Bjelovarsko-bilogorska županija</option>
<option value=147 class="i3">Bjelovar</option>
<option value=79 class="i3">Daruvar</option>  
<option value=94 class="i3">Garešnica</option>
<option value=329 class="i3">Grubišno Polje</option>
<option value=368 class="i3">Čazma</option>
<option value=6 class="i2">Brodsko-posavska županija</option>
<option value=342 class="i3">Gornji Bogićevci</option>
<option value=158 class="i3">Klakar</option>
<option value=140 class="i3">Nova Gradiška</option>
</select>
</form>

<script>
<!--
        window.onload = loadFilter;
// -->   
</script>

JavaScript:

function loadFilter() {
  'use strict';
  // indents all options depending on "i" CSS class
  function add_nbsp() {
    var opt = document.getElementsByTagName("option");
    for (var i = 0; i < opt.length; i++) {
      if (opt[i].className[0] === 'i') {
      opt[i].innerHTML = Array(3*opt[i].className[1]+1).join("&nbsp;") + opt[i].innerHTML;      // this means "&nbsp;" x (3*$indent)
      }
    }
  }
  // detects browser
  navigator.sayswho= (function() {
    var ua= navigator.userAgent, tem,
    M= ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*([\d\.]+)/i) || [];
    if(/trident/i.test(M[1])){
        tem=  /\brv[ :]+(\d+(\.\d+)?)/g.exec(ua) || [];
        return 'IE '+(tem[1] || '');
    }
    M= M[2]? [M[1], M[2]]:[navigator.appName, navigator.appVersion, '-?'];
    if((tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
    return M.join(' ');
  })();
  // quick detection if browser is firefox
  function isFirefox() {
    var ua= navigator.userAgent,
    M= ua.match(/firefox\//i);  
    return M;
  }
  // indented select options support for non-firefox browsers
  if (!isFirefox()) {
    add_nbsp();
  }
}  
Cythera answered 13/3, 2015 at 18:16 Comment(3)
Adding CSS classes to control the indenting would solve the OP's question without impairing the functionality of the select. Good idea, as optgroups can't be nested.Riti
Why do you have code for margin-left: 1em; when it's not doing anything and you end up using &nbsp;s anyway just to make the margin appear? Your CSS code here is doing nothing at all. Demo of Left Margins Not WorkingPic
@Pic note that the answer above is from 2015 and has special isFirefox() handling - back then firefox didn't need &nbsp; (which was breaking search-as-you-type functionality of dropdown lists) and it was honoring margins in dropdown lists back then (sadly, at least as with Firefox 78.5, it no longer works that way)Cythera
T
4

I have written a beautiful, nested select. Maybe it will help you.

https://jsfiddle.net/nomorepls/tg13w5r7/1/

function on_change_select(e) {
  alert(e.value, e.title, e.option, e.select);
}

$(document).ready(() => {
  // NESTED SELECT

  $(document).on('click', '.nested-cell', function() {
    $(this).next('div').toggle('medium');
  });

  $(document).on('change', 'input[name="nested-select-hidden-radio"]', function() {
    const parent = $(this).closest(".nested-select");
    const value = $(this).attr('value');
    const title = $(this).attr('title');
    const executer = parent.attr('executer');
    if (executer) {
      const event = new Object();
      event.value = value;
      event.title = title;
      event.option = $(this);
      event.select = parent;
      window[executer].apply(null, [event]);
    }
    parent.attr('value', value);
    parent.parent().slideToggle();
    const button = parent.parent().prev();
    button.toggleClass('active');
    button.addClass('selected');
    button.children('.nested-select-title').html(title);
  });

  $(document).on('click', '.nested-select-button', function() {
    const button = $(this);
    let select = button.parent().children('.nested-select-wrapper');

    if (!button.hasClass('active')) {
      select = select.detach();
      if (button.height() + button.offset().top + $(window).height() * 0.4 > $(window).height()) {
        select.insertBefore(button);
        select.css('margin-top', '-44vh');
        select.css('top', '0');
      } else {
        select.insertAfter(button);
        select.css('margin-top', '');
        select.css('top', '40px');
      }
    }
    select.slideToggle();
    button.toggleClass('active');
  });
});
.container {
  width: 200px;
  position: relative;
  top: 0;
  left: 0;
  right: 0;
  height: auto;
}

.nested-select-box {
  font-family: Arial, Helvetica, sans-serif;
  display: block;
  position: relative;
  width: 100%;
  height: fit-content;
  cursor: pointer;
  color: #2196f3;
  height: 40px;
  font-size: small;
  /* z-index: 2000; */
}

.nested-select-box .nested-select-button {
  border: 1px solid #2196f3;
  position: absolute;
  width: calc(100% - 20px);
  padding: 0 10px;
  min-height: 40px;
  word-wrap: break-word;
  margin: 0 auto;
  overflow: hidden;
}

.nested-select-box.danger .nested-select-button {
  border: 1px solid rgba(250, 33, 33, 0.678);
}

.nested-select-box .nested-select-button .nested-select-title {
  padding-right: 25px;
  padding-left: 25px;
  width: calc(100% - 50px);
  margin: auto;
  height: fit-content;
  text-align: center;
  vertical-align: middle;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
}

.nested-select-box .nested-select-button.selected .nested-select-title {
  bottom: unset;
  top: 5px;
}

.nested-select-box .nested-select-button .nested-select-title-icon {
  position: absolute;
  height: 20px;
  width: 20px;
  top: 10px;
  bottom: 10px;
  right: 7px;
  transition: all 0.5s ease 0s;
}

.nested-select-box .nested-select-button.active .nested-select-title-icon {
  -moz-transform: scale(-1, -1);
  -o-transform: scale(-1, -1);
  -webkit-transform: scale(-1, -1);
  transform: scale(-1, -1);
}

.nested-select-box .nested-select-button .nested-select-title-icon::before,
.nested-select-box .nested-select-button .nested-select-title-icon::after {
  content: "";
  background-color: #2196f3;
  position: absolute;
  width: 70%;
  height: 2px;
  transition: all 0.5s ease 0s;
  top: 9px;
}

.nested-select-box .nested-select-button .nested-select-title-icon::before {
  transform: rotate(45deg);
  left: -1.6px;
}

.nested-select-box .nested-select-button .nested-select-title-icon::after {
  transform: rotate(-45deg);
  left: 7px;
}

.nested-select-box .nested-select-wrapper {
  width: 100%;
  top: 40px;
  position: relative;
  border: 1px solid #2196f3;
  background: #ffffff;
  z-index: 2005;
  opacity: 1;
}

.nested-select {
  font-family: Arial, Helvetica, sans-serif;
  display: inline-block;
  overflow-y: scroll;
  max-height: 40vh;
  width: calc(100% - 10px);
  padding: 5px;
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.nested-select::-webkit-scrollbar {
  display: none;
}

.nested-select a,
.nested-select span {
  padding: 0 5px;
  border-radius: 3px;
  cursor: pointer;
  text-align: start;
}

.nested-select a:hover {
  background-color: #62b2f3;
  color: #ffffff;
}

.nested-select span:hover {
  background-color: #c4c4c4;
  color: #ffffff;
}

.nested-select input[type="radio"] {
  display: none;
}

.nested-select input[type="radio"]+span {
  display: block;
}

.nested-select input[type="radio"]:checked+span {
  background-color: #2196f3;
  color: #ffffff;
}

.nested-select div {
  margin-left: 15px;
}

.nested-select label>span:before,
.nested-select a:before {
  content: "\2022";
  margin-right: 5px;
}

.nested-select a {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
  <div class="nested-select-box w-100">
    <div class="nested-select-button">
      <p class="nested-select-title">
        Account
      </p>
      <span class="nested-select-title-icon"></span>
    </div>
    <div class="nested-select-wrapper" style="display: none;">
      <div class="nested-select" executer="on_change_select">

        <label>
        <input title="Accounting and legal services" value="1565142000000891539" type="radio" name="nested-select-hidden-radio">
        <span>Accounting and legal services</span>
      </label>



        <label>
        <input title="Advertising agencies" value="1565142000000891341" type="radio" name="nested-select-hidden-radio">
        <span>Advertising agencies</span>
      </label>



        <a class="nested-cell">Advertising And Marketing</a>
        <div>



          <label>
          <input title="Advertising agencies" value="1565142000000891341" type="radio" name="nested-select-hidden-radio">
          <span>Advertising agencies</span>
        </label>



          <a class="nested-cell">Adwords - traffic</a>
          <div>



            <label>
            <input title="Adwords - traffic: Charters and general search" value="1565142000003929177" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Charters and general search</span>
          </label>



            <label>
            <input title="Adwords - traffic: Distance course" value="1565142000007821291" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Distance course</span>
          </label>



            <label>
            <input title="Adwords - traffic: Events" value="1565142000003929189" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Events</span>
          </label>



            <label>
            <input title="Adwords - traffic: Practices" value="1565142000003929165" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Practices</span>
          </label>



            <label>
            <input title="Adwords - traffic: Sailing tours" value="1565142000003929183" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Sailing tours</span>
          </label>



            <label>
            <input title="Adwords - traffic: Theoretical courses" value="1565142000003929171" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Theoretical courses</span>
          </label>



          </div>



          <label>
          <input title="Branded products" value="1565142000000891533" type="radio" name="nested-select-hidden-radio">
          <span>Branded products</span>
        </label>



          <label>
          <input title="Business cards" value="1565142000005438323" type="radio" name="nested-select-hidden-radio">
          <span>Business cards</span>
        </label>



          <a class="nested-cell">Facebook, Instagram - traffic</a>
          <div>



            <label>
            <input title="Facebook, Instagram - traffic: Charters and general search" value="1565142000003929145" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Charters and general search</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Distance course" value="1565142000007821285" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Distance course</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Events" value="1565142000003929157" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Events</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Practices" value="1565142000003929133" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Practices</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Sailing tours" value="1565142000003929151" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Sailing tours</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Theoretical courses" value="1565142000003929139" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Theoretical courses</span>
          </label>



          </div>



          <label>
          <input title="Offline Advertising (posters, banners, partnerships)" value="1565142000000891377" type="radio" name="nested-select-hidden-radio">
          <span>Offline Advertising (posters, banners, partnerships)</span>
        </label>



          <label>
          <input title="Photos, video etc." value="1565142000000891371" type="radio" name="nested-select-hidden-radio">
          <span>Photos, video etc.</span>
        </label>



          <label>
          <input title="Prize fund" value="1565142000001404931" type="radio" name="nested-select-hidden-radio">
          <span>Prize fund</span>
        </label>



          <label>
          <input title="SEO" value="1565142000000891365" type="radio" name="nested-select-hidden-radio">
          <span>SEO</span>
        </label>



          <label>
          <input title="SMM Content creation (texts, copywriting)" value="1565142000000891389" type="radio" name="nested-select-hidden-radio">
          <span>SMM Content creation (texts, copywriting)</span>
        </label>



          <a class="nested-cell">YouTube</a>
          <div>



            <label>
            <input title="YouTube: travel expenses" value="1565142000008100163" type="radio" name="nested-select-hidden-radio">
            <span>YouTube: travel expenses</span>
          </label>



            <label>
            <input title="Youtube: video editing" value="1565142000008100157" type="radio" name="nested-select-hidden-radio">
            <span>Youtube: video editing</span>
          </label>



          </div>



        </div>

      </div>
    </div>
  </div>
</div>
Teazel answered 9/7, 2020 at 12:31 Comment(0)
R
2

It seems that the initial inventors of the standard want a group-based selection list rather than a hierarchical list. And, unfortunately, future HTML standard probably won't change this due to a high risk of breaking downward compatibility.

When acceptable, converting the hierarchical list into a segment-named group list may be a workaround:

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
  </optgroup>
  <optgroup label="Level One / Level Two">
    <option> A.B.1 </option>
  </optgroup>
  <optgroup label="Level One">
    <option> A.2 </option>
  </optgroup>
</select>

If a change of order is acceptable, consider merging items with identical group name for better look:

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <option> A.2 </option>
  </optgroup>
  <optgroup label="Level One / Level Two">
    <option> A.B.1 </option>
  </optgroup>
</select>

If both are not acceptable and a hierarchical selection list is really required, implementation of a modal dialog with checkboxes (and possibly some JavaScript keyboard trap for multi-selection with Shift or Ctrl key) may be needed.

Risa answered 24/7, 2021 at 14:34 Comment(0)
H
1

Nested Accordeon select

I made this approach since I couldn´t find what I was searching. A nested accordeon select. Its CSS is very simple and can be improved. The only thing you need is an object with keys and values you want to add into the select. Keys would be subgroups, and key values (arrays and single elements) would be selectable items.

Once you have your array, only thing you need to do is to call

initAccordeon(obj);

with your data object as an argument, and the nested accordeon will appear:

const obj = {
  Cars: {
    SwedishCars: [
      "Volvo",
      "Saab"
    ],
    GermanCars: [
      "Mercedes",
      {
        Audi: [
          "Audi A3", 
          "Audi A4", 
          "Audi A5" 
        ]
      }
    ]
  },
  Food: {
    Fruits: [
      "Orange",
      "Apple",
      "Banana"
    ],
    SaltyFoods: [
      "Pretzels",
      "Burger",
      "Noodles"
    ],
    Drinks: "Water"
  }
};

initAccordeon(obj);   // <--------------------------- Call initialization

function accordeonAddEvents() {
  Array.from(document.getElementsByClassName("accordeon-header")).forEach(function(header) {
    if (header.getAttribute("listener") !== "true") {
      header.addEventListener("click", function() {
        this.parentNode.getElementsByClassName("accordeon-body")[0].classList.toggle("hide");
      });
      header.setAttribute("listener", "true");
    }
  });

  Array.from(document.getElementsByClassName("button-group")).forEach(function(but) {
    if (but.getAttribute("listener") !== "true") {
      but.addEventListener("click", function() {
        if (this.getAttribute("depth") === "-1") {
          let header = this;
          while ((header = header.parentElement) && header.className !== "accordeon");
          header.getElementsByClassName("accordeon-header")[0].innerHTML = this.innerHTML;
          return;
        }
        const groups = Array.from(this.parentNode.getElementsByClassName("accordeon-group"));
        groups.forEach(g => {
          if (g.getAttribute("uuid") === this.getAttribute("uuid") &&
            g.getAttribute("depth") === this.getAttribute("depth")) {
            g.classList.toggle("hide");
          }
        });
      });
      but.setAttribute("listener", "true");
    }
  });
}

function initAccordeon(data) {
  accordeons = Array.from(document.getElementsByClassName("accordeon-body"));
  accordeons.forEach(acc => {
    acc.innerHTML = "";
    const route = (subObj, keyIndex = 0, parent = acc, depth = 0) => {
      const keys = Object.keys(subObj);
      if (typeof subObj === 'object' && !Array.isArray(subObj) && keys.length > 0) {
        while (keyIndex < keys.length) {
          var but = document.createElement("button");
          but.className = "button-group";
          but.setAttribute("uuid", keyIndex);
          but.setAttribute("depth", depth);
          but.innerHTML = keys[keyIndex];
          var group = document.createElement("div");
          group.className = "accordeon-group hide";
          group.setAttribute("uuid", keyIndex);
          group.setAttribute("depth", depth);
          route(subObj[keys[keyIndex]], 0, group, depth + 1);
          keyIndex++;
          parent.append(but);
          parent.append(group);
        }
      } else {
        if (!Array.isArray(subObj)) subObj = [subObj];
        subObj.forEach((e, i) => {
          if (typeof e === 'object') {
              route(e, 0, parent, depth);
          } else {
              var but = document.createElement("button");
              but.className = "button-group";
              but.setAttribute("uuid", i);
              but.setAttribute("depth", "-1");
              but.innerHTML = e;
              parent.append(but);
          }
        });
      }
    };
    route(data);
  });
  accordeonAddEvents();
}
.accordeon {
  width: 460px;
  height: auto;
  min-height: 340px;
  font-size: 20px;
  cursor: pointer;
  user-select: none;
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  -o-user-select: none;
  display: block;
  position: relative;
  z-index: 10;
}

.accordeon-header {
  display: inline-block;
  width: 450px;
  border: solid 0.1vw black;
  border-radius: 0.2vw;
  background-color: white;
  padding-left: 10px;
  color: black;
}

.accordeon-header:hover {
  opacity: 0.7;
}

.accordeon-body {
  display: block;
  position: absolute;
}

.button-group {
  display: block;
  cursor: pointer;
  width: 460px;
  text-align: left;
  font-size: 20px;
  font-weight: bold;
}

.accordeon-group {
  padding-left: 20px;
}

.accordeon-group .button-group {
  width: 100%;
}

.button-group[depth="-1"] {
  color: green;
}

.hide {
  display: none;
}
<div class="accordeon">
  <span class="accordeon-header">Select something</span>
  <div class="accordeon-body hide">
      
  </div>
</div>
Hamo answered 29/7, 2021 at 15:44 Comment(0)
F
1

As of the end of 2021 there is still no nesting of <optgroup>.

I've made a simple SCSS, which requires no JS at all. For <select size> and <select multiple> to align option based on <option class="depth-[0-100]">.

Only differs at <optgroup> which should not be used anymore, instead of them use <option class="depth-n" disabled label="..." />

<select size="10">
  <!-- Behaves similar to optgroup if disabled -->
  <option class="depth-0" label="Root" disabled />
  
  <!-- Examples -->
  <option class="depth-1">1</option>
  <option class="depth-2" value="something">1.2</option>
  <option class="depth-20" value="Level 20">20</option>
  
  <option class="depth-0" label="Next item on Root" disabled />
  <option class="depth-1" label="Sub" disabled />
  <option class="depth-2" value="#fff">White</option>
</select>
/* can easily be adjusted to support even more */
@for $i from 0 through 100 { 
  select[size] option.depth-#{$i},
  select[multiple] option.depth-#{$i} {
      padding-left: calc(0.2em + calc(0.8em * #{$i}));
  }
  /**
   * The label on options is handled similar to <optgroup label="..."> 
   * (tested with chrome 87 and 95)

   * This styles a <option> similar to an option group
   * but it requires the attribute data-deppth and disabled
   */
  select[size] option.depth-#{$i}[label]:disabled, 
  select[multiple] option.depth-#{$i}[label]:disabled {
    font-weight: bold;
    color: initial;
  }
}

Example Fiddle

Flyblow answered 29/10, 2021 at 16:33 Comment(0)
C
0

with divs you can do the same thing only you need an additional logic to select what you need, in this case you'd use also hidden input to change id what user has selected. It is not complete example, but I hope will help. Try to go to clothes in my example.

const categories = {
  0: {
    id: 1,
    parent_id: null,
    name: 'Pets'
  },
  1: {
    id: 2,
    parent_id: 1,
    name: 'Dogs'
  },
  2: {
    id: 3,
    parent_id: 1,
    name: 'Cats'
  },
  3: {
    id: 4,
    parent_id: 1,
    name: 'Reptiles'
  },
  4: {
    id: 5,
    parent_id: null,
    name: 'Clothes'
  },
  5: {
    id: 6,
    parent_id: 5,
    name: 'Mens'
  },
  6: {
    id: 7,
    parent_id: 5,
    name: 'Womens'
  },
  7: {
    id: 8,
    parent_id: 6,
    name: 'Sweaters'
  },
  8: {
    id: 9,
    parent_id: 8,
    name: 'Coloured'
  },
  9: {
    id: 10,
    parent_id: 8,
    name: 'Striped'
  }
};

$(function () {
    showCategories();
});

function showCategories()
{
    let content = '';
    for (const [_, category] of Object.entries(categories)) {
    if (!category.parent_id) {
        let arrow = '';
      let onclick = '';
      
      if (hasSubcategories(category)) {
            onclick = ' onclick="showSubcategories(' + category.id + ');"';
            arrow += ' ->';
      }
      
        content += `<div${onclick}>${category.name}${arrow}</div>`;
    }
  }
  $('.categories').html(content);
}

function showSubcategories(parent_id) {
    let content = '';
  
  if (isParentSubcategory(parent_id)) {
    content += '<div onclick="showSubcategories(getParentCategoryId(' + parent_id + '));"><- Go back</div>';
  } else {
    content += '<div onclick="showCategories();"><- Go back</div>';
  }
    for (const [_, category] of Object.entries(categories)) {
        if (parent_id === category.parent_id) {
        let arrow = '';
        let onclick = '';

        if (hasSubcategories(category)) {
            onclick = ' onclick="showSubcategories(' + category.id + ');"';
            arrow += ' ->';
        }
          content += `<div${onclick}>${category.name}${arrow}</div>`;
        }
      }
      $('.categories').html(content);
}

function getParentCategoryId(id) {
    for (const [_, category] of Object.entries(categories)) {
    if (category.id === id) {
            return category.parent_id;
    }
  }
}

function isParentSubcategory(parent_id)
{
    for (const [_, category] of Object.entries(categories)) {
    if (parent_id === category.id && category.parent_id === null) {
        return false;
    }
  }
  
  return true;
}

function hasSubcategories(parent) {
    for (const [_, category] of Object.entries(categories)) {
    if (category.parent_id === parent.id) {
        return true;
    }
  }
}
.categories {
  display: block;
  padding: 20px 10px;
  background-color: #f2f2f2;
  max-width: 400px;
  border: 1px solid #000;
  border-radius: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div class="categories"></div>
Claptrap answered 28/3 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.