【问题标题】:Enforce Unique Selection of Select elements强制选择元素的唯一选择
【发布时间】:2021-04-19 21:00:18
【问题描述】:

我正在尝试创建一个站点,该站点在包含最多 2 个输入的表中具有任意数量的行。我希望输入的组合是唯一的。也就是说,如果我们有以下元素

   <select><option value='0'>None</option><option value='1'>1</option><option value='2'>2</option><option value='3'>3</option></select>
   <select><option value='0'>None</option><option value='4'>4</option><option value='5'>5</option><option value='6'>6</option></select>

那么两者的任意组合都可以使用0-1次。所以,如果 1 和 4 在一行中使用,如果再次选择 1,则不应允许选择 4。这也应该在另一个方向上起作用。如果选择了 4,则不应允许再次选择 1。

用户可以随时添加或删除行,因此每次更改元素时都必须执行这些检查。

其他信息

  • 选择元素的内容是由数据库生成的,它们不是静态的。
  • 它们将始终具有包含整数的“值”属性。
  • 我在 'masters' div 中保留一份未更改的选择元素的副本,这些元素用于向表中添加更多行(以及 PHP 从数据库中转储选项的位置)。当点击“添加”按钮时,这些“母版”会被克隆并添加进去。
  • 每个允许的值为0,但只允许一次。这是唯一保证会出现在两个选择元素中的选项

我尝试过的事情

  1. 检测更改事件并尝试调整所有选择元素以强制执行唯一性,但我无法完全破解这一点。我很确定这是要走的路,但我正在努力实现它。

  2. 尝试在添加行时删除可能的重复项,但在至少做出一个选择之前,您无法确定什么是重复项。

  3. 删除与另一行组合重复的任何行,但这不能按预期工作,并且无论如何都不是用户友好的(这是下面示例中的当前实现)。

不起作用的东西

额外挑战

另一半是如果数据库中没有项目/活动,项目列和/或活动列可能会被隐藏。在这种情况下,默认值为 0,但它极大地改变了页面的预期行为并添加了一堆边缘情况。我把它排除在主要问题之外,因为我认为它会增加太多混乱,但这是我需要处理的其他事情。但是,此功能仅次于上述主要问题。

最小可重现示例

您应该能够将其直接复制并粘贴到 .html 文件中进行实验。我留下了一些样式,以便更容易理解需要什么。

jQuery.fn.outerHTML = function() {
  return jQuery('<div />').append(this.eq(0).clone()).html();
};

var projects = $("#projectsMaster");
var activities = $("#activitiesMaster");
var numRows = 0;

$(document).ready(function() {
  $(".entryTable").on("click", ".deleteButton", function() {
    if (numRows > 0) {
      var row = $(this).closest("tr");
      row.remove();
      numRows--;
    }
  });
  
  $(".entryTable").on('change', 'select', function() {
    enforceUniqueProjectsActivities(this)
  });
});

function enforceUniqueProjectsActivities(select) { 
  var usedCombos = new Array(); 
  
  $(".entryTable").find(".dataRow").each(function() {
    var activity = $(this).find(".activity").find("select").val();
    if (!activity) {
      activity = 0;
    }
    if (!project) {
      project = 0;
    }
    var project = $(this).find(".project").find("select").val();
    usedCombos.push(project + "," + activity);
  });
  
  //I was planning on going through each row and removing duplicates, but this isnt very user-friendly.
  $(".entryTable").find(".dataRow").each(function() {
    var activity = $(this).find(".activity").find("select").val();
    if (!activity) {
      activity = 0;
    }
    var project = $(this).find(".project").find("select").val();
    if (!project) {
      project = 0;
    }

    for (var i = 0; i < usedCombos.length; i++) {
      if (i === $(this).index() - 1) {
        continue;
      }
      if (project === usedCombos[i].split(",")[0] && activity === usedCombos[i].split(",")[1]) {
        $(this).remove();
      }
    }
  });
}

function addRow() {
  numRows++;
  var newRow = "<tr class='dataRow' '" +
    "'><td class='project'>" + projects.clone().outerHTML() + "</td><td class='activity'>" + activities.clone().outerHTML() +
    "</td><td><button class='deleteButton'>Delete</button></td></tr>";
  $(".entryTable").find('tr:last').prev().after(newRow);
}
.entryTable {
  margin: auto;
  width: 95%;
  border: 1px solid black;
  border-collapse: collapse;
}

