Search code examples
javascripthtml

Javascript: navigate table inputs with arrow keys


I am working on an HTML grade book for a client. I am generating the gradebook with PHP, and then outputting a HTML table as seen in the example below. Each <td> contains a div with an <input> for the teacher to type in the student's score.

Here's what I'm trying to accomplish: how can I make it so the teacher can use the arrow keys on the keyboard to navigate inside of the gradebook? IE: The teacher should be able to click a cell, type in a grade, and then hit the left/right/up/down arrow key to move to the appropriate input and type in the next grade.

I have seen numerous examples on here about how to use javascript to accomplish this task in highlighting different <td> cells, but I cannot figure out how I would go about allowing the teacher to navigate inputs with her arrow keys. Any advice would be much appreciated.

   body {
     margin: 0;
     position: absolute;
     top: 105px; left: 0px;
     width: 100%;
     height: calc(100vh - 105px);
     background-color: #FCFCFC;
     display: grid;
     grid-template-rows: 1fr;
     grid-template-areas:
       "master"}

   .master {
     grid-area: master;
     overflow-x: scroll;}

   table {border-collapse: collapse}

   th, td {
     background-color: white;
     max-width: 110px;
     border: 1px solid lightgray;}

   th {overflow: hidden;}

  thead{
    top: 0;
    position: sticky;
    z-index: 1;}

  tr td:nth-child(1),
  tr th:nth-child(1){
    position: sticky;
    left: 0;}

   thead th.navigator { /* Top left cell with navigation controls */
     padding: 10px;
     z-index: 3;}

   tr td:first-child, tr td:nth-child(2) { /* First two columns of each row */
     white-space: nowrap;
     max-width: fit-content !important;}

   td input {
     border: none;
     outline: none;
     text-align: center;
     max-width: 80%;
     font-size: 18px;
     padding: 6px 0px;
     cursor: cell;}

   th select {
     outline: none;
     -webkit-appearance: none;
     padding: 8px 12px;
     box-sizing: border-box;
     border-radius: 8px;
     width: 100%;
     border: 1px solid lightgray}

  tr:focus-within td:not(.gray) {background-color: #E9DCF9}
  tr:focus-within td:not(.gray) input {background-color: #E9DCF9}

  .due {
    font-size: 11px;
    color: darkgray;}

   .assign {padding: 20px}
   .assign span {
     cursor: pointer;
     font-size: 15px;
     overflow: hidden;
     color: #581F98}

   .avg {padding: 10px}

   .studentInfo {
     display: flex;
     align-items: center;
     margin: 10px 12px 10px 6px;}

   .studentInfo img {
     width: 25px;
     margin-right: 10px;
     clip-path: circle();}

   .red {background-color: red;}
   .gray, .gray input {background-color: #F2F2F2;}

  .score {
    display: flex;
    justify-content: center;
    align-items: center;}
        <table>
          <thead>
            <tr>
              <th class='navigator' colspan='2' rowspan='4'>
                <form method='GET'>
                  <select name='subID' onchange='this.form.submit()'>
                    <option value='1' >Reading</option>
                    <option value='2' >Social Studies</option>
                  </select>
                  <select name='week' onchange='this.form.submit()' disabled>
                    <option value='all'>Entire Quarter</option>
                  </select>
                </form>
              </th>
              <tr>
                <th class='due'><span title='Monday'>10/11</span> to <span title='Wednesday'>10/13</span></th>
                <th class='due'><span title='Wednesday'>10/20</span> to <span title='Friday'>10/22</span></th>
                <th class='due'><span title='Monday'>10/18</span> to <span title='Friday'>10/22</span></th>
                <th class='due'><span title='Wednesday'>10/20</span> to <span title='Friday'>10/22</span></th>
              </tr>
              <tr>
                <th class='assign'>
                  <span title='Assignment ID: 130' onclick='assignInfo("130");'>📚 Quiz</span>
                </th>
                <th class='assign'>
                  <span title='Assignment ID: 146' onclick='assignInfo("146");'>📚 Homework</span>
                </th>
                <th class='assign'>
                  <span title='Assignment ID: 145' onclick='assignInfo("145");'>💻 Test</span>
                </th>
                <th class='assign'>
                  <span title='Assignment ID: 147' onclick='assignInfo("147");'>✏️ Project</span>
                </th>
            </tr>
            <tr>
              <th class='avg gray'><span title='9.111/10'>91%</span></th>
              <th class='avg gray'><span title='8.672/10'>87%</span></th>
              <th class='avg gray'><span title='4.348/5'>87%</span></th>
              <th class='avg gray'><span title='8.007/10'>80%</span></th>
            </tr>
          </thead>
          <tr>
            <td>
              <div class='studentInfo'>
                <span title='Student ID: 11'><img src='../../resources/pics/students/11.jpg'></span>
                <span>John Doe</span>
              </div>
            </td>
            <td class='avg gray'>
              <span data-studentAvg='11' title='97.5/110'>89%</span>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='11' data-workID='7280' data-curScore='10' value='10'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='131' data-usid='11' data-workID='7282' data-curScore='9' value='9'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='132' data-usid='11' data-workID='7340' data-curScore='10' value='10'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='11' data-workID='7280' data-curScore='10' value='10'>
              </div>
            </td>
          </tr>
          <tr>
            <td>
              <div class='studentInfo'>
                <span title='Student ID: 12'><img src='../../resources/pics/students/12.jpg'></span>
                <span>Jane Doe</span>
              </div>
            </td>
            <td class='avg gray'>
              <span data-studentAvg='12' title='97.5/110'>69%</span>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='12' data-workID='7250' data-curScore='6' value='6'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='131' data-usid='12' data-workID='7211' data-curScore='9' value='9'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='132' data-usid='12' data-workID='7110' data-curScore='4' value='4'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='12' data-workID='7233' data-curScore='10' value='10'>
              </div>
            </td>
          </tr>
          <tr>
            <td>
              <div class='studentInfo'>
                <span title='Student ID: 13'><img src='../../resources/pics/students/13.jpg'></span>
                <span>Sally Martin</span>
              </div>
            </td>
            <td class='avg gray'>
              <span data-studentAvg='13' title='97.5/110'>100%</span>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='13' data-workID='6250' data-curScore='10' value='10'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='131' data-usid='13' data-workID='6211' data-curScore='10' value='10'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='132' data-usid='13' data-workID='7610' data-curScore='10' value='10'>
              </div>
            </td>
            <td>
              <div class='score'>
                <input type='text' data-assID='130' data-usid='13' data-workID='7933' data-curScore='10' value='10'>
              </div>
            </td>
          </tr>
        </table>


Solution

  • It's not perfect but it should give you a place to start. You'll have to add some error handling and handle edge cases.

    document.addEventListener( 'keydown', ( event ) => {
    
      const currentInput = document.activeElement;
      const currentTd = currentInput.parentNode.parentNode;
      const currentTr = currentTd.parentNode;
      const index = Array.from(currentTr.children).indexOf(currentTd);
    
      switch (event.key) {
        case "ArrowLeft":
            // Left pressed
            currentTd.previousElementSibling.getElementsByTagName('input')[0].focus();
            break;
        case "ArrowRight":
            // Right pressed
            currentTd.nextElementSibling.getElementsByTagName('input')[0].focus();
            break;
        case "ArrowUp":
            // Up pressed
            Array.from( currentTr.previousElementSibling.children )[index].getElementsByTagName('input')[0].focus();
            break;
        case "ArrowDown":
            // Down pressed
            Array.from( currentTr.nextElementSibling.children )[index].getElementsByTagName('input')[0].focus();
            break;
      }
    } )
    body {
         margin: 0;
         position: absolute;
         top: 105px; left: 0px;
         width: 100%;
         height: calc(100vh - 105px);
         background-color: #FCFCFC;
         display: grid;
         grid-template-rows: 1fr;
         grid-template-areas:
           "master"}
    
       .master {
         grid-area: master;
         overflow-x: scroll;}
    
       table {border-collapse: collapse}
    
       th, td {
         background-color: white;
         max-width: 110px;
         border: 1px solid lightgray;}
    
       th {overflow: hidden;}
    
      thead{
        top: 0;
        position: sticky;
        z-index: 1;}
    
      tr td:nth-child(1),
      tr th:nth-child(1){
        position: sticky;
        left: 0;}
    
       thead th.navigator { /* Top left cell with navigation controls */
         padding: 10px;
         z-index: 3;}
    
       tr td:first-child, tr td:nth-child(2) { /* First two columns of each row */
         white-space: nowrap;
         max-width: fit-content !important;}
    
       td input {
         border: none;
         outline: none;
         text-align: center;
         max-width: 80%;
         font-size: 18px;
         padding: 6px 0px;
         cursor: cell;}
    
       th select {
         outline: none;
         -webkit-appearance: none;
         padding: 8px 12px;
         box-sizing: border-box;
         border-radius: 8px;
         width: 100%;
         border: 1px solid lightgray}
    
      tr:focus-within td:not(.gray) {background-color: #E9DCF9}
      tr:focus-within td:not(.gray) input {background-color: #E9DCF9}
    
      .due {
        font-size: 11px;
        color: darkgray;}
    
       .assign {padding: 20px}
       .assign span {
         cursor: pointer;
         font-size: 15px;
         overflow: hidden;
         color: #581F98}
    
       .avg {padding: 10px}
    
       .studentInfo {
         display: flex;
         align-items: center;
         margin: 10px 12px 10px 6px;}
    
       .studentInfo img {
         width: 25px;
         margin-right: 10px;
         clip-path: circle();}
    
       .red {background-color: red;}
       .gray, .gray input {background-color: #F2F2F2;}
    
      .score {
        display: flex;
        justify-content: center;
        align-items: center;}
    <table>
              <thead>
                <tr>
                  <th class='navigator' colspan='2' rowspan='4'>
                    <form method='GET'>
                      <select name='subID' onchange='this.form.submit()'>
                        <option value='1' >Reading</option>
                        <option value='2' >Social Studies</option>
                      </select>
                      <select name='week' onchange='this.form.submit()' disabled>
                        <option value='all'>Entire Quarter</option>
                      </select>
                    </form>
                  </th>
                  <tr>
                    <th class='due'><span title='Monday'>10/11</span> to <span title='Wednesday'>10/13</span></th>
                    <th class='due'><span title='Wednesday'>10/20</span> to <span title='Friday'>10/22</span></th>
                    <th class='due'><span title='Monday'>10/18</span> to <span title='Friday'>10/22</span></th>
                    <th class='due'><span title='Wednesday'>10/20</span> to <span title='Friday'>10/22</span></th>
                  </tr>
                  <tr>
                    <th class='assign'>
                      <span title='Assignment ID: 130' onclick='assignInfo("130");'>📚 Quiz</span>
                    </th>
                    <th class='assign'>
                      <span title='Assignment ID: 146' onclick='assignInfo("146");'>📚 Homework</span>
                    </th>
                    <th class='assign'>
                      <span title='Assignment ID: 145' onclick='assignInfo("145");'>💻 Test</span>
                    </th>
                    <th class='assign'>
                      <span title='Assignment ID: 147' onclick='assignInfo("147");'>✏️ Project</span>
                    </th>
                </tr>
                <tr>
                  <th class='avg gray'><span title='9.111/10'>91%</span></th>
                  <th class='avg gray'><span title='8.672/10'>87%</span></th>
                  <th class='avg gray'><span title='4.348/5'>87%</span></th>
                  <th class='avg gray'><span title='8.007/10'>80%</span></th>
                </tr>
              </thead>
              <tr>
                <td>
                  <div class='studentInfo'>
                    <span title='Student ID: 11'><img src='../../resources/pics/students/11.jpg'></span>
                    <span>John Doe</span>
                  </div>
                </td>
                <td class='avg gray'>
                  <span data-studentAvg='11' title='97.5/110'>89%</span>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='11' data-workID='7280' data-curScore='10' value='10'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='131' data-usid='11' data-workID='7282' data-curScore='9' value='9'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='132' data-usid='11' data-workID='7340' data-curScore='10' value='10'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='11' data-workID='7280' data-curScore='10' value='10'>
                  </div>
                </td>
              </tr>
              <tr>
                <td>
                  <div class='studentInfo'>
                    <span title='Student ID: 12'><img src='../../resources/pics/students/12.jpg'></span>
                    <span>Jane Doe</span>
                  </div>
                </td>
                <td class='avg gray'>
                  <span data-studentAvg='12' title='97.5/110'>69%</span>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='12' data-workID='7250' data-curScore='6' value='6'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='131' data-usid='12' data-workID='7211' data-curScore='9' value='9'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='132' data-usid='12' data-workID='7110' data-curScore='4' value='4'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='12' data-workID='7233' data-curScore='10' value='10'>
                  </div>
                </td>
              </tr>
              <tr>
                <td>
                  <div class='studentInfo'>
                    <span title='Student ID: 13'><img src='../../resources/pics/students/13.jpg'></span>
                    <span>Sally Martin</span>
                  </div>
                </td>
                <td class='avg gray'>
                  <span data-studentAvg='13' title='97.5/110'>100%</span>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='13' data-workID='6250' data-curScore='10' value='10'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='131' data-usid='13' data-workID='6211' data-curScore='10' value='10'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='132' data-usid='13' data-workID='7610' data-curScore='10' value='10'>
                  </div>
                </td>
                <td>
                  <div class='score'>
                    <input type='text' data-assID='130' data-usid='13' data-workID='7933' data-curScore='10' value='10'>
                  </div>
                </td>
              </tr>
            </table>