skip to Main Content

I have a menu from which a user will have to pick a single option off of every row.

Mockup:

Menu mockup

This menu is represented in data like this:

menu = [
  [41, 36, 573, 572],
  [47, 96, 79],
  [480, 479]
]

The number of rows is variable, as is the number of options in each. The numbers that represent each option are always unique.

The issue is that not every combination is valid. I have a list of the "allowed" combinations available like this:

variants = [
  [36, 47, 480],
  [41, 96, 480],
  [41, 79, 479],
  [41, 47, 479],
  [572, 47, 479],
  [573, 47, 479],
]

I have access to an array that grows with each choice selected:

options = []
options = [41]
options = [41, 47]
options = [41, 47, 479]

Because not every combination is valid, menu options that would result in invalid combinations must be disabled, so given the options array, I need an algorithm that generates a data structure like this:

menu_toggles = [
  [true, true, true, true],
  [true, false, true],
  [false, true]
]

This structure can then be used to enable or disable the actual menu options as in the mockup.

To simplify things:

  • The user has to pick one option from the first row, and then the one below, and so on.
  • If an option in an earlier row is selected, the options selected after that one are removed.
  • Options that cannot result in a valid combination need to be disabled, ie. the corresponding place in menu_toggles must be false.

Examples

Using the menu and variants above.

No options selected

options = []
menu_toggles = [
  [true, true, true, true],
  [false, false, false],
  [false, false]
]

One option selected

options = [36]
menu_toggles = [
  [true, true, true, true],
  [true, false, false],
  [false, false]
]

Two options selected

options = [36, 47]
menu_toggles = [
  [true, true, true, true],
  [true, false, false],
  [true, false]
]

Two options selected (other)

options = [41, 47]
menu_toggles = [
  [true, true, true, true],
  [true, false, false],
  [false, true]
]

Current approach (as of this writing)

My current solution looks like this:

const available_variants = variants
  .filter(v => {
    if (!v) return false;
    if (!options.every((o, i) => o === v[i])) return false;
    return true;
  });

const menu_toggles = [
  ...menu
    .slice(0, options.length)
      .map(l => l
        .map(() => true)),

  ...menu
    .slice(options.length)
      .map((z, i) => z
        .map((value, j) => available_variants
          .some(v => v[i + options.length] === value)))
];

In this one, I first make a list of combinations that make sense for the "next" row to select, but obviously this only disables menu options from that row down.

4

Answers


  1. Chosen as BEST ANSWER

    The code below provides the intended behavior.

    const menu_toggles = menu
      .map((row, i) => {
        // This filters `variants` down to options
        // that matter specifically for this `row`
        const available_variants = variants
          .filter(variant => options
            .slice(0, i)
            .every((o, i) => o === variant[i]));
      
        return row
          .map(option => {
            // Disable options in next rows
            if (i > options.length) return false;
    
            // Enable option if it exists in filtered possible variants
            if (available_variants.some(v => v[i] === option)) return true;
    
            // Default to disabled
            return false;
          });
      });
    

  2. I made a super basic toy example that shows you one way to approach it. In particular, I think you’re interested in the disableButtons function.

    const menu = [
      [41, 36, 573, 572],
      [47, 96, 79],
      [480, 479]
    ];
    
    const variants = [
      [36, 47, 480],
      [41, 96, 480],
      [41, 79, 479],
      [41, 47, 479],
      [572, 47, 479],
      [573, 47, 479],
    ];
    
    const selections = [];
    
    const buttonList = {};
    
    function disableButtons() {
        const allowedVariants = variants.filter(arr => {
        return selections.every(x => arr.includes(x));
      });
      console.log(allowedVariants);
      const allowedValues = allowedVariants.flatMap(x => x).map(x => '' + x);
      console.log(allowedValues);
      Object.keys(buttonList).forEach(key => {
        console.log(key);
        if (!allowedValues.includes(key)) {
            buttonList[key].prop('disabled', true);
        }
      })
    }
    
    function render(menu) {
        menu.forEach(arr => {
        const $div = $('<div></div>');
        $('#outer-container').append($div);
        arr.forEach(value => {
            const $newButton = $(`<button>${value}</button>`);
          $div.append($newButton);
          buttonList[value] = $newButton;
          $newButton.click(function() {
            selections.push(value);
            disableButtons();
          });
        })
      })
    }
    
    render(menu);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="outer-container"></div>
    Login or Signup to reply.
  3. I don’t know what system to render/update your buttons you have. Here an example where it computes a whitelist based on variants and the current options and then toggles the disabled property of the buttons/checkboxes accordingly.

    let menu = [
      [41, 36, 573, 572],
      [47, 96, 79],
      [480, 479]
    ]
    
    let variants = [
      [36, 47, 480],
      [41, 96, 480],
      [41, 79, 479],
      [41, 47, 479],
      [572, 47, 479],
      [573, 47, 479],
    ]
    
    let options = [];
    
    function updateDisabled() {
      // determine which ids are part of still valid combinations
      // the current options are always left clickable so that we can turn them off.
      const allowed = new Set(options);
      // so we don't have to create the check function on every iteration in the loop.
      let variant, check = v => variant.includes(v);
      
      for (variant of variants) {
        if(options.every(check)){
          for(const item of variant) allowed.add(item);
        }
      }
    
      // set the menu items disabled if they are no longer allowed.
      for (const item of container.querySelectorAll("input")) {
        item.disabled = !allowed.has(+item.dataset.value);
      }
    }
    
    const container = document.querySelector("#container");
    container.oninput = function(e) {
      const input = e.target;
      const item = +input.dataset.value;
    
      if (input.checked) {
        options.push(item);
      } else {
        options = options.filter(elm => elm !== item);
      }
      
      updateDisabled();
    }
    
    // render menu
    container.innerHTML = menu.map(row => `<div>${
      row.map(item => `<label>
        <input type="checkbox" data-value="${item}">
        <span>${item}</span>
      </label>`).join("n")
    }</div>`).join("n");
    
    updateDisabled();
    <div id="container"></div>
    Login or Signup to reply.
  4. You could build a tree of possibilities and enable the menu row and disable the row after the selected level.

    const
        buildMenu = (menu, tree) => {
            document.getElementById('menu').innerHTML = menu
                .map((a, i) => a
                    .map((m, j) => `<button id="menu${i}|${j}" onclick="toggle(${i}, ${m})"${i ? ' disabled': ''}>${m}</button>`)
                    .join(' ')
                )
                .join('<br>');
        },
        toggle = (index, option) => {
            const
                getButton = (i, j) => document.getElementById(`menu${i}|${j}`),
                push = options[index] !== option;
            
            options.length = index;
            if (push) options.push(option);
    
            document.getElementById('options').innerHTML = options.join(' ') || 'none';
    
            menu.forEach((m, i) => m.forEach((v, j) => {
                const temp = options.slice(0, i).reduce((o, k) => o[k] || {}, tree);
                getButton(i, j).disabled = !temp[v];
                getButton(i, j).style.fontWeight = { true: 'bold', false: '' }[options[i] === v];
            }));
        },
        menu = [[41, 36, 573, 572], [47, 96, 79], [480, 479]],
        variants = [[36, 47, 480], [41, 96, 480], [41, 79, 479], [41, 47, 479], [572, 47, 479], [573, 47, 479]],
        tree = variants.reduce((r, a) => {
            a.reduce((o, k) => o[k] ??= {}, r);
            return r;
        }, {}),
        options = [];
    
    buildMenu(menu, tree);
    <div id="menu"></div><br>
    options: <span id="options">none</span>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search