Activate an input tag on the click on a image "modify"

96 views Asked by At

I am coding a website in which admin has access to a dashboard page where they can see the list of users. I would like to add the ability for the admin to change other users’ roles in the same line.

Here is the code:

<table>
<caption> Liste des utilisateurs </caption>
<tr class="title">
    <th>Date de création</th>
    <th>Prénom</th>
    <th>Nom d'utilisateur</th>
    <th>Mail</th>
    <th>Rôle</th>
    <th>Id</th>
</tr>

<tr>
    <td>0000-00-00 00:00:00</td>
    <td></td>
    <td>username</td>
    <td>*****@mail.fr</td>
    <td>admin</td>
    <td>22</td>
    <img class="information_modify" src="chemin" alt="img_modifier"> 
</tr> 
 
<tr>
    <td>2022-05-08 09:29:53</td>
    <td></td>
    <td>username</td>
    <td>****@mail.fr</td>
    <td>user</td>
    <td>23</td>
    <img class="information_modify" src="chemin" alt="img_modifier"> 
</tr>
</table>

But I have two problems:

  1. I don't know how to change <td> to <input> when someone clicks on an image; maybe with JavaScript?

  2. Even if I make it, how can I limit the change to only that line, and not all the <td>. I use a forEach loop in php to generate this code. So I can't really generate the code with each line personalised.

I need your help. Thanks in advance for your answers.

1

There are 1 answers

0
David Thomas On

There are a couple of approaches, here, the first is to explicitly answer your question, and create a system in which – when the .information_modify element is clicked – the text-content of the cells are inserted into an <input> element (and a second click on that .information_modify element 'saves' the information to the <table>, saving to the database is beyond the scope of this question):

// defining a named function to handle the toggling of the editing,
// using Arrow syntax and passing in the Event Object from the use
// of EventTarget.addEventListener():
const editToggle = (evt) => {
  // retrieving the element to which the event-handler was bound:
    let toggle = evt.currentTarget,
      // here we retrieve the closest ancestor <tr> element:
        row = toggle.closest('tr'),
      // and here we create a new <input> element:
      input = document.createElement('input'),
      // retrieving a list of cells from the current <tr>, which
      // have the class of 'canModify':
      editableCells = row.querySelectorAll('.canModify');

  // here we toggle the class of 'editInProgress' on the toggle element's
  // ancestor <tr> and <tbody> elements in order to allow for styling and
  // detection of whether editing is, or is not, in progress:
  row.classList.toggle('editInProgress');
  row.closest('tbody').classList.toggle('editInProgress');
  
  // here we test to see if the ancestor <tr> element has the class of
  // 'editInProgress' in its classList property:
  if (row.classList.contains('editInProgress')) {
    // if so, we're in an editing sitation:
    // here we iterate over the NodeList, using NodeList.prototype.forEach(),
    // along with its anonymous Arrow function:
    editableCells.forEach(
      // 'cell' is a reference to the current <td> of the NodeList of
      // <td> elements over which we're iterating:
      (cell) => {
        // here we clone the created <input> element:
        let clone = input.cloneNode(),
        // we retrieve the value of the data-list attribute, using the
        // HTMLElement.dataset API; though we could instead have  used
        // cell.getAttribute('data-list'):
                dataList = cell.dataset.list;
            
        // we set the <input> element's type property to be equal
        // to the data-type attribute/property:
        clone.type = cell.dataset.type;
        // setting the value of the <input> to be equal to the
        // text-content of the current <td> element, trimming away
        // leading or trailing white-space:
        clone.value = cell.textContent.trim();
        
        // here we use CSSStyleDeclaration.setProperty() to set the property
        // of the custom CSS Property '--width' to be equal to the rounded-
        // value of the width of the <td> element; this is because:
        // a: I don't like the way that an <input> of default-width makes the
        //    dimensions of the <table> jump around, and
        // b: trying to use
        //      clone.width = `${Math.round(cell.getBoundingClientRect().width)}px`
        //   didn't seem to work (the width kept being set to 0 in the HTML, despite
        //   retrieving the correct value):
        clone.style.setProperty('--width',`${Math.round(cell.getBoundingClientRect().width)}px`);
        
        // if the dataList is a truthy value (hence it's not
        // undefined, null or an empty-string):
        if (dataList) {
          // we use HTMLElement.setAttribute() to set the
          // attribute-value (dataList) of the 'list' attribute:
            clone.setAttribute('list', dataList);
        }
        
        // we remove the current text-content (having cached it,
        // and set the <input> element's value):
        cell.textContent = '';
        // and append the cloned <input> to the <td>:
        cell.append(clone);
    });
  // otherwise, if the <tr> element does not have the class of 'editInProgress':
  } else {
    // we again iterate over the editableCells, and again in the same manner:
    editableCells.forEach(
        (cell) => {
        // here we retrieve the first/only <input> found within the
        // <td> element:
        let input = cell.querySelector('input'),
            // we retrieve the dataList element from the <input> element's
            // list property:
                dataList = input.list,
            // and recover the current-value of the <input>:
                text = input.value.trim();
        
        // if the dataList is truthy (not null, undefined...),
        // and the text is not an empty string,
        // and an Array formed from the <option> elements of the
        // <datalist> doesn't include the current <input> value:
        if (dataList && text.length > 0 && ![...dataList.options].map((opt)=>opt.text).includes(text)) {
          // we append a new <option> element to the <datalist>, whose
          // text, and therefore value, is set to be equal to the current
          // <input> value (so a new option, such as 'Dr' or 'Lead Administrator'
          // doesn't have to be fully typed out again, should it be required again):
            dataList.append(new Option(text));
        }
        
        // we remove all contents of the <td>:
        cell.innerHTML = '';
        // and we append the text, from the <input> element's value:
        cell.append(text);
    });
  }
},
// caching a NodeList of all '.information_modify' elements:
editButtons = document.querySelectorAll('.information_modify');

