I've been stuck with this problem for a while now and almost got it to work, but I'm left with an issue I cannot solve or find a solution anywhere.
So I'm trying to make an application which helps users log certain data - it all works on a table, adding rows and deleting them works just fine, but I have a problem with editing them.
At the end of each row, there are 2 buttons - delete and edit.
Edit pops up a modal, which should (and it does as far as I know) display inputs with values read from a specific table row. When you do this for the first time after reloading it works fine, but afterward, if you have more than one table rows it just starts to clean up these tr's upon submitting an edit of a row.
There's clearly a problem with logic, and I just cannot solve it. Most likely it can be found in this block
Record.prototype.addRow = function(){
let newLog = document.createElement('tr');
newLog.setAttribute('id',idContainer);
idContainer++;
recordTable.appendChild(newLog);
//store values and add them to each td
let valueStorage = [this.time,this.latitude,this.longitude,this.heading,
this.speed,this.wind,this.sea,this.visibility,this.remarks];
for (let i=0; i<9;i++) {
let tableData = document.createElement('td');
newLog.appendChild(tableData);
tableData.textContent = valueStorage[i];
}
//add 2 buttons - delete and edit
let deleteBtn = document.createElement('button');
deleteBtn.setAttribute('class','new-record__delete-row-btn')
newLog.appendChild(deleteBtn);
let editBtn = document.createElement('button');
editBtn.setAttribute('class','new-record__edit-row-btn')
newLog.appendChild(editBtn);
//adding functionality to edit/delete btns
function editThisRow(e){
//on pop up display values from the edited row
let popUpHeading = document.querySelector('.new-record__popup h2');
let thisRow = e.target.parentNode;
let editStorage = [];
for(let i = 0; i < 9; i++){
editStorage.push(thisRow.childNodes[i].textContent);
}
editStorage[1]=editStorage[1].replace(/([A-Z])/,"");
editStorage[2]=editStorage[2].replace(/([A-Z])/,"");
for(let i = 0; i <8; i++){
newRecordInputs[i].value = editStorage[i];
}
remarks.value = editStorage[8];
popUpHeading.textContent = 'Edit your record!'
openNewRecordPopup();
//adding event listener to the record edit button, so that it applies changes
const recordEditBtn = document.querySelector('#edit-record')
newRecordSubmitBtn.style.display = "none";
recordEditBtn.style.display = "block";
recordEditBtn.addEventListener('click',()=>{
let thisRowTds = thisRow.childNodes;
for(let i = 0; i < 8; i++){
thisRowTds[i].textContent = newRecordInputs[i].value
}
thisRowTds[8].textContent = remarks.value;
closeNewRecordPopup();
popUpHeading.textContent = 'Fill out inputs below to make a new record';
newRecordSubmitBtn.style.display = "block";
recordEditBtn.style.display = "none";
sortRecords();
})
}
deleteBtn.addEventListener('click',()=>{
newLog.remove();
})
editBtn.addEventListener('click',editThisRow);
}
All the code needed and live example can be found on GitHub:
https://github.com/michynow/electronic-ship-log-book
https://michynow.github.io/electronic-ship-log-book/
quick note: This is not designed for mobile use, and I really would appreciate vanilla JS solutions, without lib's or frameworks.
All edit clicks keep on stacking in below button:
const recordEditBtn = document.querySelector('#edit-record')
I have changed it to have fresh event handler always like this:
let oldRecordEditBtn = document.querySelector('#edit-record')
let recordEditBtn = oldRecordEditBtn.cloneNode(true);
oldRecordEditBtn.parentNode.replaceChild(recordEditBtn, oldRecordEditBtn);
So now below snippet is working fine for your issue:
//Set ship header and type
const shipDetailsBtn = document.querySelector('.ship-details__button');
const shipDetailsClosingBtn = document.querySelector('#ship-details__popup-closing-btn');
const shipDetailsPopUp = document.querySelector('.ship-details__popup');
const shipTypeSpan = document.querySelector('#ship-type-span');
const shipNameSpan = document.querySelector('#ship-name-span');
//opening of a form
shipDetailsBtn.addEventListener('click', () => {
shipDetailsPopUp.style.visibility = "visible";
disableButtons();
})
//disabling of button operation when popup is open;
function disableButtons() {
voyageDetailsBtn.disabled = true;
newRecordBtn.disabled = true;
shipDetailsBtn.disabled = true;
}
function enableButtons() {
voyageDetailsBtn.disabled = false;
newRecordBtn.disabled = false;
shipDetailsBtn.disabled = false;
}
//closing of a form
shipDetailsClosingBtn.addEventListener('click', () => {
shipDetailsPopUp.style.visibility = "hidden";
enableButtons();
})
//pop up input selection
const shipTypeSelect = document.querySelector('#ship-type');
const shipDetailsPopUpSubmit = document.querySelector('#ship-details-submit-btn');
//Submitting of ship details
shipDetailsPopUpSubmit.addEventListener('click', () => {
shipDetailsPopUp.style.visibility = "hidden";
shipTypeSpan.textContent = shipTypeSelect.options[shipTypeSelect.selectedIndex].value + " ";
shipNameSpan.textContent = document.querySelector('#ship-name-input').value;
enableButtons();
shipDetailsBtn.textContent = "Edit ship details";
})
//date and destination pop up form
const voyageDetailsBtn = document.querySelector('.voyage-details__button');
const voyageDetailsPopUp = document.querySelector('.voyage-details__popup');
const voyageDetailsClosingBtn = document.querySelector('#voyage-details__popup-closing-btn');
voyageDetailsBtn.addEventListener('click', () => {
voyageDetailsPopUp.style.visibility = "visible";
disableButtons();
})
//Update date and destination rows
const dateSpan = document.querySelector('#date-span');
const destinationSpanFrom = document.querySelector('#dest-span__from');
const destinationSpanTo = document.querySelector('#dest-span__to');
const voyageDetailsSubmit = document.querySelector('#voyage-details__submit-btn');
const destFromInput = document.querySelector('#ship-destination-input__from');
const destToInput = document.querySelector('#ship-destination-input__to');
voyageDetailsSubmit.addEventListener('click', () => {
dateSpan.textContent = " " + document.querySelector('#date-input').value;
//prevent empty inputs on destination form
if (destFromInput.value !== "" || destToInput.value !== "") {
destinationSpanFrom.textContent = " " + destFromInput.value + ' to: ';
destinationSpanTo.textContent = destToInput.value;
voyageDetailsBtn.textContent = "Edit date and destination";
voyageDetailsPopUp.style.visibility = "hidden";
enableButtons();
} else {
alert('Please fill in voyage details!');
}
})
//remember to add a default attribute setting the current date as placeholder in date form;
//closing of a voyage details pop up
voyageDetailsClosingBtn.addEventListener('click', () => {
voyageDetailsPopUp.style.visibility = "hidden";
enableButtons();
})
//add new data pop up opening / closing
const newRecordBtn = document.querySelector('.new-record__btn');
const newRecordPopUp = document.querySelector('.new-record__popup');
const newRecordSubmitBtn = document.querySelector('#new-record__submit-btn');
const recordTable = document.querySelector('.records-table');
newRecordBtn.addEventListener('click', openNewRecordPopup);
function openNewRecordPopup() {
newRecordPopUp.style.visibility = "visible";
disableButtons();
}
const newRecordClosingBtn = document.querySelector('#new-record__popup-closing-btn');
function closeNewRecordPopup() {
newRecordPopUp.style.visibility = "hidden";
enableButtons();
}
newRecordClosingBtn.addEventListener('click', closeNewRecordPopup);
// store input values in an array, then pass it to all created td's
let newRecordInputs = document.querySelectorAll('.new-record__popup input');
let remarks = document.querySelector('textarea');
function clearInputs() {
for (let i = 0; i < newRecordInputs.length; i++) {
newRecordInputs[i].value = "";
remarks.value = "";
};
}
newRecordSubmitBtn.addEventListener('click', addRecord);
class Record {
constructor(time, latitude, longitude, heading, speed, wind, sea, visibility, remarks) {
this.time = time;
this.latitude = latitude;
this.longitude = longitude;
this.heading = heading;
this.speed = speed;
this.wind = wind;
this.sea = sea;
this.visibility = visibility
this.remarks = remarks;
this.addRow();
}
}
//set id for each row
let idContainer = 0;
Record.prototype.addRow = function() {
let newLog = document.createElement('tr');
newLog.setAttribute('id', idContainer);
idContainer++;
recordTable.appendChild(newLog);
//store values and add them to each td
let valueStorage = [this.time, this.latitude, this.longitude, this.heading,
this.speed, this.wind, this.sea, this.visibility, this.remarks
];
for (let i = 0; i < 9; i++) {
let tableData = document.createElement('td');
newLog.appendChild(tableData);
tableData.textContent = valueStorage[i];
}
//add 2 buttons - delete and edit
let deleteBtn = document.createElement('button');
deleteBtn.setAttribute('class', 'new-record__delete-row-btn')
newLog.appendChild(deleteBtn);
let editBtn = document.createElement('button');
editBtn.setAttribute('class', 'new-record__edit-row-btn')
newLog.appendChild(editBtn);
//adding functionality to edit/delete btns
let editThisRow = function(e) {
//on pop up display values from the edited row
let popUpHeading = document.querySelector('.new-record__popup h2');
let thisRow = e.target.parentNode;
let editStorage = [];
for (let i = 0; i < 9; i++) {
editStorage.push(thisRow.childNodes[i].textContent);
}
editStorage[1] = editStorage[1].replace(/([A-Z])/, "");
editStorage[2] = editStorage[2].replace(/([A-Z])/, "");
for (let i = 0; i < 8; i++) {
newRecordInputs[i].value = editStorage[i];
}
remarks.value = editStorage[8];
popUpHeading.textContent = 'Edit your record!'
openNewRecordPopup();
//adding event listener to the record edit button, so that it applies changes
let oldRecordEditBtn = document.querySelector('#edit-record')
let recordEditBtn = oldRecordEditBtn.cloneNode(true);
oldRecordEditBtn.parentNode.replaceChild(recordEditBtn, oldRecordEditBtn);
newRecordSubmitBtn.style.display = "none";
recordEditBtn.style.display = "block";
recordEditBtn.addEventListener('click', () => {
let thisRowTds = thisRow.childNodes;
for (let i = 0; i < 8; i++) {
thisRowTds[i].textContent = newRecordInputs[i].value
}
thisRowTds[8].textContent = remarks.value;
closeNewRecordPopup();
popUpHeading.textContent = 'Fill out inputs below to make a new record';
newRecordSubmitBtn.style.display = "block";
recordEditBtn.style.display = "none";
sortRecords();
})
}
deleteBtn.addEventListener('click', () => {
newLog.remove();
})
editBtn.addEventListener('click', editThisRow);
}
function addRecord() {
//selecting all input values and storing them in an object
let timeValue = document.querySelector('#new-record__UTC-time').value;
let northOrSouth = document.querySelector('.north-south');
let northOrSouthValue = northOrSouth.options[northOrSouth.selectedIndex].value;
let latitudeValue = document.querySelector('#new-record__latitude').value + " " + northOrSouthValue;
let eastOrWest = document.querySelector('.east-west');
let eastOrWestValue = eastOrWest.options[eastOrWest.selectedIndex].value;
let longitudeValue = document.querySelector('#new-record__longitude').value + " " + eastOrWestValue;
let headingValue = document.querySelector('#new-record__heading').value;
let speedValue = document.querySelector('#new-record__SOG').value;
let windValue = document.querySelector('#new-record__wind-force').value;
let seaValue = document.querySelector('#new-record__sea-state').value;
let visibilityValue = document.querySelector('#new-record__visibility').value;
let anotherRecord = new Record(timeValue, latitudeValue, longitudeValue,
headingValue, speedValue, windValue, seaValue, visibilityValue, remarks.value);
closeNewRecordPopup();
clearInputs();
sortRecords();
}
//function for sorting out table rows by time dynamically upon edit or new record
function sortRecords() {
let rows, switching, i, x, y, shouldSwitch;
switching = true
while (switching) {
switching = false;
rows = recordTable.rows;
for (i = 2; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("td")[0];
y = rows[i + 1].getElementsByTagName("td")[0];
if (x.textContent > y.textContent) {
shouldSwitch = true;
break;
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
}
}
}
body {
z-index: 0;
padding-top: 2vh;
/*height: 100vh;
width: 100vw;*/
/*overflow: hidden;*/
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
background: -webkit-gradient(linear, left top, left bottom, color-stop(50%, white), to(#347deb));
background: linear-gradient(to bottom, white 50%, #347deb);
position: relative;
}
header {
text-align: center;
color: black;
}
header p,
header .destination {
text-align: left;
padding-left: 5vw;
font-weight: bold;
}
.records-table-container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
width: 90vw;
height: 60vh;
margin: auto;
overflow-x: auto;
background-color: #faf6c3;
-webkit-box-shadow: 0px -1px 22px -3px rgba(0, 0, 0, 0.75);
box-shadow: 0px -1px 22px -3px rgba(0, 0, 0, 0.75);
}
.records-table-container .new-record__delete-row-btn,
.records-table-container .new-record__edit-row-btn {
padding: 0.3rem;
width: 60px;
margin: 0.1rem;
color: white;
font-weight: bold;
border: none;
cursor: pointer;
background-color: #963c2c;
-webkit-transition: 0.3s all;
transition: 0.3s all;
}
.records-table-container .new-record__delete-row-btn:hover,
.records-table-container .new-record__edit-row-btn:hover {
background-color: #803325;
}
.records-table-container .new-record__delete-row-btn::after,
.records-table-container .new-record__edit-row-btn::after {
content: "Delete";
}
.records-table-container .new-record__edit-row-btn {
background-color: #5a51d6;
}
.records-table-container .new-record__edit-row-btn::after {
content: "Edit";
}
.records-table-container .new-record__edit-row-btn:hover {
background-color: #4f47bf;
}
.records-table-container table,
.records-table-container th,
.records-table-container td,
.records-table-container tr {
border: 1px solid black;
padding: 5px 2px;
border-collapse: collapse;
text-align: center;
}
.records-table-container .table__input-general-description {
width: 40vw;
}
.operation-buttons {
text-align: center;
padding: 5vh;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-pack: distribute;
justify-content: space-around;
}
.operation-buttons .new-record__btn {
border: none;
background-color: #4fa867;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.operation-buttons .new-record__btn:hover {
background-color: #3f8a53;
cursor: pointer;
}
.operation-buttons .export-button {
border: none;
background-color: #963c2c;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.operation-buttons .export-button:hover {
background-color: #803325;
cursor: pointer;
}
.operation-buttons .ship-details__button {
border: none;
background-color: #5a51d6;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.operation-buttons .ship-details__button:hover {
background-color: #4f47bf;
cursor: pointer;
}
.operation-buttons .voyage-details__button {
border: none;
background-color: #88b33e;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.operation-buttons .voyage-details__button:hover {
background-color: #739636;
cursor: pointer;
}
.ship-details__popup {
display: block;
visibility: hidden;
z-index: 9;
position: absolute;
top: 25%;
left: 20%;
width: 60vw;
background-color: lightgrey;
text-align: center;
padding: 1rem;
border: 1px solid black;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.ship-details__popup #ship-details__popup-closing-btn {
height: 20px;
width: 20px;
background-color: darkred;
color: white;
text-align: center;
font-weight: bold;
position: absolute;
right: 20px;
cursor: pointer;
border: 2px solid black;
}
.ship-details__popup form {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
width: 40%;
min-height: 25vh;
margin: 0 auto;
line-height: 1.5rem;
}
.ship-details__popup form input {
text-align: center;
}
.ship-details__popup select {
text-align-last: center;
padding: 0.1rem;
}
.ship-details__popup #ship-details-submit-btn {
border: none;
background-color: #31508f;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.ship-details__popup #ship-details-submit-btn:hover {
background-color: darkred;
cursor: pointer;
}
.voyage-details__popup {
display: block;
visibility: hidden;
z-index: 9;
position: absolute;
top: 25%;
left: 20%;
width: 60vw;
background-color: lightgrey;
text-align: center;
padding: 1rem;
border: 1px solid black;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.voyage-details__popup form {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
width: 40%;
min-height: 25vh;
margin: 0 auto;
line-height: 1.5rem;
}
.voyage-details__popup form input {
text-align: center;
}
.voyage-details__popup #date-input {
padding-left: 15%;
cursor: pointer;
}
.voyage-details__popup #voyage-details__popup-closing-btn {
height: 20px;
width: 20px;
background-color: darkred;
color: white;
text-align: center;
font-weight: bold;
position: absolute;
right: 20px;
cursor: pointer;
border: 2px solid black;
}
.voyage-details__popup #voyage-details__submit-btn {
border: none;
background-color: #31508f;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
.voyage-details__popup #voyage-details__submit-btn:hover {
background-color: darkred;
cursor: pointer;
}
.new-record__popup {
display: block;
visibility: hidden;
z-index: 9;
position: absolute;
top: 25%;
left: 20%;
width: 60vw;
background-color: lightgrey;
text-align: center;
padding: 1rem;
border: 1px solid black;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
top: 15%;
margin: auto;
}
.new-record__popup form {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
width: 40%;
min-height: 25vh;
margin: 0 auto;
line-height: 1.5rem;
}
.new-record__popup form input {
text-align: center;
}
.new-record__popup #new-record__popup-closing-btn {
height: 20px;
width: 20px;
background-color: darkred;
color: white;
text-align: center;
font-weight: bold;
position: absolute;
right: 20px;
cursor: pointer;
border: 2px solid black;
}
.new-record__popup #new-record__submit-btn {
border: none;
background-color: #31508f;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
width: 40%;
margin: auto;
}
.new-record__popup #new-record__submit-btn:hover {
background-color: darkred;
cursor: pointer;
}
.new-record__popup #edit-record {
border: none;
background-color: #31508f;
color: white;
font-weight: bold;
padding: 10px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
width: 40%;
margin: auto;
display: none;
}
.new-record__popup #edit-record:hover {
background-color: darkred;
cursor: pointer;
}
.new-record__popup form {
width: auto;
display: -ms-grid;
display: grid;
-ms-grid-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr;
grid-gap: 0.8rem;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
line-height: 1.5rem;
padding-bottom: 2rem;
}
.new-record__popup form #new-record__remarks {
resize: none;
height: 3rem;
padding: 0.2rem;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your ship's log book.</title>
<meta name="description" content="Electronic log book of your ship">
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
</head>
<body>
<header>
<h1><span id="ship-type-span"></span><span id="ship-name-span">Ship's name</span> logbook</h1>
<div class="date-destination-div">
<p>Date:<span id="date-span"></span></p>
<p>
Voyage from: <span id="dest-span__from"></span><span id="dest-span__to"></span>
</p>
</div>
</header>
<div class="records-table-container">
<table class="records-table" sortable>
<thead>
<tr class=table__input-general>
<th colspan="5">Voyage details</th>
<th colspan="3">Weather conditions</th>
<th class="table__input-general-description" rowspan="2">General description / comments</th>
<th class="actions" rowspan="2">Edit / delete record</th>
</tr>
<tr class="table__input-details">
<td>Time <br> UTC</td>
<td>Latitude <br> [° , ']</td>
<td>Longitude <br> [° , ']</td>
<td>Heading <br> [°]</td>
<td>SOG <br>[kt]</td>
<!-- weather conditions -->
<td>Wind force</td>
<td>Sea state</td>
<td>Visibility</td>
</tr>
</thead>
</table>
</div>
<div class="operation-buttons">
<button class="new-record__btn">
Add new record
</button>
<button class="voyage-details__button">
Enter date and destination
</button>
<button class="ship-details__button">
Set ship's details
</button>
<button class="export-button">
Export to PDF
</button>
</div>
<div class="ship-details__popup">
<div id="ship-details__popup-closing-btn">X</div><br>
<h2>Enter your ship's name and type</h2>
<form>
<label for="ship-type">Select your ship's type</label>
<!-- Selection of ship type - to be modified upon submit -->
<select name="ship-type" id="ship-type">
<option value="M/V">Motor Vessel</option>
<option value="M/T">Motor Tanker</option>
<option value="S/V">Sailing Vessel</option>
<option value="S/Y">Sailing Yacht</option>
<option value="OSV">Offshore Support Vessel</option>
<option value="DSV">Dive Support Vessel</option>
<option value="PSV">Platform Supply Vessel</option>
<option value="SOV">Service Operation Vessel</option>
<option value="Tug">Tugboat</option>
</select>
<label for="ship-name">Enter your ship's name:</label>
<input type="text" id="ship-name-input" placeholder="Enter your ship's name here" required><br>
<button type="button" id="ship-details-submit-btn">Submit</button>
</form>
</div>
<!-- Pop up with date setting and destination -->
<div class="voyage-details__popup">
<div id="voyage-details__popup-closing-btn">X</div><br>
<h2>Fill out inputs below to update voyage details</h2>
<form>
<label for="date">Enter date:</label>
<input type="date" id="date-input" name="date" value="" min="2018-01-01" max="2021-01-01">
<label for="destination">Enter your last port of call:</label>
<input type="text" id="ship-destination-input__from" placeholder="Enter your last port of call">
<label for="destination">Enter your current destination:</label>
<input type="text" id="ship-destination-input__to" placeholder="Enter your current destination"><br>
<button type="button" id="voyage-details__submit-btn">Submit</button>
</form>
</div>
<!--New record pop up-->
<div class="new-record__popup">
<div id="new-record__popup-closing-btn">X</div><br>
<h2>Fill out inputs below to make a new record</h2>
<form autocomplete="off">
<label for="UTC-time">UTC Time </label>
<input type="text" name="UTC-time" placeholder="Enter time of the record" id="new-record__UTC-time">
<label for="latitude">Latitude [° , '] </label>
<div class="latitude-container">
<input type="text" name="latitude" placeholder="Enter latitude" id="new-record__latitude">
<select class="north-south">
<option value="N">N</option>
<option value="S">S</option>
</select>
</div>
<label for="longitude">Longitude [° , '] </label>
<div class="longitude-container">
<input type="text" name="longitude" placeholder="Enter longitude" id="new-record__longitude" title="Degrees and minutes">
<select class="east-west">
<option value="E">E</option>
<option value="W">W</option>
</select>
</div>
<label for="heading">Heading [°] </label>
<input type="text" name="heading" placeholder="Enter your heading" id="new-record__heading">
<label for="SOG">Speed Over Ground [kt] </label>
<input type="number" name="SOG" placeholder="Enter your speed" min="-5" max="40" id="new-record__SOG">
<label for="wind-force">Wind Force [B] </label>
<input type="number" name="wind-force" placeholder="Enter wind force" min="0" max="12" id="new-record__wind-force">
<label for="sea-state">Sea State </label>
<input type="number" name="sea-state" placeholder="Enter sea state" min="0" max="9" id="new-record__sea-state">
<label for="visibility">Visibility</label>
<input type="text" name="visibility" placeholder="Enter visibility" id="new-record__visibility">
<label for="remarks">General remarks </label>
<textarea name="remarks" name="remarks" id="new-record__remarks" placeholder="Add remarks..."></textarea>
</form>
<button type="button" id="new-record__submit-btn">Submit record</button>
<button type="button" id="edit-record">Edit record</button>
</div>
</body>
</html>
Related
Please before tagging the question as duplicated, I tell you, I've been searching a lot and can't find a clear answer, so it may be worth it to try to get a clear one for the year we are living, 2022.
I have this contenteditable div:
<div class="input-field" placeholder="Tell me something..." contentEditable="true"></div>
I've been able to center the text and regular emojis setting the line-height in css but when I enter an image I am using as a custom emoji it does not get centered.
<img src="img/emojis/red-circle.png" class="image-emoji">
I am adding the image to the input field with event.target after I click it from another box:
inputField.innerHTML +=
"<div class='emo'>" + event.target.outerHTML + "</div>";
But the Divs get removed by innerHTML, so it blocks me from adding display:flex; align-items: center;
And if I set display:flex; align-items: center; directly on the contenteditable I get horizontal scrolling which I do not want.
Then if I set the css to be display:flex; align-items: center; flex-direction: column; on the contenteditable the image-emojis display one on each line every time, not side by side.
Please help.
EDIT:
Following the first answer advice I tested flex-wrap:wrap; on the contenteditable which also produces horizontal scrolling on long words with no spaces and flex-wrap:nowrap; does the same.
EDIT-2:
As suggested adding break-word:break-all; works partially but now I notice another problem with display: flex; It prevents the ENTER key from adding additional lines even thought <div> <br> </div> are added to the Html, any idea?
EDIT-3
Finally not using flex box, but just line-height to align text and emojis and as per Lukas below:
.input-field img {
vertical-align: middle;
}
...aligned the image.
const emojiBox = document.querySelector(".emoji-box");
const inputField = document.querySelector(".input-field");
emojiBox.addEventListener("click", (e) => {
let emoji = null;
let isImage = null;
let emojiImage = null;
let removeBR = null;
if (e.target != e.currentTarget) {
removeBR = inputField.querySelector("br");
if (removeBR) {
removeBR.outerHTML = "";
}
if (
e.target.tagName.toLowerCase() === "img" &&
e.target.classList.value.toLowerCase() === "emoji"
)
isImage = true;
if (isImage) {
emojiImage = e.target;
} else {
emoji = e.target;
}
}
if (emoji) {
inputField.innerHTML += emoji.outerHTML;
} else if (emojiImage) {
inputField.innerHTML += emojiImage.outerHTML;
}
cursorAtTheEnd();
});
function cursorAtTheEnd() {
let sel = window.getSelection();
sel.selectAllChildren(inputField);
sel.collapseToEnd();
inputField.focus();
}
html {
font-size: 62.5%;
}
.image-emoji {
max-width: 1.8rem;
max-height: 1.8rem;
border-radius: 100%;
padding: 0;
margin: 0 0.15rem 0;
}
.input-field {
display: flex;
align-items: center;
flex-wrap: wrap;
word-break: break-all;
font-size:1.6rem;
min-height: 3rem;
max-height: 20rem;
width: 40rem;
margin: 0.5rem;
padding: 1rem 6rem 1rem 4rem;
border: 2px solid #e6e6e6;
border-radius: 0.5rem;
outline: none;
overflow-y: auto;
}
[contenteditable="true"]:empty:before {
content: attr(placeholder);
pointer-events: none;
display: block; /* For Firefox */
}
.emoji-box {
display: flex;
align-items: center;
font-size:1.6rem;
background: white;
border: 0.2rem solid #eee;
border-radius: 0.5rem;
height: 3rem;
width: 40rem;
padding: 1rem 6rem 1rem 4rem;
margin: 0.5rem;
color: #183153;
cursor: pointer;
overflow-y: auto;
}
<div class="emoji-box">
<span>🙂</span>
<img src="https://upload.wikimedia.org/wikipedia/commons/0/02/Red_Circle%28small%29.svg" class="image-emoji">
</div>
<div class="input-field" placeholder="Tell me something..." contentEditable="true"></div>
try this
const emojiBox = document.querySelector(".emoji-box");
const inputField = document.querySelector(".input-field");
let lastKeyEnter = false;
function removeBR() {
let removeBR = inputField.querySelector("br:last-of-type");
if (
removeBR &&
removeBR.previousElementSibling &&
removeBR.previousElementSibling.tagName === "BR"
) {
removeBR.remove();
}
}
inputField.onkeydown = (e) => {
if (e.keyCode === 13) {
lastKeyEnter = true;
} else if (lastKeyEnter) {
lastKeyEnter = false;
}
};
emojiBox.addEventListener("click", (e) => {
let emoji = null;
let isImage = null;
let emojiImage = null;
if (e.target != e.currentTarget) {
if (
e.target.tagName.toLowerCase() === "img" &&
e.target.classList.value.toLowerCase() === "emoji"
)
isImage = true;
if (isImage) {
emojiImage = e.target;
} else {
emoji = e.target;
}
}
let lastChild = inputField.lastChild;
if (
lastChild &&
lastChild.previousSibling &&
lastChild.previousSibling.tagName === undefined
) {
lastChild.tagName === "BR" ? lastChild.remove() : "";
}
if (emoji && emoji.tagName === "SPAN") {
lastKeyEnter ? removeBR() : "";
lastKeyEnter = false;
inputField.innerHTML += emoji.innerHTML;
} else if (emoji && emoji.tagName === "IMG") {
lastKeyEnter ? removeBR() : "";
lastKeyEnter = false;
inputField.innerHTML += emoji.outerHTML;
} else if (emojiImage) {
lastKeyEnter ? removeBR() : "";
lastKeyEnter = false;
inputField.innerHTML += emojiImage.outerHTML;
}
cursorAtTheEnd();
});
function cursorAtTheEnd() {
let sel = window.getSelection();
sel.selectAllChildren(inputField);
sel.collapseToEnd();
inputField.focus();
}
html {
font-size: 62.5%;
}
.image-emoji {
max-width: 1.8rem;
max-height: 1.8rem;
border-radius: 100%;
padding: 0;
margin: 0 0.15rem 0;
}
.input-field {
word-break: break-all;
font-size:1.6rem;
min-height: 3rem;
max-height: 20rem;
width: 40rem;
margin: 0.5rem;
padding: 1rem 6rem 1rem 4rem;
border: 2px solid #e6e6e6;
border-radius: 0.5rem;
outline: none;
overflow-y: auto;
display: inline-block;
line-height: 2rem;
}
.input-field img {
vertical-align: text-bottom;
}
[contenteditable="true"]:empty:before {
content: attr(placeholder);
pointer-events: none;
display: block; /* For Firefox */
}
.emoji-box {
display: flex;
align-items: center;
font-size:1.6rem;
background: white;
border: 0.2rem solid #eee;
border-radius: 0.5rem;
height: 3rem;
width: 40rem;
padding: 1rem 6rem 1rem 4rem;
margin: 0.5rem;
color: #183153;
cursor: pointer;
overflow-y: auto;
}
<div class="emoji-box">
<span>🙂</span>
<img src="https://upload.wikimedia.org/wikipedia/commons/0/02/Red_Circle%28small%29.svg" class="image-emoji">
</div>
<div class="input-field" placeholder="Tell me something..." contentEditable="true"></div>
Ive been studying javascript and following along on this online tutorial for a to-do list. I switched up and added a few of my own features, but I am not sure how would go about adding a feature where I can edit a individual list item?
I started off creating a function editTodo(key), I know I would have to append my new text to the old list text? If someone could give me a hint or guide me in the right direction?
//array that holds todo list items
let listItems = [];
//Function will create a new list item based on whatever the input value
//was entered in the text input
function addItem (text) {
const todo = {
text, //whatever user types in
checked: false, //boolean which lets us know if a task been marked complete
id: Date.now(), //unique identifier for item
};
//it is then pushed onto the listItems array
listItems.push(todo);
renderTodo(todo);
}
function checkDone(key) {
//findIndex is an array method that returns position of an element in array
const index = listItems.findIndex(item => item.id === Number(key));
//locates the todo item in the listItems array and set its checked property
//to opposite. 'true' will become 'false'
listItems[index].checked = !listItems[index].checked;
renderTodo(listItems[index]);
}
function deleteTodo(key) {
//find todo object in the listItems array
const index = listItems.findIndex(item => item.id === Number(key));
//create a new object with properties of the current list item
//delete property set to true
const todo = {
deleted: true,
...listItems[index]
};
//remove the list item from the array by filtering it out
listItems = listItems.filter(item => item.id !== Number(key));
renderTodo(todo);
}
//edits list item
function editTodo(key) {
//find todo object in the listItems array
const index = listItems.findIndex(item => item.id === Number(key));
}
//selects form element
const form = document.querySelector('.js-form');
const addGoal = document.getElementById('addBtn');
//adds a submit event listener
function selectForm(event) {
//prevent page refresh on form submission
event.preventDefault();
//select the text input
const input = document.querySelector('.js-todo-input');
//gets value of the input and removes whitespace beginning/end of string
//we then save that to new variable -> text
const text = input.value.trim();
//checks whether 2 operands are not equal, returning true or false (boolean)
//if input value is not equal to blank, add user input
if (text !== '') {
addItem(text);
input.value = ''; //value of text input is cleared by setting it to empty
input.focus(); //focused so user can add many items to list witout focusing the input
}
};
addGoal.addEventListener('click', selectForm, false);
form.addEventListener('submit', selectForm, false);
function renderTodo(todo) {
//saves local storage items, convert listItems array to JSON string
localStorage.setItem('listItemsRef', JSON.stringify(listItems));
//selects the first element with a class of 'js-to'list'
const list = document.querySelector('.js-todo-list');
//selects current todo (refer to top) list item in DOM
const item = document.querySelector(`[data-key='${todo.id}']`);
//refer to function deleteTodo(key)
if (todo.deleted) {
//remove item from DOM
item.remove();
return
}
//use the ternary operator to check if 'todo.checked' is true
//if true, assigns 'done' to checkMarked. if not, assigns empty string
const checkMarked = todo.checked ? 'done' : '';
//creates list 'li' item and assigns it to 'goal'
const goal = document.createElement('li');
//sets the class attribute
goal.setAttribute('class', `todo-item ${checkMarked}`);
//sets the data-key attribute to the id of the todo
goal.setAttribute('data-key', todo.id);
//sets the contents of the list item 'li'
goal.innerHTML = `
<input id="${todo.id}" type="checkbox" />
<label for="${todo.id}" class="tick js-tick"></label>
<span>${todo.text}</span>
<button class="edit-todo js-edit-todo"><i class="fa-solid fa-pencil"></i></button>
<button class="delete-todo js-delete-todo">X</button>
`;
//if item already exists in the DOM
if (item) {
//replace it
list.replaceChild(goal, item);
}else {
//otherwise if it doesnt (new list items) add at the end of the list
list.append(goal);
}
}
//selects entire list
const list = document.querySelector('.js-todo-list');
//adds click event listener to the list and its children
list.addEventListener('click', event => {
if (event.target.classList.contains('js-tick')) {
const itemKey = event.target.parentElement.dataset.key;
checkDone(itemKey);
}
//add this 'if block
if (event.target.classList.contains('js-delete-todo')) {
const itemKey = event.target.parentElement.dataset.key;
deleteTodo(itemKey);
}
})
//render any existing listItem when page is loaded
document.addEventListener('DOMContentLoaded', () => {
const ref = localStorage.getItem('listItemsRef');
if (ref) {
listItems = JSON.parse(ref);
listItems.forEach(t => {
renderTodo(t);
});
}
});
#import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.4;
}
.container {
width: 100%;
max-width: 500px;
margin: 0 auto;
padding-left: 10px;
padding-right: 10px;
color: rgb(43, 43, 43);
height: 90vh;
margin-top: 20vh;
margin-bottom: 5vh;
overflow-y: auto;
}
.app-title {
text-align: center;
margin-bottom: 20px;
font-size: 80px;
opacity: 0.5;
}
.todo-list {
list-style: none;
margin-top: 20px;
}
.todo-item {
margin-bottom: 10px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.todo-item span {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
font-size: 22px;
}
.done span {
background-color:#0099e5;
color:#fff;
}
input[type="checkbox"] {
display: none;
}
#addBtn {
padding: 8px 16px;
font-size:16px;
font-weight:bold;
text-decoration: none;
background-color:#0099e5;
color:#fff;
border-radius: 3px;
border: 3px solid #333;
margin-left:10px;
cursor:pointer;
}
#addBtn:hover {
background-color:#0078b4;
}
.tick {
width: 30px;
height: 30px;
border: 3px solid #333;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.tick::before {
content: '✓';
font-size: 20px;
display: none;
}
.done .tick::before {
display: inline;
}
.delete-todo {
border: none;
font-size:16px;
background-color:red;
color:#fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius:20px;
}
.edit-todo {
border: none;
font-size:16px;
background-color:green;
color:#fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius:20px;
}
.empty-warning {
flex-direction:column;
align-items:center;
justify-content:center;
display:none;
}
.todo-list:empty {
display:none;
}
.todo-list:empty + .empty-warning {
display:flex;
}
.empty-warning-title {
margin-top:15px;
opacity: 0.8;
color: rgb(43, 43, 43);
}
form {
width: 100%;
display: flex;
justify-content: space-between;
margin-left:5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 3px solid #333;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do List</title>
<link rel = "stylesheet" href = "style.css">
<script src="https://kit.fontawesome.com/67e5409c20.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1 class="app-title">To Do List</h1>
<form class="js-form">
<input autofocus type="text" aria-label="Enter a new todo item" placeholder="Ex - Walk the dog" class="js-todo-input">
<input type="button" id="addBtn" value="Add">
</form>
<ul class="todo-list js-todo-list"></ul>
<div class="empty-warning">
<h2 class="empty-warning-title">Add your first goal</h2>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
I added the edit feature to the event handler for click events on the list:
Figure I
if (event.target.matches('.edit-todo') && event.target !== event.currentTarget) {
const text = event.target.previousElementSibling;
text.toggleAttribute('contenteditable');
if (text.contenteditable) {
text.focus();
}
}
Basically, when the user clicks an edit button the contenteditable attribute is toggled (true/false) on the <span> that sits right before the button (hence .previousElementSibling).
I also added 2 CSS rulesets as well:
Figure II
.fa-pencil { pointer-events: none }
[contenteditable] { outline: 3px inset blue }
For some reason my mouse cannot click font-awesome icons, I have no idea why. So I disabled click events on the edit icon in order to click the edit button. Others might have the same problem as I do -- I'm 99% sure there's no harm in keeping that ruleset since it just makes the edit button 100% the origin element on the event chain. The second ruleset is a visual cue to the user that the <span> is editable.
let listItems = [];
function addItem(text) {
const todo = {
text,
checked: false,
id: Date.now(),
};
listItems.push(todo);
renderTodo(todo);
}
function checkDone(key) {
const index = listItems.findIndex(item => item.id === Number(key));
listItems[index].checked = !listItems[index].checked;
renderTodo(listItems[index]);
}
function deleteTodo(key) {
const index = listItems.findIndex(item => item.id === Number(key));
const todo = {
deleted: true,
...listItems[index]
};
listItems = listItems.filter(item => item.id !== Number(key));
renderTodo(todo);
}
function editTodo(key) {
const index = listItems.findIndex(item => item.id === Number(key));
}
const form = document.querySelector('.js-form');
const addGoal = document.getElementById('addBtn');
function selectForm(event) {
event.preventDefault();
const input = document.querySelector('.js-todo-input');
const text = input.value.trim();
if (text !== '') {
addItem(text);
input.value = '';
input.focus();
}
};
addGoal.addEventListener('click', selectForm, false);
form.addEventListener('submit', selectForm, false);
function renderTodo(todo) {
// localStorage.setItem('listItemsRef', JSON.stringify(listItems));
const list = document.querySelector('.js-todo-list');
const item = document.querySelector(`[data-key='${todo.id}']`);
if (todo.deleted) {
item.remove();
return
}
const checkMarked = todo.checked ? 'done' : '';
const goal = document.createElement('li');
goal.setAttribute('class', `todo-item ${checkMarked}`);
goal.setAttribute('data-key', todo.id);
goal.innerHTML = `
<input id="${todo.id}" type="checkbox" />
<label for="${todo.id}" class="tick js-tick"></label>
<span>${todo.text}</span>
<button class="edit-todo js-edit-todo"><i class="fa-solid fa-pencil"></i></button>
<button class="delete-todo js-delete-todo">X</button>
`;
if (item) {
list.replaceChild(goal, item);
} else {
list.append(goal);
}
}
const list = document.querySelector('.js-todo-list');
list.addEventListener('click', function(event) {
if (event.target.classList.contains('js-tick')) {
const itemKey = event.target.parentElement.dataset.key;
checkDone(itemKey);
}
if (event.target.classList.contains('js-delete-todo')) {
const itemKey = event.target.parentElement.dataset.key;
deleteTodo(itemKey);
}
if (event.target.matches('.edit-todo') && event.target !== event.currentTarget) {
const text = event.target.previousElementSibling;
text.toggleAttribute('contenteditable');
if (text.contenteditable) {
text.focus();
}
}
})
/*
document.addEventListener('DOMContentLoaded', () => {
const ref = localStorage.getItem('listItemsRef');
if (ref) {
listItems = JSON.parse(ref);
listItems.forEach(t => {
renderTodo(t);
});
}
});*/
#import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.4;
}
.container {
width: 100%;
max-width: 500px;
margin: 0 auto;
padding-left: 10px;
padding-right: 10px;
color: rgb(43, 43, 43);
height: 90vh;
margin-top: 20vh;
margin-bottom: 5vh;
overflow-y: auto;
}
.app-title {
text-align: center;
margin-bottom: 20px;
font-size: 80px;
opacity: 0.5;
}
.todo-list {
list-style: none;
margin-top: 20px;
}
.todo-item {
margin-bottom: 10px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.todo-item span {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
font-size: 22px;
}
.done span {
background-color: #0099e5;
color: #fff;
}
input[type="checkbox"] {
display: none;
}
#addBtn {
padding: 8px 16px;
font-size: 16px;
font-weight: bold;
text-decoration: none;
background-color: #0099e5;
color: #fff;
border-radius: 3px;
border: 3px solid #333;
margin-left: 10px;
cursor: pointer;
}
#addBtn:hover {
background-color: #0078b4;
}
.tick {
width: 30px;
height: 30px;
border: 3px solid #333;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.tick::before {
content: '✓';
font-size: 20px;
display: none;
}
.done .tick::before {
display: inline;
}
.delete-todo {
border: none;
font-size: 16px;
background-color: red;
color: #fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 20px;
}
.edit-todo {
border: none;
font-size: 16px;
background-color: green;
color: #fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 20px;
}
.empty-warning {
flex-direction: column;
align-items: center;
justify-content: center;
display: none;
}
.todo-list:empty {
display: none;
}
.todo-list:empty+.empty-warning {
display: flex;
}
.empty-warning-title {
margin-top: 15px;
opacity: 0.8;
color: rgb(43, 43, 43);
}
form {
width: 100%;
display: flex;
justify-content: space-between;
margin-left: 5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 3px solid #333;
}
.fa-pencil {
pointer-events: none
}
[contenteditable] {
outline: 3px inset blue
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do List</title>
<link rel="stylesheet" href="style.css">
<script src="https://kit.fontawesome.com/67e5409c20.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1 class="app-title">To Do List</h1>
<form class="js-form">
<input autofocus type="text" aria-label="Enter a new todo item" placeholder="Ex - Walk the dog" class="js-todo-input">
<input type="button" id="addBtn" value="Add">
</form>
<ul class="todo-list js-todo-list"></ul>
<div class="empty-warning">
<h2 class="empty-warning-title">Add your first goal</h2>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
I have a simple note taking web application. Each time the user clicks on the '+' button a new note is added. Once he clicks on the '⟳' button all are excluded. I want to add five new item slots in the note that the user clicked. In order to do that I need to know which note he did so. This last bit is the one confusing me. How can I know which button the user clicked if all of the buttons are generated by the user?
HTML:
<html lang="en">
<head>
<title>To Do Lists</title>
<link rel="stylesheet" href="styles.css" />
<script src="script.js"></script>
</head>
<body>
<div class="top_bar">
<button id="plus">+</button>
<button id="restart">⟳</button>
</div>
<div id="notes" class="notes">
</div>
</body>
</html>
CSS
padding: 0px;
margin: 0px;
}
body{
display: flex;
flex-direction: column;
}
.top_bar{
width: 100%;
height: 10vh;
background-color: #95E29B;
display: flex;
align-items: center;
justify-content: space-between;
}
button{
font-size: 35px;
border: none;
width: 15%;
height: 10vh;
background-color: #3BCE4B;
cursor: pointer;
}
.notes{
width: 100%;
height: 90vh;
overflow: auto;
background-color: black;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.note{
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
height: 40vh;
width: 30%;
border-radius: 10px;
background-color: white;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-end;
}
.note_input{
margin-top: 20px;
margin-right: 5%;
font-size: 30px;
width: 90%;
border-style: solid;
border-top: none;
border-left: none;
border-right: none;
border-color: black;
}
form{
margin-top: 10px;
margin-right: 15%;
width: 80%;
height: 49%;
overflow-y: auto;
}
li{
border: none;
width: 70%;
display: flex;
}
.li_input{
border-style: solid;
border-color: black;
border-top: none;
border-left: none;
border-right: none;
margin-left: 10px;
font-size: 20px;
}
.more_items{
width: 35px;
height: 35px;
margin-right: 2%;
border-radius: 100%;
font-size: 20px;
}
JavaScript
const add_note = () => {
// Creates a new note and its props
const new_note = document.createElement("div");
const new_input = document.createElement("input");
const new_form = document.createElement("form");
const new_ol = document.createElement("ol");
const new_button = document.createElement("button");
//Populates the new note with inputs and checkboxes
for (let i = 0; i < 5; i++) {
let new_li = document.createElement("li");
let new_checkbox = document.createElement("input");
new_checkbox.setAttribute("type", "checkbox");
let new_li_input = document.createElement("input");
new_li_input.classList.add("li_input");
new_ol.appendChild(new_li);
new_li.appendChild(new_checkbox);
new_li.appendChild(new_li_input);
}
//New note's settings
new_note.classList.add("note");
new_note.appendChild(new_input);
new_input.classList.add("note_input");
new_input.setAttribute("placeholder", "Note's title");
new_note.appendChild(new_form);
new_ol.classList.add("ols");
new_form.appendChild(new_ol);
new_note.appendChild(new_button);
new_button.classList.add("more_items");
//Inserts the new note and button
const note_block = document.getElementById("notes");
note_block.appendChild(new_note);
new_button.addEventListener("click", add_more_items);
new_button.innerHTML = "+";
};
//Adds more items
const add_more_items = () => {
//console.log(new_button.parentElement.nodeName);
//let new_ol = document.getElementsByClassName("ols")[];
for (let i = 0; i < 5; i++) {
let new_li = document.createElement("li");
let new_checkbox = document.createElement("input");
new_checkbox.setAttribute("type", "checkbox");
let new_li_input = document.createElement("input");
new_li_input.classList.add("li_input");
new_ol.appendChild(new_li);
new_li.appendChild(new_checkbox);
new_li.appendChild(new_li_input);
}
};
//Removes all notes
const remove_note = () => {
let amount_of_notes = document.getElementsByClassName("note").length;
console.log(amount_of_notes);
while (amount_of_notes != 0) {
amount_of_notes--;
document.getElementsByClassName("note")[amount_of_notes].remove();
}
alert("All notes were removed.");
};
// Loads the buttons
const load_buttons = () => {
document.getElementById("plus").addEventListener("click", add_note);
document.getElementById("restart").addEventListener("click", remove_note);
};
// Main method
document.addEventListener("DOMContentLoaded", () => {
load_buttons();
});
Given your html is fairly simple, you can do this in a rudimentary style by making use of parentNode.
Your current code is erroring because you're trying to target new_ol to add the fresh <li> elements but it doesn't exist in scope of the add_more_items function. And even if it did, it would be ambiguous - which <ol> should it refer to?
Instead, you can work out the parent <ol> from the clicked button, like so:
const add_more_items = (e) => {
const new_ol = e.target.parentNode.querySelector('ol');
// rest of your code
}
Here's a full snippet putting all that together. I've put it in a codepen as the snippet editor here struggled with some parts of your layout: https://codepen.io/29b6/pen/qBxxXqG
Bear in mind that traversing the DOM like this isn't ideal. The main problem with this approach is that if your HTML structure changes, you can end up chaining multiple parentNodes together which gets ugly fast. But it should help you understand the concept of selecting an element's parent like you asked.
So i am making a planning app where people can add and remove tasks. Now i want to add one last function to this app and that is when someone creates a new task. The cursor automaticaly jumps to the beginning of the Task title. This way it will be way more clear for the user that they can edit the text on thier task cards. This action should always trigger when a new task is created and the cursor should always jump to the last task added.
// Add button
const AddToDoButton = document.getElementById('new-to-do');
// Columns
const ToDoColumn = document.getElementById('to-do-column');
const InProgressColumn = document.getElementById('in-progress-column');
const CompleteColumn = document.getElementById('complete-column');
// all columns
const Columns = document.querySelectorAll('.task-column');
// selecting all tasks and initial function call
var Tasks = document.querySelectorAll('.task');
NewTasksCheck();
//example task initial function call
AddNewTask();
// ---------------------------
// Add new task structure
// ---------------------------
AddToDoButton.addEventListener('click', AddNewTask);
function AddNewTask(){
// all HTML task parts
const NewTaskContainer = document.createElement('div');
const NewTaskHead = document.createElement('div');
const NewTaskName = document.createElement('h3');
const NewTaskDeleteButton = document.createElement('button');
const NewTaskDescription = document.createElement('p');
// adding drag
NewTaskContainer.setAttribute('draggable', 'true');
// making content editable
NewTaskName.setAttribute('contenteditable', 'true');
NewTaskDescription.setAttribute('contenteditable', 'true');
// hover change cursor to pointer
NewTaskDeleteButton.addEventListener('mouseover', () => {
NewTaskDeleteButton.style.cursor = 'pointer';
})
NewTaskName.addEventListener('mouseover', () => {
NewTaskName.style.cursor = 'text';
});
NewTaskDescription.addEventListener('mouseover', () => {
NewTaskDescription.style.cursor = 'text';
});
// delete function call
NewTaskDeleteButton.addEventListener('click', function(){
NewTaskContainer.parentNode.removeChild(NewTaskContainer);
});
// adding all the classes
NewTaskContainer.classList.add('task');
NewTaskHead.classList.add('task-head');
NewTaskDeleteButton.classList.add('delete-task');
NewTaskDescription.classList.add('task-description');
// adding all the content
NewTaskName.innerHTML = "Your task name";
NewTaskDescription.innerHTML = "Your description"
NewTaskDeleteButton.innerHTML = "X"
// HTML structure
ToDoColumn.appendChild(NewTaskContainer);
NewTaskContainer.appendChild(NewTaskHead);
NewTaskHead.appendChild(NewTaskName);
NewTaskHead.appendChild(NewTaskDeleteButton);
NewTaskContainer.appendChild(NewTaskDescription);
NewTasksCheck();
}
// ---------------------------
// dragable functionality
// ---------------------------
function NewTasksCheck(){
Tasks = document.querySelectorAll('.task');
Tasks.forEach(draggable => {
draggable.addEventListener('dragstart', () => {
draggable.classList.add('dragging');
// console.log("dragstart");
});
draggable.addEventListener('dragend', () => {
draggable.classList.remove('dragging');
// console.log("dragend");
});
});
Columns.forEach(Column => {
Column.addEventListener('dragover', (e) => {
e.preventDefault();
const draggable = document.querySelector('.dragging');
Column.appendChild(draggable);
});
});
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
header {
height: 8vh;
background-color: rgb(175, 175, 175);
display: flex;
align-items: center;
justify-content: space-between;
}
header > h1 {
margin-left: 50px;
}
main {
background-color: rgb(206, 234, 243);
display: flex;
justify-content: space-around;
}
.task-column {
background-color: lightcoral;
min-height: fit-content;
margin: 10px;
width: 400px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 10px;
}
.task-column > * {
margin: 10px;
}
.task {
background-color: white;
padding: 10px;
width: 90%;
border-radius: 10px;
cursor: move;
}
.task-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.1rem;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: solid black 1px;
}
.delete-task {
background-color: red;
border: none;
height: 30px;
width: 30px;
border: solid black 1px;
border-radius: 50%;
font-size: 1.1rem;
font-weight: bold;
}
.add-to-do {
margin-right: 50px;
padding: 15px 30px;
border-radius: 10px;
border: solid black 1px;
background-color: rgb(140, 233, 2);
font-size: 1.1rem;
font-weight: bold;
transition: ease-in-out 100ms;
}
.add-to-do:hover {
transform: scale(110%);
}
.add-to-do:active {
background-color: darkseagreen;
}
.task.dragging {
opacity: 0.5;
}
<header>
<h1>planning</h1>
<button id="new-to-do" class="add-to-do">Add To Do</button>
</header>
<main>
<div id="to-do-column" class="task-column">
<h2>To Do</h2>
</div>
<div id="in-progress-column" class="task-column">
<h2>In Progress</h2>
</div>
<div id="complete-column" class="task-column">
<h2>Complete</h2>
</div>
</main>
<script src="script.js"></script>
</body>
</html>
<!-- example task structure -->
<!-- <div draggable="true" class="task">
<div class="task-head">
<h3 contenteditable="true">Task name</h3>
<button class="delete-task">X</button>
</div>
<p contenteditable="true" class="task-description">
Task description
</p>
</div> -->
You can call focus after the NewTaskName appended
NewTaskName.focus();
"use strict";
const openModal = document.querySelector('[data-create]');
const exitModal = document.querySelector('[data-close]');
const saveBtn = document.querySelector('#save');
openModal.addEventListener('click', showModal);
exitModal.addEventListener('click', closeModal);
saveBtn.addEventListener('click', saveCard);
/*
Detects if text has been inputed. If not, an error is shown
if yes the createFlashCard and closed modal function are called.
*/
function saveCard() {
let questionArea = document.querySelector('#question');
let answerArea = document.querySelector('#answer');
let showAlert = document.querySelector('.show-error-message');
if (questionArea = null || questionArea.value == '') {
if (answerArea = null || answerArea.value == '') {
showAlert.classList.remove("hide-error");
}
setTimeout(() => {
showAlert.classList.add("hide-error");
}, 3000);
} else {
closeModal();
createFlashCard();
}
}
/*
Removes the is-hidden css class to open modal
*/
function showModal(e) {
let modal = document.querySelector('.modal-design');
modal.classList.remove("is-hidden");
}
/*
Adds the is-hidden css class to close modal
*/
function closeModal() {
let modal = document.querySelector('.modal-design');
modal.classList.add('is-hidden');
}
/*
Creates a flash card using input string values.
Then renders a card using the .innerHTML property.
Each card rendered will be clickable to show the answer.
*/
function createFlashCard() {
let questionText = document.querySelector('.question-text').value;
let answerText = document.querySelector('.answer-text').value;
let cardSection = document.querySelector('.card-container');
let createArticle = document.createElement('article');
createArticle.className += "card";
createArticle.innerHTML = `
<div class="card-question-button">
<h4 id="title">${questionText}</h4>
<button id="show">></button>
</div>
<div id="answer-card">
<p id="answer-card-p">${answerText}</p>
</div>`;
cardSection.appendChild(createArticle);
openCloseCards();
}
function openCloseCards() {
let buttons = document.querySelectorAll('.card-question-button');
buttons.forEach(function (btn) {
btn.addEventListener('click', function (e) {
let questions = e.currentTarget.parentElement;
questions.classList.toggle("show-text");
})
})
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
line-height: 1.5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/*
variables
*/
:root {
--primary-color:#5dcbd8;
--secondary-color: hsl(186, 100%, 94%);
--third-color: #F6F6F8;
--fourth-color: #fff;
--button-border: none;
--error-color: hsl(0deg 58% 70%);
--shadow: 0px 2px 3px 0 hsla(0 , 0%, 0%, 0.2);
}
header {
padding: 15px;
color: var(--third-color);
height: 100px;
background-color: var(--primary-color);
}
/**
Prompt question card
*/
.prompt-question {
display: flex;
padding: 15px;
align-items: center;
justify-content: space-around;
}
#create {
border: var(--button-border);
border-radius: 5px;
background-color: var(--secondary-color);
height: 60px;
width: 70px;
font-size: 25px;
transition: .3s ease-in-out;
}
#create:hover {
cursor: pointer;
background-color: #b5f0f7;
}
/*
Modal Design
*/
.modal-placement{
display: flex;
justify-content: center;
}
.modal-design {
width: 600px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #F6F6F8;
box-shadow:var(--shadow);
border-radius: 4px;
margin: 30px;
}
.is-hidden {
visibility: hidden;
opacity: 1;
}
.erase-modal-c {
display: flex;
justify-content: flex-end;
}
#erase {
background-color:var(--error-color);
display: flex;
justify-content: center;
border: none;
padding: 5px;
height: 25px;
width: 25px;
}
#close {
margin-top: -8px;
margin-left: 3px;
font-size: 20px;
transform: rotate(45deg);
}
#erase:hover {
cursor: pointer;
}
h3 {
padding-top: 15px;
}
/**
Textarea design
*/
#question, #answer {
height: 90px;
}
textarea {
font-size: 15px;
padding: 4px;
resize: vertical;
}
#save {
border: var(--button-border);
margin-top: 25px;
height: 45px;
font-size: 15px;
background-color:var(--primary-color);
color: var(--third-color);
}
#save:hover {
cursor: pointer;
background-color: #90d8e0;
}
.show-error-message {
background-color: var(--error-color);
color: var(--fourth-color);
margin-top: 15px;
padding: 10px;
text-align: center;
}
.hide-error {
visibility: hidden;
opacity: 0;
}
/*
Card container
*/
.card-container {
display: grid;
background-color: var(--third-color);
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
margin-top: 40px;
}
#title {
margin-left: 10px;
}
.card {
display: flex;
margin: 20px;
flex-direction: column;
align-items: center;
background-color:var(--fourth-color);
padding: 7px;
justify-content: space-between;
box-shadow:var(--shadow);
border-radius: 5px;
transition: .3s ease-in-out;
z-index: 1000;
}
.card-question-button {
display: flex;
justify-content: space-between;
padding: 5px;
width: 100%;
}
.card-question-button:hover {
cursor: pointer;
}
/*
Answer card
*/
#answer-card {
display: none;
border-top: 1px solid rgba(0, 0, 0, 0.2);
padding: 15px;
text-align: left;
}
#answer-card-p {
text-align: left;
}
.show-text #answer-card {
display: block;
}
#show {
border: none;
background-color: var(--fourth-color);
font-size: 20px;
transition: .2s ease-in-out;
}
#show:hover {
cursor:pointer;
transform: translateX(-2px);
}
/* Media Queries */
#media screen and (max-width: 800px) {
.prompt-question {
box-shadow:var(--shadow);
border-radius: 4px;
margin: 20px;
}
.card-container {
grid-template-columns: 1fr;
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<h1>Flash Cards +</h1>
</header>
<article class="prompt-question">
<button id="create" data-create>+</button>
<p class="info">
Create a new card
</p>
</article>
<article class="modal-placement">
<section class="modal-design is-hidden">
<div class="erase-modal-c">
<button id="erase" data-close>
<p id="close">+</p>
</button>
</div>
<h3>Question</h3>
<textarea class="question-text textA" id="question"></textarea>
</div>
<h3>Answer</h3>
<textarea class="answer-text textA" id="answer"></textarea>
<button id="save">Save</button>
<div class="show-error-message hide-error">Please Submit Values</div>
</section>
</article>
<section class="card-container">
</section>
<script src="js/script.js"></script>
</html>
This is a flashcard project I am building. I am fairly new to programming and was looking for help on this particular bug that I cannot wrap my head around. Included in the gist is the css, html, and javascript code. Any tips on code structure is also appreciated.
Question:
When I dynamically create cards using JavaScript I want to show open and close behavior for each of them. The first card created does open and close as expected. However, the following cards do not. For example, one stays open while the other one closes.
I would like each card to open and close independently when clicked after dynamically being created. It seems the behavior is depending on the previous card.
When you call createFlashCard() The html that is generated:
<div class="card-question-button">
<h4 id="title">${questionText}</h4>
<button id="show">></button>
</div>
<div id="answer-card">
<p id="answer-card-p">${answerText}</p>
</div>
These elements will have the same ID's used more than once in a page when they should be unique. Add a unique identifier or remove the id attribute if its not needed. For example:
let cardCount=0;
function createFlashCard() {
cardCount++;
let questionText = document.querySelector('.question-text').value;
let answerText = document.querySelector('.answer-text').value;
let cardSection = document.querySelector('.card-container');
let createArticle = document.createElement('article');
createArticle.className += "card";
let title="title"+ cardCount;
let buttonID="show"+cardCount;
...
<button id=${buttonID}>
...
}
When you are executing your openCloseCards function, you are creating multiple event listeners based on the number of items in buttons.
I'd recommend adding the event listener in your createFlashCard function and removing the function definition of openCloseCards and it's call.
With this change the code seems to working as it should.
function createFlashCard() {
let questionText = document.querySelector('.question-text').value;
let answerText = document.querySelector('.answer-text').value;
let cardSection = document.querySelector('.card-container');
let createArticle = document.createElement('article');
createArticle.className += "card";
createArticle.innerHTML = `
<div class="card-question-button">
<h4 id="title">${questionText}</h4>
<button id="show">></button>
</div>
<div id="answer-card">
<p id="answer-card-p">${answerText}</p>
</div>`;
cardSection.appendChild(createArticle);
createArticle.addEventListener('click', function(e) {
let questions = e.target.parentElement;
questions.classList.toggle("show-text");
console.log(questions.classList)
})
}