2

I am working on the basic concept for multiple group selections where some input options are disabled because they are generally unavailable at the moment and others being disabled because they were already picked elsewhere.

This can be two or more forms or multiple elements within one form as in the example. I need to link the numerical output of selections and disable/enable options between separated elements.

We use PHP to generate the option list alphabetically through a server-side PHP include. I left the PHP part out for having only static HTML to test.

The example shows two multiple select form inputs. Team members are identified by number in the background and with human names in the UX. The normally hidden numerical output is now made visible for testing below each team.

I wanted to simply pre-check already selected values from the hidden numerical output onBlur and onFocus when selecting the next team. Yet since the form elements are taken over by the multi.js, there are no Focus and Blur events triggered anymore.

CSS based on multi.min.css by Fabian Lindfors:

/* basic styling */
body { font-family: Avenir,sans-serif; }
.container { box-sizing: border-box; margin: 0 auto; max-width: 500px; padding: 0 20px; width: 100%; }
.developer { color:#999; font-size: small; margin-bottom:20px }
/* form and selection styling */
label{ margin-left:20px; color: #666; font-weight:bolder }
.multi-wrapper{ border: 1px solid #999; border-radius: 8px; width: 450px; margin:10px 0 10px 0 }
.multi-wrapper .non-selected-wrapper,.multi-wrapper .selected-wrapper{ box-sizing: border-box; display: inline-block; height: 150px; overflow-y: scroll; padding: 10px; vertical-align: top; width: 50% }
.multi-wrapper .non-selected-wrapper{ background: #fafafa; border-radius: 0 0 0 8px; border-right: 1px solid #ccc }
.multi-wrapper .selected-wrapper{ background: #FFF; border-radius: 0 0 8px 0 }
.multi-wrapper .header{ color: #4f4f4f; cursor: default; font-weight: 700; margin-bottom: 5px; padding: 5px 10px }
.multi-wrapper .item{ cursor: pointer; display: block; padding: 5px 10px }
.multi-wrapper .item: hover{ background: #ececec; border-radius: 2px }
.multi-wrapper .item-group{ padding: 5px 10px }
.multi-wrapper .item-group .group-label{ display: block; font-size: .875rem; opacity: .5; padding: 5px 0 }
.multi-wrapper .search-input{ border: 0; border-bottom: 1px solid #ccc; border-radius: 8px 8px 0 0; display: block; font-size: 1em; margin: 0; outline: 0; padding: 10px 20px; width: 100%; box-sizing: border-box }
.multi-wrapper .non-selected-wrapper .item.selected{ display:  none; opacity: .5 }
.multi-wrapper .non-selected-wrapper .item.disabled,.multi-wrapper .selected-wrapper .item.disabled{ opacity: .5; text-decoration: line-through }
.multi-wrapper .non-selected-wrapper .item.disabled: hover,.multi-wrapper .selected-wrapper .item.disabled: hover{ background: inherit; cursor: inherit}

The HTML form:

<div class="container">
    <h1>team selection demo</h1>
    <form>
        <label for="team_1">Select members for day shift</label>
        <select onChange="reportUpdatedValues(this,this.name);" 
            multiple="multiple"
            name="team_1"
            id="team_1_select">
        <option value="13" disabled="disabled">Alex</option>
        <option value="1">Bob</option>
        <option value="8">Diana</option>
        <option value="5">Frank</option>
        <option value="9">Fred</option>
        <option value="11">Helen</option>
        <option value="10">Jeanne</option>
        <option value="4">Linda</option>
        <option value="3">Mary</option>
        <option value="2" disabled="disabled">Max</option>
        <option value="7">Mo</option>
        <option value="6">Paul</option>
        <option value="12">Sara</option>
        </select>
        <span class="developer" style="display:inherit; padding:10px 0 10px 20px">
            normally hidden digital output:
        <input id="output_team_1" type="text" style="float:right">
        </span>
        <label for="team_2">Select members for night shift</label>
        <select onChange="reportUpdatedValues(this,this.name);" 
            multiple="multiple"
            name="team_2"
            id="team_2_select">
        <option value="13" disabled="disabled">Alex</option>
        <option value="1">Bob</option>
        <option value="8">Diana</option>
        <option value="5">Frank</option>
        <option value="9">Fred</option>
        <option value="11">Helen</option>
        <option value="10">Jeanne</option>
        <option value="4">Linda</option>
        <option value="3">Mary</option>
        <option value="2" disabled="disabled">Max</option>
        <option value="7">Mo</option>
        <option value="6">Paul</option>
        <option value="12">Sara</option>
        </select>
    </form>
    <span class="developer" style="display:inherit; padding:10px 0 10px 20px">
        normally hidden digital output:
        <input id="output_team_2" type="text" style="float:right">
    </span>
</div>

The javascript

// initialise multi, set headers for group 1
       var select = document.getElementById("team_1_select");
       multi(select, {
           non_selected_header: "Candidates",
           selected_header: "Team 1"
       });

// initialise multi, set headers for group 2
       var select = document.getElementById("team_2_select");
       multi(select, {
           non_selected_header: "Candidates",
           selected_header: "Team 2"
       });

function reportUpdatedValues(element,team){
// Return an array of the selected options in element
      var result = [];
      var options = element && element.options;
      var opt;
    
      for (var i=0, iLen=options.length; i<iLen; i++) {
        opt = options[i];
    
        if (opt.selected) {
          result.push(opt.value || opt.text);
        }
      }
      // for development purpose only we display the result in the team output
      document.getElementById('output_'+team).value = result;
      return result;
}

/*! multi.min.js version 03-12-2018 by Fabian Lindfors */
var multi=function(){var e=function(e,t,n){var a=e.options[t.target.getAttribute("multi-index")];if(!a.disabled){a.selected=!a.selected;var i,d,r,l=n.limit;if(l>-1){for(var s=0,o=0;o<e.options.length;o++)e.options[o].selected&&s++;if(s===l){this.disabled_limit=!0,"function"==typeof n.limit_reached&&n.limit_reached();for(o=0;o<e.options.length;o++){(c=e.options[o]).selected||c.setAttribute("disabled",!0)}}else if(this.disabled_limit){for(o=0;o<e.options.length;o++){var c;"false"===(c=e.options[o]).getAttribute("data-origin-disabled")&&c.removeAttribute("disabled")}this.disabled_limit=!1}}i="change",d=e,(r=document.createEvent("HTMLEvents")).initEvent(i,!1,!0),d.dispatchEvent(r)}},t=function(e,t){if(e.wrapper.selected.innerHTML="",e.wrapper.non_selected.innerHTML="",t.non_selected_header&&t.selected_header){var n=document.createElement("div"),a=document.createElement("div");n.className="header",a.className="header",n.innerText=t.non_selected_header,a.innerText=t.selected_header,e.wrapper.non_selected.appendChild(n),e.wrapper.selected.appendChild(a)}if(e.wrapper.search)var i=e.wrapper.search.value;for(var d=null,r=null,l=0;l<e.options.length;l++){var s=e.options[l],o=s.value,c=s.textContent||s.innerText,p=document.createElement("a");if(p.tabIndex=0,p.className="item",p.innerHTML=c,p.setAttribute("role","button"),p.setAttribute("data-value",o),p.setAttribute("multi-index",l),s.disabled&&(p.className+=" disabled"),s.selected){p.className+=" selected";var u=p.cloneNode(!0);e.wrapper.selected.appendChild(u)}if("OPTGROUP"==s.parentNode.nodeName&&s.parentNode!=r){if(r=s.parentNode,(d=document.createElement("div")).className="item-group",s.parentNode.label){var m=document.createElement("span");m.innerHTML=s.parentNode.label,m.className="group-label",d.appendChild(m)}e.wrapper.non_selected.appendChild(d)}s.parentNode==e&&(d=null,r=null),(!i||i&&c.toLowerCase().indexOf(i.toLowerCase())>-1)&&(null!=d?d.appendChild(p):e.wrapper.non_selected.appendChild(p))}};return function(n,a){if((a=void 0!==a?a:{}).enable_search=void 0===a.enable_search||a.enable_search,a.search_placeholder=void 0!==a.search_placeholder?a.search_placeholder:"Search...",a.non_selected_header=void 0!==a.non_selected_header?a.non_selected_header:null,a.selected_header=void 0!==a.selected_header?a.selected_header:null,a.limit=void 0!==a.limit?parseInt(a.limit):-1,isNaN(a.limit)&&(a.limit=-1),null==n.dataset.multijs&&"SELECT"==n.nodeName&&n.multiple){n.style.display="none",n.setAttribute("data-multijs",!0);var i=document.createElement("div");if(i.className="multi-wrapper",a.enable_search){var d=document.createElement("input");d.className="search-input",d.type="text",d.setAttribute("placeholder",a.search_placeholder),d.addEventListener("input",function(){t(n,a)}),i.appendChild(d),i.search=d}var r=document.createElement("div");r.className="non-selected-wrapper";var l=document.createElement("div");l.className="selected-wrapper",i.addEventListener("click",function(t){t.target.getAttribute("multi-index")&&e(n,t,a)}),i.addEventListener("keypress",function(t){var i=32===t.keyCode||13===t.keyCode;t.target.getAttribute("multi-index")&&i&&(t.preventDefault(),e(n,t,a))}),i.appendChild(r),i.appendChild(l),i.non_selected=r,i.selected=l,n.wrapper=i,n.parentNode.insertBefore(i,n.nextSibling);for(var s=0;s<n.options.length;s++){var o=n.options[s];o.setAttribute("data-origin-disabled",o.disabled)}t(n,a),n.addEventListener("change",function(){t(n,a)})}}}();"undefined"!=typeof jQuery&&function(e){e.fn.multi=function(t){return t=void 0!==t?t:{},this.each(function(){var n=e(this);multi(n.get(0),t)})}}(jQuery);

In the case example the names Alex and Max are on vacation and therefore PHP already made them generally unavailable for all teams. Now, if Diana (numerical member 8) gets selected for Team 1, the Diana option should become disabled or hidden for Team 2, Team 3, Team 4 and so on. The total number of teams can vary per case.

Daniel_Knights
  • 7,940
  • 4
  • 21
  • 49
2x2p
  • 414
  • 3
  • 17
  • I should add, the solution I am searching for must become quite scalable as in the future I will apply a selection mask. Let's say Sara never does night shifts, she can only be on Team 1, or in case Mo is selected for the late shift (Team 2) he cannot be on the next day early shift (Team 3) but again is available for (Team 4). But this gets solved once I understand how I can make elements or forms listen to each other through one central handler. – 2x2p May 05 '19 at 16:37
  • Just as a suggestion, it is not a great UX disabling the select input options. Why not handle this in your PHP back-end to retrieve available users only? – k3llydev May 05 '19 at 16:54
  • A great part of it could be done in PHP, but still the input options on UX level should be enabled/disabled client side before sending it to the server. E-mails might be sent to the members when added or removed from a Team. The planner must be able to fiddle with teams and communicate offline up to the point of submitting the total output. Else each change should be sent and subsequently all forms refreshed after server evaluation. Yes pausing e-mails until a double confirmation of teams can be a quick and dirty way. But having team selection done client side is more elegant. – 2x2p May 05 '19 at 17:22
  • What I mean was that a solution to this could be to return the available users from the back-end. Every time your database changes, because I am pretty sure you have one for this. (If not, you should) your PHP script would only return the available users. This with the objective of automate what is "automatable". Either way, your app will need to save the information somewhere if you want to have multiple users viewing the same data. – k3llydev May 05 '19 at 17:38
  • (A) I would like to show disabled members in the UX, as this way we can request the reason for unavailability from a database. (B) for refreshing the input list for remaining forms after server evaluation, I would still need a handler that tells the UX e.g. remove/disable option 8 from select for "team2" – 2x2p May 05 '19 at 18:38

2 Answers2

0

I have added the following old school js functions to deal with at least a part of the issue and for now I have removed the multi.js UX initialisation to simplify the case. So now members selected on Team 1 will become disabled for selection on Team 2 and vice versa without enabling those that were generally disabled at the beginning.

Yet the remaining question is still how the object when using multi.js can listen to changes of other objects. This is not yet solved.

Also this is not a central handler so it isn't deserving the first prize for a beautiful solution.

The added JavaScript:

resize_textarea(document.getElementById('team_1_select'));
resize_textarea(document.getElementById('team_2_select'));

function disableOptionsForElement(array, element) {
  // convert to Array if argument 1 is not array
  if (Array.isArray(array) == false) {
    array = array.split(",");
  }
  // loop through values in array
  for (var i = 0, iLen = array.length; i < iLen; i++) {
    // look for first option in element with current value
    var opt = getOptionByValue(element, array[i]);
    if (opt) {
      // only disable option on target if value exists
      opt.disabled = true;
      opt.selected = false;
    }
  }
}

function enableOptionsForElement(array, element) {
  // convert to Array if argument 1 is not array
  if (Array.isArray(array) == false) {
    array = array.split(",");
  }
  // loop through values in array
  for (var i = 0, iLen = array.length; i < iLen; i++) {
    // look for first option in element with current value
    var opt = getOptionByValue(element, array[i]);
    if (opt) {
      // only enable option on target if value exists
      opt.disabled = false;
    }
  }
}

function parseSelectedValues(element, target) {
  // Return an array of the selected options in element
  // target is optional DOMElement for output
  var result = [];
  var options = element && element.options;
  var opt;
  for (var i = 0, iLen = options.length; i < iLen; i++) {
    var opt = options[i];
    if (opt.selected) {
      result.push(opt.value || opt.text);
    }
  }
  if ((typeof target === "object") && (target !== null)) {
    target.value = result;
  }
  return result;
}

function parseEnabledValues(element) {
  // Return an array of the selected options in element
  var result = [];
  var options = element && element.options;
  var opt;
  for (var i = 0, iLen = options.length; i < iLen; i++) {
    var opt = options[i];
    if (opt.disabled == false) {
      result.push(opt.value || opt.text);
    }
  }
  return result;
}

function getOptionByValue(element, val) {
  // Return only a menu option element that has the requested value
  var options = element && element.options;
  var opt;
  for (var i = 0, iLen = options.length; i < iLen; i++) {
    opt = options[i];
    if (opt.value === val) {
      return opt;
    }
  }
  return false;
}

function resize_textarea(area) {
  //auto expand textarea to fit new number of lines
  area.style.height = (6 + area.scrollHeight) + "px";
}

The HTML changed accordingly

<div class="container">
  <h2>team selection demo</h2>
  <form><label for="team_1">Pick your team</label>
    <select class="multi-wrapper" id="team_1_select" multiple="multiple" name="team_1" onchange="this.form.output1.value = parseSelectedValues(this); enableOptionsForElement ( parseEnabledValues( this ) , this.form.team_2_select ); disableOptionsForElement( parseSelectedValues( this ) , this.form.team_2_select );">
      <option disabled="disabled" value="13">Alex</option>
      <option value="1">Bob</option>
      <option value="8">Diana</option>
      <option value="5">Frank</option>
      <option value="9">Fred</option>
      <option value="11">Helen</option>
      <option value="10">Jeanne</option>
      <option value="4">Linda</option>
      <option value="3">Mary</option>
      <option disabled="disabled" value="2">Max</option>
      <option value="7">Mo</option>
      <option value="6">Paul</option>
      <option value="12">Sara</option>
    </select> <span class="developer" style="display:inherit; padding:10px 0 10px 20px">normally hidden digital output: <input id="output1" readonly style="float:right" type="text" value=""></span>
    <!-- end of Team 1 -->
    <label for="team_2">Pick the other team</label>
    <select class="multi-wrapper" id="team_2_select" multiple="multiple" name="team_2" onchange="parseSelectedValues(this, this.form.output2); enableOptionsForElement ( parseEnabledValues( this ) , this.form.team_1_select ); disableOptionsForElement( parseSelectedValues( this ) , this.form.team_1_select );">
      <option disabled="disabled" value="13">Alex</option>
      <option value="1">Bob</option>
      <option value="8">Diana</option>
      <option value="5">Frank</option>
      <option value="9">Fred</option>
      <option value="11">Helen</option>
      <option value="10">Jeanne</option>
      <option value="4">Linda</option>
      <option value="3">Mary</option>
      <option disabled="disabled" value="2">Max</option>
      <option value="7">Mo</option>
      <option value="6">Paul</option>
      <option value="12">Sara</option>
    </select> <span class="developer" style="display:inherit; padding:10px 0 10px 20px">normally hidden digital output: <input id="output2" readonly style="float:right" type="text" value=""></span>
    <!-- end of Team 2 -->
  </form>
</div>
Malekai
  • 4,765
  • 5
  • 25
  • 60
2x2p
  • 414
  • 3
  • 17
0

Acceptable workaround found by fardhana on GitHub making multi.js refresh itself via dispatchEvent().

So I added a timer to make the customised UX refresh every 800 milliseconds. Now the select element can be taken over by multi.js and programmatically changed values in the element will appear to the user with an acceptable minimal delay.

// initialise multi, set headers for group 1 and add auto refresh timer
var team1 = document.getElementById('team_1_select');
multi(team1, {
  non_selected_header: "Candidates",
  selected_header: "Team A"
});
var timer1 = function() {
  team1.dispatchEvent(new Event('change'));
};
setInterval(timer1, 800);
// initialise multi, set headers for group 2 and add auto refresh timer
var team2 = document.getElementById('team_2_select');
multi(team2, {
  non_selected_header: "Candidates",
  selected_header: "Team B"
});
var timer2 = function() {
  team2.dispatchEvent(new Event('change'));
};
setInterval(timer2, 800);

NOTE: this solution is based on this issue.

Malekai
  • 4,765
  • 5
  • 25
  • 60
2x2p
  • 414
  • 3
  • 17
  • drawback I found later when the timed Event( 'change' ) is triggered, this will stop a mouse scrolling or a finger scroll movement on a tablet or phone :-/ – 2x2p May 08 '19 at 14:46