// iterating over that NodeList, again using NodeList.prototype.forEach():
editButtons.forEach(
  // binding the editToggle() function (note the deliberate lack of parentheses) as
  // the event-handler for the 'click' event:
    (button) => button.addEventListener('click', editToggle)
);
*, ::before, ::after {
  box-sizing: border-box;
  font-family: Roboto, Montserrat, system-ui;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: collapse;
  margin-block: 1em;
  margin-inline: auto;
  table-layout: fixed;
  width: clamp(auto, 80vw, 900px);
}

th {
  font-weight: bold;
}

th, td {
  border-block-end: 2px solid #000;
  padding-block: 0.25em;
  padding-inline: 0.5em;
  }
  
.information_modify {
  aspect-ratio: 1;
  border: 1px solid rgb(200 200 200 / 0.7);
  cursor: pointer;
  display: inline-block;
  height: 1em;
  overflow: hidden;
}

img:empty {
  --alpha: 0.5;
  --accent-color: rgb(200 200 200 / var(--alpha));
  background-image: repeating-linear-gradient(45deg, transparent 0 5px, var(--accent-color) 5px 8px);
}

img:empty:hover {
  --alpha: 1;
}

/* highlighting the <td> elements of the currently-selected
   <tr> */
tr.editInProgress > td {
  background-color: #ffa;
}

/* any <td> elements which are not found within a <tr> with
   the class of 'editInProgress' but are found within an
   ancestor (in this case <tbody>) element with the class of
   'editInProgress': */
.editInProgress tr:not(.editInProgress) td {
  /* to give the impression that other <td> elements
     are not interactive while editing in progress: */
  cursor: not-allowed;
  opacity: 0.4;
  pointer-events: none;
  user-select: none;
}

input {
  /* here we set the width of the <input> elements to be either
     equal to the CSS property '--width' or the default width
     of 5em if the '--width' property is invalid for any reason: */
  width: var(--width, 5em);
}
<!-- using datalist elements to hold potential options where it
     makes sense to do so:-->
<datalist id="prenom">
  <option>Mr</option>
  <option>Ms</option>
  <option>Mrs</option>
</datalist>
<datalist id="role">
  <option>admin</option>
  <option>analyst</option>
  <option>executive officer</option>
  <option>marketing</option>
  <option>sales</option>
  <option>superuser</option>
  <option>user</option>
</datalist>