.entryTable input,
select {
  width: 100%;
  font-size: 18px;
}

.entryTable tr {
  border-bottom: 1px solid black;
  border-color: black
}

.entryTable td {
  text-align: center;
  padding: 10px;
  border-right: 1px solid black;
}

#addRow {
  background-color: darkgray
}

.masters {
  display: none;
}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<div class="masters">
  <select id="projectsMaster">
    <option value='0'>None</option>
    <option value='1'>1</option>
    <option value='2'>2</option>
    <option value='3'>3</option>
  </select>
  <select id="activitiesMaster">
    <option value='0'>None</option>
    <option value='4'>4</option>
    <option value='5'>5</option>
    <option value='6'>6</option>
  </select>
</div>

<br/>
<table class="entryTable" id-">
  <tr>
    <th>Project</th>
    <th>Activity</th>
    <th></th>
  </tr>
  <tr id="addRow">
    <td></td>
    <td></td>
    <td style='text-align: center'><button onclick="addRow()">Add</button></td>
  </tr>
</table>

【问题讨论】:

    标签: javascript html jquery


    【解决方案1】:

    实现此目的的一种方法是构建一个连接在一起的选定值对的数组。然后选择新的值时,您可以将其与现有数组进行比较,以查看它是否已存在,并适当地处理。

    请注意,我还对构建 HTML 的逻辑进行了一些修改,以使其更加一致和简洁。

    我还删除了默认“请选择”选项中的 0 值,因为这会在与其他值进行比较时导致不必要的复杂化。

    jQuery($ => {
      $('button.add').on('click', () => {
        $('.entryTable tfoot tr').clone().insertBefore('.entryTable tbody tr:last');
      });
    
      $(".entryTable").on("click", ".deleteButton", e => {
        $(e.target).closest("tr").remove();
      });
      
      let getSelections = () => {
        return $('tbody .dataRow').map((i, row) => {
          let $row = $(row);
          return `${$row.find('.projectsMaster').val()}${$row.find('.activitiesMaster').val()}`;
        }).get();
      }
    
      $(".entryTable").on('change', 'select', e => {  
        let $row = $(e.target).closest('tr');
        let item = `${$row.find('.projectsMaster').val()}${$row.find('.activitiesMaster').val()}`;
            
        if (item.length == 2 && getSelections().filter(i => i == item).length > 1) {
          e.preventDefault();
          console.log('Already selected!');
        }
      });
    });
    table>tfoot {
      display: none;
    }
    
    .entryTable {
      margin: auto;
      width: 95%;
      border: 1px solid black;
      border-collapse: collapse;
    }
    
    .entryTable input,
    select {
      width: 100%;
      font-size: 18px;
    }
    
    .entryTable tr {
      border-bottom: 1px solid black;
      border-color: black
    }
    
    .entryTable td {
      text-align: center;
      padding: 10px;
      border-right: 1px solid black;
    }
    
    .addRow {
      background-color: darkgray
    }
    
    .masters {
      display: none;
    }
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    
    <table class="entryTable">
      <tbody>
        <tr>
          <th>Project</th>
          <th>Activity</th>
          <th></th>
        </tr>
        <tr class="addRow">
          <td></td>
          <td></td>
          <td style='text-align: center'><button class="add">Add</button></td>
        </tr>
      </tbody>
      <tfoot>
        <tr class="dataRow">
          <td class="project">
            <select class="projectsMaster">
              <option value="">Select</option>
              <option value="1">1</option>
              <option value="2">2</option>
              <option value="3">3</option>
            </select>
          </td>
          <td class="activity">
            <select class="activitiesMaster">
              <option value="">Select</option>
              <option value="4">4</option>
              <option value="5">5</option>
              <option value="6">6</option>
            </select>
          </td>
          <td><button class="deleteButton">Delete</button></td>
        </tr>
      </tfoot>
    </table>

    【讨论】:

    • 虽然我觉得其他答案更适合我的特定用例,但我真的很喜欢您构建代码并使其更简洁的方式。我也会拿一些你的,因为它比我的干净得多。 +1 向我展示了处理这些事情的更好方法,谢谢!
    • 没问题,很高兴你从中得到了一些东西。要带走的要点是全局变量的使用,即。 numRows,是不好的做法,应该避免。
    【解决方案2】:

    找到了您正在寻找的确切内容...在用户看到选项之前,我们的函数正在检查其他值和具有相同值的其他行,然后禁用不再可用的选项。 我更改了重复检查功能和“更改”侦听器(我将其更改为“focusin”侦听器)。

    jQuery.fn.outerHTML = function() {
      return jQuery('<div />').append(this.eq(0).clone()).html();
    };
    
    var projects = $("#projectsMaster");
    var activities = $("#activitiesMaster");
    var numRows = 0;
    
    $(document).ready(function() {
      $(".entryTable").on("click", ".deleteButton", function() {
        if (numRows > 0) {
          var row = $(this).closest("tr");
          row.remove();
          numRows--;
        }
      });
      
      // run the checking before user sees the options
      $(".entryTable").on('focusin', 'select', function() {
        // we must run that function giving it the other select because i wrote it backwards ;) sorry
        var $this = $(this),
            $other = $this.closest('tr').find('select').not($(this));
        enforceUniqueProjectsActivities($other);
      });
    });
    
    function enforceUniqueProjectsActivities(select, isSecondRun) {
      var $this = $(select),
          thisVal = $this.val(),
          thisCol = $this.closest('td').is('.activity') ? 'activity':'project',
          pairCol = (thisCol == 'project') ? 'activity':'project',
          $table = $this.closest('.entryTable'),
          $thisRow = $this.closest('tr'),
          $pair = $thisRow.find('td.'+pairCol+' select'),
          $otherRows = $table.find('tr').not($thisRow),
          $otherLikeThis = $otherRows.find('.'+ thisCol+' option:selected[value="'+thisVal+'"]');
     
      // clear all disabled props of the options of the $pair element
      $pair.find('option').prop('disabled',false);
      
      if(thisVal==''||thisVal=='0')return;
    
      // check which pair-values are already used and disable these options
      $otherLikeThis.each(function(){
        var $currPair = $(this).closest('tr').find('td.'+pairCol+' select'),
            currPairVal = $currPair.val();
        if(currPairVal && currPairVal!='0'){
          $pair.find('option[value="'+ currPairVal +'"]').prop('disabled',true);
        }
      });
      
    }
    
    function addRow() {
      numRows++;
      var newRow = "<tr class='dataRow' '" +
        "'><td class='project'>" + projects.clone().outerHTML() + "</td><td class='activity'>" + activities.clone().outerHTML() +
        "</td><td><button class='deleteButton'>Delete</button></td></tr>";
      $(".entryTable").find('tr:last').prev().after(newRow);
    }
    .entryTable {
      margin: auto;
      width: 95%;
      border: 1px solid black;
      border-collapse: collapse;
    }
    
    .entryTable input,
    select {
      width: 100%;
      font-size: 18px;
    }
    
    .entryTable tr {
      border-bottom: 1px solid black;
      border-color: black
    }
    
    .entryTable td {
      text-align: center;
      padding: 10px;
      border-right: 1px solid black;
    }
    
    #addRow {
      background-color: darkgray
    }
    
    .masters {
      display: none;
    }
    option:disabled {
      background-color:#ccc;
      color:#888;
    }
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <div class="masters">
      <select id="projectsMaster">
        <option value='0'>Select</option>
        <option value='1'>1</option>
        <option value='2'>2</option>
        <option value='3'>3</option>
      </select>
      <select id="activitiesMaster">
        <option value='0'>Select</option>
        <option value='4'>4</option>
        <option value='5'>5</option>
        <option value='6'>6</option>
      </select>
    </div>
    
    <br/>
    <table class="entryTable" id-">
      <tr>
        <th>Project</th>
        <th>Activity</th>
        <th></th>
      </tr>
      <tr id="addRow">
        <td></td>
        <td></td>
        <td style='text-align: center'><button onclick="addRow()">Add</button></td>
      </tr>
    </table>

    PS。我从一个不同的想法开始......这个想法是错误的 - 但函数很好(只需要另一个选择作为参数 - 这就是监听器函数变大的原因)

    【讨论】:

    • 这正是我要找的!给我一点在现场实施它,我会带着你接受的答案回来!
    • @webdev-dan 我能够实现它。有一种方法可以让我两次选择相同的值,但通过一系列交互,我并不太担心。我还必须删除几行以强制 0 值只能选择一次,就像所有其余值一样。我看到的唯一另一个问题是,当添加行时,默认值可以使用两次,但我认为如果我添加一个空字符串的值作为第一个可能修复它的元素,但是需要考虑一些数据库复杂性为此。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-30
    • 1970-01-01
    • 2015-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多