I'm currently missing something about how querySelectorAll is working in my code. I'm using it to loop through and set event listeners of all tds in a table in order to grab reference to whichever one is clicked so that I can modify that td only. However in implementing a side functionality, I have run into a problem that I don't understand. It appears that even though each td has a class, and the class is given a background-color style via CSS, the value is considered blank unless I manually set it (via my paintColor function). Console.log reveals that the value is blank until its expressly set. Why?
Thanks
HTML
<table class="table">
<!-- Row 1 -->
<tr class="row">
<td class="cell"></td>
<td class="cell"></td>
<div class="menus">
<div class="menu-left">
<input type="checkbox" id="eye-check" unchecked/>
<span>Eyedropper</span>
<table class="eye-table">
<td class="eye-cell">
<span class="tooltip">Hexidecimal value of selected eyedropper
color.</span>
</td>
</table>
<div class="menu-left-text">
<p class="eye-text">rgb(255,255,255)</p>
</div>
</div>
CSS
.cell {
border: 1px solid lightgrey;
/* box-shadow: 1.5px 1.5px 1.5px lightgrey; */
background-color: #ffffff;
width: 16px;
height: 16px;
cursor: crosshair;
z-index: 4;
}
JS
// Gloabal Variables
let cells = document.querySelectorAll(".cell");
let activeColor = "rgb(255,255,255)";
let table = document.querySelector("table.table");
let activeCell = document.querySelector(".cell");
let dropper;
let eyeCell = document.querySelector(".eye-cell");
let eyeText = document.querySelector(".eye-text");
// Listen for EyeDropper
document.querySelector("#eye-check").addEventListener("click", evt =>{
dropper = document.querySelector("#eye-check");
if (dropper.checked) {
// Set Dropper Indicator on Table Border
table.style.border = "5px dashed black";
} else {
// Set Paint Indicator on Table Border
table.style.border = "5px solid silver";
}
});
// Listen for Clicks
for (let i = 0; i < cells.length; i++) {
cells[i].addEventListener("click", evt => {
activeCell = cells[i];
// Check for EyeDropper Toggle
checkDropper();
});
}
// Check for Eye Dropper Checked
function checkDropper() {
dropper = document.querySelector("#eye-check");
if (dropper.checked) {
activateDropper();
} else {
activatePaint();
}
}
// Dropper is Active, Activate Dropper
function activateDropper() {
// Set EyeDropper Color
eyeCell.style.backgroundColor = activeCell.style.backgroundColor;
// Set Text Value
eyeText.innerHTML = activeCell.style.backgroundColor.replace(/\s
/g,"");}
// Dropper is Inactive, Activate Painting
function activatePaint() {
// Get Active Color
getActiveColor();
// Paint Color into Cell
paintColor();
}
// Get Brush Color
function getActiveColor() {
activeColor = document.querySelector("#brush-color").value;
}
// Paint Color into active cell
function paintColor() {
activeCell.style.backgroundColor = activeColor;
}
Click here
I believe what is happening here is that the Node object doesn't have styles explicitly set on it and that's why you are getting the empty string when accessing the obj.style.whatever. HTML Node objects and CSS are separate entities and the CSS is telling the browser (not the Node) to change its style. If you set your CSS inline on the element, it should show when accessing it the way you do.
See here: http://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
Related
I am new to programming and this is my first question. The problem I am having is I am trying to use DOM manipulation on all the child nodes of an html collection. I am expecting the nodes to change background color when they are hovered. Here is what I have tried so far:
let x = 0;
do{
const square = document.createElement("div");
square.className = "squares";
square.setAttribute("id","block");
document.getElementById("container").appendChild(square);
x++;
}
while(x < 16);
var container = document.getElementById("container");
var cells = container.childNodes;
cells.forEach(function(){
cells.onmouseover = function(){
cells.style.backgroundColor = "black";
}
});
console.log(`${cells.length}`);
This doesn't work even though console.log shows 16 child nodes being targeted.
var container = document.getElementById("container");
var cells = container.children[0];
cells.onmouseover = function(){
cells.style.backgroundColor = "black";
}
I have tried this and can use index but of course only that cell will change bg color. I want any cell that is hovered to change also.
I am at a loss for what I am doing wrong here. If anyone can point me in the right direction I would greatly appreciate it.
Welcome to Stack Overflow.
There is an issue in your forEach cycle. Consider the following:
cells.forEach(cell => {
cell.onmouseover = () => {
cell.style.backgroundColor = "black"
}
})
Note that you need to refer to cycle variable instead of the cells array.
Instead of attaching listeners to all the squares you can use event delegation and just have one listener on the container that captures the events from its children as they "bubble up" the DOM.
// Cache the container element, and add a listener to it
const container = document.querySelector('.container');
container.addEventListener('mouseover', handleMouse);
// Create some squares HTML by pushing template
// strings into an array
const html = [];
for (let i = 1; i < 10; i++) {
html.push(`<div class="square">${i}</div>`);
}
// Add that HTML to the container making sure
// we join the array of strings into one string
container.innerHTML = html.join('');
// When a event is fired check that it was
// was from an element with a square class
// and then add an active class to it
function handleMouse(e) {
if (e.target.matches('.square')) {
e.target.classList.add('active');
}
}
.container { display: grid; grid-template-columns: repeat(3, 50px); grid-gap: 0.2em; }
.square { font-size: 1.2em; padding: 0.7em 0.2em; background-color: #565656; color: white; text-align: center; }
.square.active { background-color: thistle; color: black; cursor: pointer; }
<div class="container"></div>
Additional documentation
Template/string literals
In this program, I'm able to add inputs with a button but I need to show the length of each input as it changes. I'm able to get the length using an EventListener, but I'm not sure how to change the text value for any newly created buttons.
On line 12, you can see that I'm able to change the value successfully on the first input but I'm using an html variable. If you look at my addCell() function, you'll see that I have an element as a child of each node to keep track of the length of each input. I need to access that element in my change() function so I can set the event.target.value.length to the corresponding nodes child element.
I've tried using this, setting var x = this and I've tried using the event.target properties to find the corresponding node and even innerHTML.
var i = 0;
var count = 1;
var length = 2;
var chars = 0;
document.addEventListener('input', function (evt) {
change(evt);
});
function change(elem) {
var check = document.getElementById("first");
if (event.target == check) {
document.getElementById("len").innerHTML = event.target.value.length;
return;
}
// Here's where I'm stuck
}
function removeCell() {
if (count <= 1) {
alert("Illegal operation, the police have been notified.")
return;
}
var elem = document.getElementById('main');
elem.removeChild(elem.lastChild);
count = count - 1;
length = length - 1;
}
function addCell() {
var node = document.createElement('div');
node.innerHTML += length;
var inp = document.createElement('INPUT');
var size = document.createElement('size');
inp.setAttribute("type", "text");
node.appendChild(inp);
node.appendChild(size);
document.getElementById('main').appendChild(node);
count += 1;
length += 1;
i += 1;
}
#area {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
#texts {
width: 220px;
height: 50px;
border-style: solid;
}
body {
background-color: grey;
}
<div id="area">
<form id="main">
<pre><b> input </b> length</pre>
<span id="list">
1<input type="text" id="first"> <var id="len"></var>
</span>
</form>
<br />
<button onclick="addCell()">Add Cell</button>
<button onclick="removeCell()">Remove Cell</button>
<button onclick="sort()">Sort</button>
</div>
Since I'm able to use alert() to show me the correct length of each newly created input each time it changes, I know there's a way to access the "size" element I created to update it using event.target.value.length
Your problem is that you use a "global" input event listener and your change() function is not programmed to handle multiple input fields because in it you are querying known element ids first and len.
If you want to go with a global listener you have to tell your change() function how to access the new input and corresponding target fields.
An easier way is that you modify your addCell() function and attach an event listener to the input field that you are creating instead of using a global one. Thereby each input field holds its own event listener. Since both the input field and your size element, which displays the length of the input value, are created in the same scope you can use easily write the length to the corresponding size element.
inp.addEventListener('input', function(){
size.innerText = inp.value.length;
});
If you want this to work with your provided HTML you need to remove your first input field and call addCell() manually so that your initial input gets rendered.
Your code should then look like this (note: I set var count = 0; and var length = 1;):
var i = 0;
var count = 0;
var length = 1;
var chars = 0;
function removeCell() {
if (count <= 1) {
alert("Illegal operation, the police have been notified.")
return;
}
var elem = document.getElementById('main');
elem.removeChild(elem.lastChild);
count = count - 1;
length = length - 1;
}
function addCell() {
var node = document.createElement('div');
node.innerHTML += length;
var inp = document.createElement('INPUT');
var size = document.createElement('size');
inp.setAttribute("type", "text");
inp.addEventListener('input', function(){
size.innerText = inp.value.length;
});
node.appendChild(inp);
node.appendChild(size);
document.getElementById('main').appendChild(node);
count += 1;
length += 1;
i += 1;
}
addCell();
#area {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
#texts {
width: 220px;
height: 50px;
border-style: solid;
}
body {
background-color: grey;
}
<div id="area">
<form id="main">
<pre><b> input </b> length</pre>
<span id="list"></span>
</form>
<br />
<button onclick="addCell()">Add Cell</button>
<button onclick="removeCell()">Remove Cell</button>
<button onclick="sort()">Sort</button>
</div>
If HTML layout is planned out and is consistent you can use [name] attribute for form controls and .class or even just the tagName. Use of #id when dealing with multiple tags is difficult and unnecessary. Just in case if you weren't aware of this critical rule: #ids must be unique there cannot be any duplicate #ids on the same page. Having duplicate #ids will break JavaScript/jQuery 90% of the time.
To accessing tags by .class, #id, [name], tagName, etc. use document.querySelector() and document.querySelectorAll() for multiple tags.
To access forms and form controls (input, output, select, etc) by [name] or #id use the HTMLFormElement and HTMLFormControlsCollection APIs.
.innerHTML is destructive as it overwrites everything within a tag. .insertAdjacentHTML() is non-destructive and can place an htmlString in 4 different positions in or around a tag.
Event handlers and event listeners work only on tags that were initially on the page as it was loaded. Any tags dynamically added afterwards cannot be registered to listen/handle events. You must delegate events by registering an ancestor tag that's been on the page since it was loaded. This was done with delRow() since the buttons are dynamically created on each row (changed it because one delete button that removes the last row isn't that useful. ex. 7 rows and you need to delete 4 rows just to get to the third row).
Here's a breakdown of: [...ui.len] ui references all form controls .len is all tags with the [name=len]. The brackets and spread operator converts the collection of len tags to an array.
There's no such thing as <size></size>. So document.createElement('size') is very wrong.
const main = document.forms.main;
main.oninput = count;
main.elements.add.onclick = addRow;
document.querySelector('tbody').onclick = delRow;
function count(e) {
const active = e.target;
const ui = e.currentTarget.elements;
const row = active.closest('tr');
const idx = [...row.parentElement.children].indexOf(row);
const length = [...ui.len][idx];
length.value = active.value.length;
return false;
}
function addRow(e) {
const tbody = document.querySelector('tbody');
let last = tbody.childElementCount+1;
tbody.insertAdjacentHTML('beforeend', `<tr><td data-idx='${last}'><input name='txt' type="text"></td><td><output name='len'>0</output></td><td><button class='del' type='button'>Delete</button></td>`);
return false;
}
function delRow(e) {
if (e.target.matches('.del')) {
const row = e.target.closest('tr');
let rows = [...row.parentElement.children];
let qty = rows.length;
let idx = rows.indexOf(row);
for (let i = idx; i < qty; i++) {
rows[i].querySelector('td').dataset.idx = i;
}
row.remove();
}
return false;
}
body {
background-color: grey;
}
#main {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
tbody tr td:first-of-type::before {
content: attr(data-idx)' ';
}
<form id="main">
<table>
<thead>
<tr>
<th class='txt'>input</th>
<th class='len'>length</th>
<th><button id='add' type='button'>Add</button></th>
</tr>
</thead>
<tbody>
<tr>
<td data-idx='1'><input name='txt' type="text"></td>
<td><output name='len'>0</output></td>
<td><button class='del' type='button'>Delete</button></td>
</tr>
</tbody>
</table>
<!--These are dummy nodes because of the
HTMLFormControlsCollection API ability to use id or name, there
must be at least 2 tags with the same name in order for it to
be considered iterable-->
<input name='txt' type='hidden'>
<input name='len' type='hidden'>
</form>
I'm trying to select an item in a DOM draggable list so that I can move it to another field with a button. Is there a mouseDown or similar function to mark the selected item?
var nmbrCustomers = 10; // Enter the number of customers!
var customerArray = new Array(nmbrCustomers);
for (var i = 1;i < nmbrCustomers + 1; i++){
customerArray[i] = "f_" + i
}
customerArray.forEach(myFunc);
function myFunc(item,index){
console.log(document.getElementById(item).innterHTML);
}
With this code I can reach all the elements I want, but I don't know how to make them selected/marked. The results from the above code gives the following code for each element:
<td class="menuGroup" nowrap="nowrap">
<img id="someimageID" class="navigatorGroupImage" align="left"
src="someimageDir" onclick="toggleGroup(9)"
ondragstart="dragStartGroupIcon(event)" alt="Alternar expandir/colapsar">
<span id="someID"
class="navigatorGroupText" draggable="true"
onmousedown="mouseDownGroup('f_9', Event.extend(event))"
onmouseup="mouseUpGroup('f_9', Event.extend(event))"
onmousemove="mouseMoveGroup('f_9', Event.extend(event))"
title="someName.">
</span>
</td>
You can dynamically add and remove a CSS class to the dragged element. The following code snippet will work if you add a draggable CSS class to the elements you want to drag - change the CSS rule to suit your requirements.
let sources = document.querySelectorAll('.draggable');
sources.forEach(source => {
source.addEventListener('dragstart', dragStart);
source.addEventListener('dragend', dragEnd);
});
function dragStart(e) {
// Add a 'dragging' CSS class to the element
this.classList.add('dragging');
}
function dragEnd(e) {
// Remove the 'dragging' CSS class
this.classList.remove('dragging');
}
.dragging {
opacity: .50;
border: 6px solid #000;
}
when I assign the
// psuedo code
.innerHTML = [ a button here ]; // see exact code below
it does nothing when clicked. Is there an alternative to doing this? or is it not possible?
the table is dynamically created by JavaScript. As each cell is populated, a criterion is checked - and if met, will produce a button in the following cell. this works and does as intended, however, the button does nothing when clicked.
When checking the source, the button does not show, even though it is there on the HTML page.
I tried elements, that did not work, and I think it is because it is handled outside of the table, where this code is internal.
// the culprit line
cell = row.insertCell(-1); cell.innerHTML = '<button class="calc" onclick="calculateGrade();"> + </button>';
Not sure if I need to post more code, but this line should show what I am attempting to do.
the button has to be done dynamically since it checks each row.
For educational purposes:
why is the button not able to respond?
is there a way to make this work as is?
is there another option to achieve the same results?
edit ----------------------------------
Just a snippit of the function it calls. code ommited as it never gets there
function calculateGrade() {
console.log('here'); // <--- THIS NEVER SHOWS UP, BUTTON IS NOT SENDING IT HERE
// code cleared for clarity
}
EDIT 2
for(let i=0; i < studentList.length-1; i++) {
row = table.insertRow(-1);
var
cell = row.insertCell(-1); cell.innerHTML = studentList[i];
cell = row.insertCell(-1); cell.innerHTML = grade
cell = row.insertCell(-1); cell.innerHTML = maxGrade;
cell = row.insertCell(-1); cell.innerHTML = student.name;
if(student.grade < maxGrade) { // change later to within 90%
cell = row.insertCell(-1); cell.innerHTML = '<button onclick="calculateGrade();" > + </button>';
}else{
cell = row.insertCell(-1); cell.innerHTML = 'Pass';
}
}
manipulation the dom
if you want to manipulate the dom "a hundrets of time" you should use a framework like jQuery or the javascript own functions for dom manipulation instead of innerHTML.
Why?
HTML has changed from time to time and you can't know, if your innerHTML will be correct (formatted) in the future.
document.createElementas example will work in every browser.
Also there is the possibility, that dom manipulation functions will be more optimized by compilers and engines in future.
Dynamic listeners
If you create dom elements dynamicly you don't have to declare the listeners as text.
let button = document.createElement('button');
button.addEventListener('onclick', calculateGrade);
cell.appendChild(button);
also this is less vulnerable for typos ;)
dynamic element creation and reflow
Whenever you change something in the dom that is a visible change, the browser fill fire a reflow. It will recalculate all elements sizes. As you can imagine, adding "a hundrets" of buttons will cause a hundrets of reflows.
Reflow will not trigger on invisible objects. so best practise if you manipulate a lot of elements is to turn the parent invisible, then make your changes and make the parent visible again. In this case you trigger a reflow only twice.
function myFunction() {
var row = document.getElementById("myRow");
var x = row.insertCell(-1);
x.innerHTML = '<button class="calc" onclick="calculateGrade();"> +button </button>';
}
function calculateGrade() {
alert("Hi am working action");
}
Check here the new add button is working when I am adding cell.innerHTML there will calc button and when you will click it will show "Hi am working action," I think it will help you
<!DOCTYPE html>
<html>
<head>
<style>
table, td {
border: 1px solid black;
}
</style>
</head>
<body>
<p>Click the button to insert new cell(s) at the beginning of the table row.</p>
<table>
<tr>
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
<tr id="myRow">
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
<tr>
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
</table><br>
<button onclick="myFunction()">Try it</button>
</body>
</html>
Do not use onevent attributes:
<button onclick="func()">...</button>
Use either onevent property:
document.querySelector('button').onclick = function;
Or an eventListener:
document.querySelector('button').addEventListener('click', function)
Use Event Delegation -- a programming pattern that allows us to register a single event handler to listen for an unlimited number of elements. Here's a simplified outline:
Choose an element that is a common ancestor to all of the elements we want to react to an event. This includes window and document as well, but window should be used for key events (ex. document.onclick = calculateGrade)
The callback function (ex. calculateGrade(e)) must pass the Event Object (event, evt, or e are the most common names).
Reference the clicked, changed, hovered, etc. element with the event property Event.target (ex. const tgt = e.target).
Isolate Event.target by if...else control statements (ex. if (tgt.matches('button')) {...)
The rest of the code is just to dynamically generate a table for demonstration purposes. Although the code that generates a table in OP question is dubious it wasn't part of the question so there's no explanation for that part in the demo.
const tData = [{
name: 'zer00ne',
grade: 100
}, {
name: 'Trump',
grade: 25
}, {
name: 'Joe Average',
grade: 75
}, {
name: 'Slacker',
grade: 50
}];
const tHdrs = [`<th>#</th>`, `<th>Name</th>`, `<th>Grade</th>`, `<th> </th>`];
const frag = document.createDocumentFragment();
const table = document.createElement('table');
frag.appendChild(table);
const title = table.createCaption();
title.textContent = 'Student Grades';
const head = table.createTHead();
const hRow = head.insertRow(-1);
let h = tHdrs.length - 1;
for (let th of tHdrs) {
th = hRow.insertCell(0).outerHTML = tHdrs[h];
--h;
}
let rowQty = tData.length;
let colQty = tHdrs.length;
const body = document.createElement('tbody');
table.appendChild(body);
for (let r = 0; r < rowQty; r++) {
let row = body.insertRow(-1);
for (let c = 0; c < colQty; c++) {
let col = c => row.insertCell(c);
let cell = col(c);
if (c === 0) {
cell.textContent = r + 1;
} else if (c === colQty - 1) {
if (tData[r].grade > 69) {
cell.textContent = 'Pass';
} else {
cell.innerHTML = `<button>Calculate</button>`;
}
} else if (c === 1) {
cell.textContent = tData[r].name;
} else {
cell.textContent = tData[r].grade;
}
}
}
const foot = table.createTFoot();
const fRow = foot.insertRow(-1);
const fCol = fRow.insertCell(0);
fCol.textContent = 'Passing grade is 70% or more.'
fCol.setAttribute('colspan', '4');
fCol.className = 'note';
document.body.appendChild(frag);
// Relevant Code
document.onclick = calculateGrade;
function calculateGrade(e) {
const tgt = e.target;
if (tgt.matches('button')) {
let row = tgt.closest('tr');
let grd = row.children[2].textContent;
let num = parseFloat(grd);
console.log(num);
}
return false;
}
:root {
font: 400 5vh/1 Verdana;
}
table {
table-layout: fixed;
border-collapse: collapse;
width: 96%;
}
caption {
font-weight: 700;
font-size: 1.2rem;
}
th,
td {
border: 2px ridge #000;
min-height: 30px;
}
th:first-of-type {
width: 10%;
}
th:nth-of-type(3) {
width: 20%;
}
th:last-of-type {
width: 25%
}
td:first-of-type,
td:nth-of-type(3) {
text-align: center;
font-size: 1.2rem;
font-family: Consolas;
}
td:nth-of-type(3)::after {
content: '%';
}
td:first-of-type.note {
text-align: right;
font-size: 0.85rem;
font-family: Verdana;
border: none
}
button {
font: inherit;
width: 100%
}
I am adding a CSS rule to an element with jQuery. E.g. $('#any-el').css('outline', '1px solid red').
I later remove the style with $('#any-el').css('outline', '').
I don't want to disturb any other styles this element may have, and I've found that this method works well for CSS added with external or inline stylesheets, but it will (obviously) modify and then remove any 'outline' given added via JavaScript at any previous point.
What is the best way to ensure that the value of the 'outline' style attribute is restored to the way it was before I modified it?
Here is the whole relevant part of the code:
var last_el = null
$('#mask').mousemove(function(e) {
var mask = $(this).detach()
var el = window.document.elementFromPoint(e.clientX, e.clientY)
if (el != last_el) {
if (last_el) {
$(last_el).css('outline', '')
}
if (el) {
$(el).css('outline', '1px solid red')
}
last_el = el
}
$('body').append(mask)
})
(Basically the #mask overlays the whole page and I outline the element which the mouse is hovering over, underneath the mask.)
You could store the value of the previous outline style in a variable before changing it, and then later restore the value of the variable. E.g.
var last_el = null;
var outline_style = null;
$('#mask').mousemove(function(e) {
var mask = $(this).detach();
var el = window.document.elementFromPoint(e.clientX, e.clientY);
if (el != last_el) {
if (last_el) {
$(last_el).css('outline', outline_style);
}
if (el) {
outline_style = $(el).css('outline');
$(el).css('outline', '1px solid red');
}
last_el = el;
}
$('body').append(mask);
});
The simplest thing you could do to add styles and restore them without any disturbance would be to put your new styles in a class and add/remove that class rather than setting inline styles. Make sure that all styles within the class are made !important.
var $span = $('span');
$('button').click(function () {
var $this = $(this),
override = $this.text() === 'Override';
$span.toggleClass('outline-override');
$this.text(override? 'Restore' : 'Override');
});
.outline-override {
outline: 1px solid red !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<span style="outline: 1px solid black;">test</span>
<button>Override</button>