<table>
  <caption> Liste des utilisateurs </caption>
  <!-- wrapping the row of <th> elements in a <thead>: -->
  <thead>
    <tr class="title">
      <th>Date de création</th>
      <th>Prénom</th>
      <th>Nom d'utilisateur</th>
      <th>Mail</th>
      <th>Rôle</th>
      <th>Id</th>
      <th></th>
    </tr>
  </thead>
  <!-- wrapping the contents of the <table> in a <tbody>: -->
  <tbody>
    <!-- the fake details here were created from a JS Fiddle I created
         here: https://jsfiddle.net/davidThomas/mv2docp7/ feel free to
         play around for your own development needs: -->
    <tr>
      <td>1994-01-31 12:16</td>
      <!-- I added the class of 'canModify' to the elements that it makes sense
           to modify (it doesn't make sense - to me - to modify a user's
           id or creation-date), but adjust to taste.
           I added the custom data-* attributes for use in the JavaScript,
           data-type: determines the <input> type, whereas data-list
           identifies the <datalist> element to be associated to that
           <input>: -->
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Joanne Randall</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">user</td>
      <td>1664</td>
      <!-- an <img>, in a <table>, must be wrapped in either a <th> or
           <td> element; so I wrapped said element in a <td>: -->
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1982-02-19 09:12</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Tim Scott</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">user</td>
      <td>166</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1990-01-25 09:17</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Cameron May</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">marketing</td>
      <td>1736</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1980-01-17 14:01</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Carol Vance</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">sales</td>
      <td>564</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1999-08-01 09:44</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Megan Fraser</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">user</td>
      <td>1804</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1979-11-30 12:21</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Sarah Paterson</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">executive officer</td>
      <td>1164</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1986-10-08 15:25</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Nicholas Morgan</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">admin</td>
      <td>733</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1978-07-19 09:00</td>
      <td class="canModify" data-type="text" data-list="prenom"></td>
      <td class="canModify" data-type="text">Sam Abraham</td>
      <td class="canModify" data-type="email">*****@mail.fr</td>
      <td class="canModify" data-type="text" data-list="role">superuser</td>
      <td>1440</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
  </tbody>
</table>

JS Fiddle demo.

The above approach, though, seems a little unnecessary; since a user only really needs the illusion of change to enable editing. With that in mind, the following approach retains <input> elements within the table-cells and simply changes their style to indicate editing possibilities:

const editToggle = (evt) => {
    let toggle = evt.currentTarget,
      row = toggle.closest('tr');

    row.classList.toggle('editInProgress');
    row.closest('tbody').classList.toggle('editInProgress');

    // here we select, and then iterate over, the NodeList of all <input>
    // elements in the current <tr> element:
    row.querySelectorAll('input').forEach(
      // passing in a reference to the current element (here: <input>) to
      // the body of the anonymous Arrow function:
      (el) => {
        // retrieving the value of the current <input>, and using
        // String.prototype.trim() to remove leading, and trailing,
        // white-space:
        let value = el.value.trim(),
            // retrieving the <datalist> element associated with
            // the current <input>:
            dataList = el.list,
            // if the dataList is truthy (so not null, undefined, false...), we
            // use the conditional ('ternary') operator to return an Array of
            // the text of each <option> of the <datalist>; otherwise we return
            // an empty Array:
            dataListOptions = dataList ? [...dataList.options].map((opt) => opt.text) : [];
            
        // if the dataList is truthy (as above), and the value is not an empty string,
        // and if the dataListOptions Array does not include the current value:
        if (dataList && value.length > 0 && !dataListOptions.includes(value)) {
          // we append a new <option> element to the <datalist>, with its
          // text (and implicit value) set to the value of the current <input>:
          dataList.append(new Option(value));
        }
        
        // if the ancestor <tr> has the class of 'editInProgress':
        if (row.classList.contains('editInProgress')) {
          // we remove the 'readonly' attribute:
          el.removeAttribute('readonly');
        } else {
          // otherwise, we set the readonly attribute:
          el.setAttribute('readonly', '');
        }
      })
  },
  // retrieving the NodeList of all elements with the class of 'information_modify':
  editButtons = document.querySelectorAll('.information_modify');

// iterating over that NodeList using NodeList.prototype.forEach():
editButtons.forEach(
  // binding the editToggle() function as the event-handler for the 'click' event:
  (button) => button.addEventListener('click', editToggle)
);
*,
 ::before,
 ::after {
  box-sizing: border-box;
  font-family: Roboto, Montserrat, system-ui;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: collapse;
  margin-block: 1em;
  margin-inline: auto;
  table-layout: fixed;
  width: clamp(50em, 80vw, 1000px);
}

th {
  font-weight: bold;
}

th,
td {
  border-block-end: 2px solid #000;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.information_modify {
  aspect-ratio: 1;
  border: 1px solid rgb(200 200 200 / 0.7);
  cursor: pointer;
  display: inline-block;
  height: 1em;
  overflow: hidden;
}

img:empty {
  --alpha: 0.5;
  --accent-color: rgb(200 200 200 / var(--alpha));
  background-image: repeating-linear-gradient(45deg, transparent 0 5px, var(--accent-color) 5px 8px);
}

img:empty:hover {
  --alpha: 1;
}

td {
  max-width: 10.5em;
}

td,
input {
  color: inherit;
}

/* selecting any <td> or <th> element which is the second-child
   of its parent: */
:is(td, th):nth-child(2) {
  /* setting the width to 2em, this is to narrow the 'prenom'
     cells: */
  width: 5em;
}

/* selecting any <td> or <th> element which is the last-child
   of its parent: */
:is(td, th):last-child {
  /* setting its width to 2em, to narrow the last column;
     I set the .information_modify <img> elements to be
     1em in width and height, so adjust to your own requirements: */
  width: 2em;
}

tr.editInProgress>td {
  background-color: #ffa;
}

/* selecting all <td> or <input> elements which are within a <tr>
   element that does not match the selector which is itself in
   another ancestor which has the class of 'editInProgress': */
.editInProgress tr:not(.editInProgress) :is(td, input) {
  /* trying to indicate a lack of interactivity: */
  cursor: not-allowed;
  opacity: 0.4;
  pointer-events: none;
  user-select: none;
}

input {
  /* removing the border of all <input> elements, to remove the
     impression of those elements being an <input>: */
  border: unset;
  outline: none;
  /* setting the width of the elements to the minimum size of
     either 10em or 95% of the parent: */
  width: min(10em, 95%);
}
<datalist id="prenom">
  <option>Mr</option>
  <option>Ms</option>
  <option>Mrs</option>
</datalist>
<datalist id="role">
  <option>admin</option>
  <option>analyst</option>
  <option>executive officer</option>
  <option>marketing</option>
  <option>sales</option>
  <option>superuser</option>
  <option>user</option>
</datalist>

<table>
  <caption> Liste des utilisateurs </caption>
  <thead>
    <tr class="title">
      <th>Date de création</th>
      <th>Prénom</th>
      <th>Nom d'utilisateur</th>
      <th>Mail</th>
      <th>Rôle</th>
      <th>Id</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1994-01-31 12:16</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Anthony Sutherland" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="user" readonly>
      </td>
      <td>1664</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1982-02-19 09:12</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Owen Ince" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="analyst" readonly>
      </td>
      <td>166</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1990-01-25 09:17</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Sonia Hardacre" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="analyst" readonly>
      </td>
      <td>1736</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1980-01-17 14:01</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Liam Knox" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="superuser" readonly>
      </td>
      <td>564</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1999-08-01 09:44</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Stephanie Mathis" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="admin" readonly>
      </td>
      <td>1804</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1979-11-30 12:21</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Kevin Jackson" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="analyst" readonly>
      </td>
      <td>1164</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1986-10-08 15:25</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Sonia Hardacre" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="superuser" readonly>
      </td>
      <td>733</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
    <tr>
      <td>1978-07-19 09:00</td>
      <td class="canModify" data-type="text" data-list="prenom">
        <input type="text" list="prenom" readonly>
      </td>
      <td class="canModify" data-type="text">
        <input type="text" value="Trevor Lawrence" readonly>
      </td>
      <td class="canModify" data-type="email">
        <input type="email" value="*****@mail.fr" readonly>
      </td>
      <td class="canModify" data-type="text" data-list="role">
        <input type="text" list="role" value="user" readonly>
      </td>
      <td>1440</td>
      <td>
        <img class="information_modify" src="chemin" alt="img_modifier">
      </td>
    </tr>
  </tbody>
</table>

JS Fiddle demo.

With the second approach there is the obvious problem that the selection behaviour is noticeably different than if the cell-contents weren't simply 'disguised' <td> elements, because of the default (and apparently non-adjustable) user-select: contain, which causes a user-selection to be contained within the element in which it started and not to participate in a selection that began elsewhere.

Not to mention that I'm choosing to toggle the presence of the readonly attribute on the <input> elements.

The preferred approach, I think, will come down to personal preference and project requirements.