19

I've created a very basic spreadsheet using an HTML table. It works perfectly, except the user must use the mouse to click on every <td> in order to edit it. I'm capturing the click event with jQuery and displaying a dialog to edit it. I would like the user to be able to use the arrow keys to navigate to each cell, with the cell css background changing to indicate focus, and clicking the Enter key would trigger the jQuery dialog event. I'm using jQuery 1.9.

Here is a jsfiddle of basically what I have.

How do you save the currently selected cell, so that when you click on a cell with the mouse, and then use the arrow keys, it will navigate from the 'current' cell?

Thanks.

Lane
  • 2,669
  • 3
  • 25
  • 38
  • 1
    provide a jsfiddle of what you have now and I'll do the code for it. – JF it Apr 02 '14 at 16:25
  • possible duplicate of [How can I make the up and down arrow keys navigate a table's rows, and programmatically apply the sudo :hover to those rows?](http://stackoverflow.com/questions/14927751/how-can-i-make-the-up-and-down-arrow-keys-navigate-a-tables-rows-and-programma) – Diodeus - James MacFarlane Apr 02 '14 at 16:26
  • 1
    What HTML, CSS, and JavaScript code have you got now? It sounds like you should use `contenteditable=true` on the cells, but are you using some completely different approach? – Jukka K. Korpela Apr 02 '14 at 16:27
  • @Diodeus: That post is similar, but only traverses rows, not cells. And only the keyboard can be used for navigation, not the mouse as well. – Lane Apr 02 '14 at 16:34
  • The principles are the same. You need a keyboard handler and a method to store the cursor position. There are plenty of examples if you look for them. http://stackoverflow.com/search?q=javascript+arrow+keys+html+table – Diodeus - James MacFarlane Apr 02 '14 at 16:36
  • @Diodeus: I've searched the examples and I think I found one that is similar. Please reopen my question so I have the opportunity to answer it myself, so that others searching for this will not have to look as hard. Thanks. – Lane Apr 02 '14 at 17:15

4 Answers4

20

Below is a vanilla JavaScript solution using the onkeydown event and using the previousElementSibling and nextElementSibling properties.

https://jsfiddle.net/rh5aoxsL/

The problem with using tabindex is that you dont get to navigate the way you would in Excel and you can navigate away from the spreadsheet itself.

The HTML

<table>
  <tbody>
    <tr>
      <td id='start'>1</td>
      <td>2</td>
      <td>3</td>
      <td>4</td>
    </tr>
    <tr>
      <td>5</td>
      <td>6</td>
      <td>7</td>
      <td>8</td>
    </tr>
    <tr>
      <td>9</td>
      <td>10</td>
      <td>11</td>
      <td>12</td>
    </tr>
    <tr>
      <td>13</td>
      <td>14</td>
      <td>15</td>
      <td>16</td>
    </tr>
  </tbody>
</table>

The CSS

table {
  border-collapse: collapse;
  border: 1px solid black;
}
table td {
  border: 1px solid black;
  padding: 10px;
  text-align: center;
}

The JavaScript

var start = document.getElementById('start');
start.focus();
start.style.backgroundColor = 'green';
start.style.color = 'white';

function dotheneedful(sibling) {
  if (sibling != null) {
    start.focus();
    start.style.backgroundColor = '';
    start.style.color = '';
    sibling.focus();
    sibling.style.backgroundColor = 'green';
    sibling.style.color = 'white';
    start = sibling;
  }
}

document.onkeydown = checkKey;

function checkKey(e) {
  e = e || window.event;
  if (e.keyCode == '38') {
    // up arrow
    var idx = start.cellIndex;
    var nextrow = start.parentElement.previousElementSibling;
    if (nextrow != null) {
      var sibling = nextrow.cells[idx];
      dotheneedful(sibling);
    }
  } else if (e.keyCode == '40') {
    // down arrow
    var idx = start.cellIndex;
    var nextrow = start.parentElement.nextElementSibling;
    if (nextrow != null) {
      var sibling = nextrow.cells[idx];
      dotheneedful(sibling);
    }
  } else if (e.keyCode == '37') {
    // left arrow
    var sibling = start.previousElementSibling;
    dotheneedful(sibling);
  } else if (e.keyCode == '39') {
    // right arrow
    var sibling = start.nextElementSibling;
    dotheneedful(sibling);
  }
}
huehuehue
  • 201
  • 2
  • 2
  • 2
    I made a jquery version of this and extended it to be able to navigate across tables, whilst applying a class to the "focussed" element - I'm disappointed though that I have to use `$(document).keydown(function(e) {...` and that I cannot use something more targeted like`$(document).on("keydown", "td", function(e) {...` - does anyone know if this is just because `tabindex` is not applied to ``'s - I don't want to apply `tabindex` to `'s` because tables are added and removed dynamically. Is there a way to call `on()` more specifically? [jsFiddle](https://jsfiddle.net/rwone/opjxb00o) – user1063287 Aug 28 '17 at 02:57
  • It also has: click to focus cell, when at beginning or end of row, left or right arrows move to previous or next rows, if they exist, otherwise they move to previous or next table if it exists, and a handler for Enter keypress event. – user1063287 Aug 28 '17 at 03:17
  • Can you edit your fiddle and add the logic for when the page contains multiple tables. Your current solution is only working best on a single table. e.g. when we click the particular table after that the navigation will start for that table. – Subhojit Mondal Jul 31 '19 at 09:18
  • can we apply same code for angular material table also – Eswar Sep 02 '20 at 04:45
11

I figured it out, based on information I found on a few other posts. I rolled it all together, and the results are perfect.

Note: You have to put a tabindex attribute on every <td> to allow navigation.

Here's the jsfiddle. The same code is broken out below.

The HTML:

<table>
    <thead>
        <tr>
            <th>Col 1</th>
            <th>Col 2</th>
            <th>Col 3</th>
            <th>Col 4</th>
            <th>Col 5</th>
            <th>Col 6</th>
            <th>Col 7</th>
            <th>Col 8</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td tabindex="1">1</td>
            <td tabindex="2">2</td>
            <td tabindex="3">3</td>
            <td tabindex="4">4</td>
            <td tabindex="5">5</td>
            <td tabindex="6">6</td>
            <td tabindex="7">7</td>
            <td tabindex="8">8</td>
        </tr>
        <tr>
            <td tabindex="10">10</td>
            <td tabindex="11">11</td>
            <td tabindex="12">12</td>
            <td tabindex="13">13</td>
            <td tabindex="14">14</td>
            <td tabindex="15">15</td>
            <td tabindex="16">16</td>
            <td tabindex="17">17</td>
        </tr>
    </tbody>
</table>

<div id="edit">
    <form>
        <input type="text" id="text" value="To edit..." />
        <input type="submit" value="Save" />
    </form>
</div>

The CSS:

* {
    font-size: 12px;
    font-family: 'Helvetica', Arial, Sans-Serif;
    box-sizing: border-box;
}

table, th, td {
    border-collapse:collapse;
    border: solid 1px #ccc;
    padding: 10px 20px;
    text-align: center;
}

th {
    background: #0f4871;
    color: #fff;
}

tr:nth-child(2n) {
    background: #f1f1f1;
}
td:hover {
    color: #fff;
    background: #CA293E;
}
td:focus {
    background: #f44;
}

.editing {
    border: 2px dotted #c9c9c9;
}

#edit { 
    display: none;
}

The jQuery:

var currCell = $('td').first();
var editing = false;

// User clicks on a cell
$('td').click(function() {
    currCell = $(this);
    edit();
});

// Show edit box
function edit() {
    editing = true;
    currCell.toggleClass("editing");
    $('#edit').show();
    $('#edit #text').val(currCell.html());
    $('#edit #text').select();
}

// User saves edits
$('#edit form').submit(function(e) {
    editing = false;
    e.preventDefault();
    // Ajax to update value in database
    $.get('#', '', function() {
        $('#edit').hide();
        currCell.toggleClass("editing");
        currCell.html($('#edit #text').val());
        currCell.focus();
    });
});

// User navigates table using keyboard
$('table').keydown(function (e) {
    var c = "";
    if (e.which == 39) {
        // Right Arrow
        c = currCell.next();
    } else if (e.which == 37) { 
        // Left Arrow
        c = currCell.prev();
    } else if (e.which == 38) { 
        // Up Arrow
        c = currCell.closest('tr').prev().find('td:eq(' + 
          currCell.index() + ')');
    } else if (e.which == 40) { 
        // Down Arrow
        c = currCell.closest('tr').next().find('td:eq(' + 
          currCell.index() + ')');
    } else if (!editing && (e.which == 13 || e.which == 32)) { 
        // Enter or Spacebar - edit cell
        e.preventDefault();
        edit();
    } else if (!editing && (e.which == 9 && !e.shiftKey)) { 
        // Tab
        e.preventDefault();
        c = currCell.next();
    } else if (!editing && (e.which == 9 && e.shiftKey)) { 
        // Shift + Tab
        e.preventDefault();
        c = currCell.prev();
    } 

    // If we didn't hit a boundary, update the current cell
    if (c.length > 0) {
        currCell = c;
        currCell.focus();
    }
});

// User can cancel edit by pressing escape
$('#edit').keydown(function (e) {
    if (editing && e.which == 27) { 
        editing = false;
        $('#edit').hide();
        currCell.toggleClass("editing");
        currCell.focus();
    }
});
Lane
  • 2,669
  • 3
  • 25
  • 38
  • I am trying to apply this to to a backbone view. The navigation does not seem to work at all. I have to hit tab to bring my focus to the row of the table and then pressing the arrow keys has no effect whats so ever. What are some changes that I will have to do in order to make this work with backbone? – aliirz Sep 15 '14 at 21:33
  • @aliirz, I'm not sure. I've looked at Backbone but I have never used it. If you figure out what needs to be changed, post another answer to this question; I would like to see your work. – Lane Sep 17 '14 at 17:11
0

This code will properly helps you to navigate the table using arrow keys. In every cell there are textboxes if you want to edit press f2 and edit the cell.

$(document).ready(function()
  {
   var tr,td,cell;
   td=$("td").length;
   tr=$("tr").length;
   cell=td/(tr-1);//one tr have that much of td
   //alert(cell);
   $("td").keydown(function(e)
   {
    switch(e.keyCode)
    {
    
     case 37 : var first_cell = $(this).index();
         if(first_cell==0)
         {
        $(this).parent().prev().children("td:last-child").focus();
         }
         else
        $(this).prev("td").focus();break;//left arrow
     case 39 : var last_cell=$(this).index();
         if(last_cell==cell-1)
         {
        $(this).parent().next().children("td").eq(0).focus();
         }
         $(this).next("td").focus();break;//right arrow
     case 40 : var child_cell = $(this).index(); 
         $(this).parent().next().children("td").eq(child_cell).focus();break;//down arrow
     case 38 : var parent_cell = $(this).index();
         $(this).parent().prev().children("td").eq(parent_cell).focus();break;//up arrow
    }
    if(e.keyCode==113)
    {
     $(this).children().focus();
    }
   });
   $("td").focusin(function()
   {
    $(this).css("outline","solid steelblue 3px");//animate({'borderWidth': '3px','borderColor': '#f37736'},100);
   });
   $("td").focusout(function()
   {
    $(this).css("outline","none");//.animate({'borderWidth': '1px','borderColor': 'none'},500);
   });
   
  });
input
  {
   width:100%;
   border:none; 
  }
<html>
 <head>
 <title>Web Grid Using Arrow Key</title>
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  </head>
<body>
  <h1>Web Grid Table</h1>
  <div id="abc" class="table_here" role="grid">
   <table class="table" border="1" style="width:50%; padding:15px;">
    <tr>
     <th>Name</th>
     <th>Email</th>
     <th>Mobile</th>
     <th>Address</th>
    </tr>
    <tr role="row">
     <td role="gridcell" tabindex="0" aria-label="name" aria-describedby="f2_key">
      <input type="text" class="link" tabindex="-1" name="name" aria-label="name">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Email Id" aria-describedby="f2_key">
      <input type="text" class="link" tabindex="-1" name="email" aria-label="email">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Mobile Number" aria-describedby="f2_key">
      <input type="text" class="link" tabindex="-1" name="mob" aria-label="mobile">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Address" aria-describedby="f2_key">
      <input type="text" class="link" tabindex="-1" name="add" aria-label="address">
     </td>
     <p id="f2_key" style="display:none;" aria-hidden="true">Press F2 Key To Edit cell</p>
    </tr>
    <tr role="row">
     <td role="gridcell" tabindex="-1" aria-label="name" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="name">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Email Id" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="email">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Mobile Number" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="mob">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Address" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="add">
     </td>
    </tr>
        <tr role="row">
     <td role="gridcell" tabindex="-1" aria-label="name" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="name">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Email Id" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="email">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Mobile Number" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="mob">
     </td>
     <td role="gridcell" tabindex="-1" aria-label="Address" aria-describedby="f2_key">
      <input type="text" tabindex="-1" class="link" name="add">
     </td>
    </tr>
      </table>
    </div>
   </body>
</html>
Hope this will helps to you..!!
Madhav Saraf
  • 100
  • 1
  • 9
0

For arrow key focusing, I ended up bundling together a bunch of different solutions posted here and came up with this. Still not sure why .next(), or .prev() wouldn't work for rows... seemed to need .prevAll and .nextAll for some reason:

   $("input").keydown(function (e) {
    var textInput = this;
    var val = textInput.value;
    var isAtStart = false, isAtEnd = false;
    var cellindex = $(this).parents('td').index();
    if (typeof textInput.selectionStart == "number") {
        // Non-IE browsers


        isAtStart = (textInput.selectionStart == 0);
        isAtEnd = (textInput.selectionEnd == val.length);
    } else if (document.selection && document.selection.createRange) {
        // IE <= 8 branch
        textInput.focus();
        var selRange = document.selection.createRange();
        var inputRange = textInput.createTextRange();
        var inputSelRange = inputRange.duplicate();
        inputSelRange.moveToBookmark(selRange.getBookmark());
        isAtStart = inputSelRange.compareEndPoints("StartToStart", inputRange) == 0;
        isAtEnd = inputSelRange.compareEndPoints("EndToEnd", inputRange) == 0;
    }

      // workaround for text inputs of 'number' not working in Chrome... selectionStart/End is null.  Can no longer move cursor left or right inside this field.
    if (textInput.selectionStart == null) {
        if (e.which == 37 || e.which == 39) {

            isAtStart = true;
            isAtEnd = true;
        }
    }

    if (e.which == 37) {
        if (isAtStart) {
            $(this).closest('td').prevAll('td').find("input").focus();
        }
    }
    if (e.which == 39) {

        if (isAtEnd) {
            $(this).closest('td').nextAll('td').find("input").not(":hidden").first().focus();
        }
    }
    if (e.which == 40) {
              $(e.target).closest('tr').nextAll('tr').find('td').eq(cellindex).find(':text').focus();
    }
    if (e.which == 38) {
    $(e.target).closest('tr').prevAll('tr').first().find('td').eq(cellindex).find(':text').focus();
    }

});
pcalkins
  • 1,188
  • 13
  • 20