Skip to content
Advertisement

Sort Option children by index value for both parent Selects (Javascript Only)

I’m trying to create a Dual List Box using only vanilla Javascript, no JQuery or JS packages, for learning purposes.

When a user double-clicks an Option, that Option appends to the other parent Select (boxB) and is removed from the original parent Select (boxA), and vice versa. The Option elements have data-index attributes with ascending numerical values. How do I maintain the numerical order of the Options by index value, regardless of which parent Select they append?

<!-- HTML -->
<select id="ctst1" class="form-control form-control-sm" size="8" multiple>
    <option data-index="1" value="CPT">CPT</option>
    <option data-index="2" value="SST">SST</option>
    <option data-index="3" value="EDTA">EDTA</option>
    <option data-index="4" value="PAXgene">PAXgene</option>
    <option data-index="5" value="NP SWB">NP SWB</option>
    <option data-index="6" value="OP SWB">OP SWB</option>
    <option data-index="7" value="RCTL SWB">RCTL SWB</option>
    <option data-index="8" value="Self NS">Self NS</option>
</select>
<select id="ctst2" class="form-control form-control-sm" size="8" multiple></select>

// Javascript
const dataFrame = {
    "selCollTubeSampTypes": [
        ["CPT", "CPT"],
        ["SST", "SST"],
        ["EDTA", "EDTA"],
        ["PAXgene", "PAXgene"],
        ["NP SWB", "NP SWB"],
        ["OP SWB", "OP SWB"],
        ["RCTL SWB", "RCTL SWB"],
        ["Self NS", "Self NS"]
    ]
};

function dualListBox(qryId1, qryId2, dataset) {
    const boxA = document.getElementById(qryId1);
    const boxB = document.getElementById(qryId2);
    // Populate avail box with data
    for (let i = 0; i < dataset.length; i++) {
        const eleOption = document.createElement("option");
        eleOption.dataset.index = i + 1; // Adds data-index value to Option node
        if (dataset[i][0] === "") {
            eleOption.textContent = dataset[i][1];
        } else {
            eleOption.value = dataset[i][0];
            eleOption.textContent = dataset[i][1];
        };
        boxA.appendChild(eleOption);
    };
    // When option in Box A is double-clicked, move it to Box B
    boxA.addEventListener("dblclick", e => {
        let eTargetOpt = e.target.closest("option");
        if (!eTargetOpt) return;
        if (!boxA.contains(eTargetOpt)) return;
        if (eTargetOpt) {
            boxB.appendChild(eTargetOpt);
            boxB.selectedIndex = "-1";
            boxA.removeChild(eTargetOpt);
        };
        e.stopPropagation();
    }, false);
    // When option in Box B is double-clicked, move it to Box A
    boxB.addEventListener("dblclick", e => {
        let eTargetOpt = e.target.closest("option");
        if (!eTargetOpt) return;
        if (!boxB.contains(eTargetOpt)) return;
        if (eTargetOpt) {
            boxA.appendChild(eTargetOpt);
            boxA.selectedIndex = "-1";
            boxB.removeChild(eTargetOpt);
        };
        e.stopPropagation();
    }, false);

dualListBox("ctst1", "ctst2", dataFrame.selCollTubeSampTypes);

Advertisement

Answer

Since you are basically using the same logic for both selects, I consolidated into one function. Rather than use ID’s, which can be problematic, we can use a single className and rely on basic JS to determine which is which. The sorting mechanism – your original question – works by taking all the option elements into an array, sorting the array by dataset.index, and then repopulating the select element. One set of logic works for both boxes

const dataFrame = {
  "selCollTubeSampTypes": [
    ["CPT", "CPT"],
    ["SST", "SST"],
    ["EDTA", "EDTA"],
    ["PAXgene", "PAXgene"],
    ["NP SWB", "NP SWB"],
    ["OP SWB", "OP SWB"],
    ["RCTL SWB", "RCTL SWB"],
    ["Self NS", "Self NS"]
  ]
};

const dualListBox = (className, dataset) => {
  let boxSet = document.querySelectorAll(`.${className}`);
  dataset.forEach((item, i) => {
    let val = item[0] != "" ? item[0] : item[1];
    boxSet[0].innerHTML += `<option value="${val}" data-index="${i}">${item[1]}</option>`;
  })
  boxSet.forEach(myBox => myBox.addEventListener("dblclick", e => {
    let eTargetOpt = e.target.closest("option");
    if (!eTargetOpt) return;
    let targBox = myBox == boxSet[0] ? boxSet[1] : boxSet[0]
    if (eTargetOpt) {
      let targOptions = [...targBox.querySelectorAll('option')];
      targBox.innerHTML = '';
      myBox.removeChild(eTargetOpt);
      targOptions.push(eTargetOpt);
      targOptions.sort((a, b) => a.dataset.index - b.dataset.index)
      targOptions.forEach(opt => targBox.appendChild(opt))
    }
    e.stopPropagation();
  }, false));
}

dualListBox("box-set", dataFrame.selCollTubeSampTypes);
<select class="form-control form-control-sm box-set" size="8" multiple></select>
<select class="form-control form-control-sm box-set" size="8" multiple></select>
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement