JavaScript removeChild help - javascript

I am writing a simple little piece of code to draw pixels wherever the mouse is in a box. I also want to have a clear button. Drawing works fine, but I can't seem to get the clear button to work. Here are the relevant parts of my .js file:
function pixel(x, y) {
var pix = document.createElement("div");
pix.setAttribute("style", "position:absolute;left:" + x + "px;top:" +
y + "px;width:3px;height:3px;background:#000;cursor:crosshair");
return pix;
}
var mouseDown = false;
function draw(event) {
if (!mouseDown) return;
var x = event.clientX;
var y = event.clientY;
document.getElementById("box").appendChild(pixel(x, y));
}
/* Neither 1, 2, nor 3 work! */
function clear() {
var box = document.getElementById("box");
/* 1 */
// box.innerHTML = "";
/* 2 */
// box.childNodes = new NodeList();
/* 3 */
for (n in box.childNodes)
box.removeChild(n);
}
The relevant part of my HTML file is:
<body onmousedown="mouseDown=true" onmouseup="mouseDown=false">
<div id="box" onmouseover="document.getElementById('box').style.cursor='crosshair'"
onmousemove="draw(event)"></div>
<button onclick="clear()">Clear</button>
</body>
The box is also formatted a bit with CSS but that shouldn't be an issue. I feel like the problem might be that I'm deleting the pixels from the box but not from the document or something, but I'm a JavaScript noob so I don't know.

Rename your function to something else (not clear()).
function removePixels() {
var box = document.getElementById("box");
if (box.hasChildNodes() )
{
while ( box.childNodes.length >= 1 )
{
box.removeChild( box.firstChild );
}
}
}//end function

I don't think clear is a valid name for a function.
http://jsfiddle.net/zUJ2e/
EDIT: Yep, definitely not
http://www.roseindia.net/javascript/javascript-clear-method.shtml

You shouldn't use a "for ... in" loop on a NodeList:
for (var n = 0; n < childNodes.length; ++n)
box.removeChild(childNodes[n]);
A NodeList isn't an Array, though it sort-of acts like one, sometimes. In general, "for ... in" is for objects, not Arrays.
Another, totally separate note: you may run into problems on some browsers setting the "style" that way (for your "pixels"). The "style" property of a DOM node is treated as a weird magic thing in all browsers, but my recollection is that doing what you're doing may not always work. Instead, you would set individual properties of someElement.style.

The way that you're hooking up for your button to the event handler is inadvertently hitting document.clear(), rather than the clear() function that you've defined.
One way to avoid this is to rename the function to something else. If you rename the function to myClear(), for example, that should resolve this particular conflict. This does feel a bit dodgy, however.
You can bind your event-handler in JavaScript itself, which seems more reliable. If you're using the JQuery library, you can do something like this, for example:
// when the document is ready...
$(document).ready(function() {
// connect all buttons to the clear event handler.
$('button').click(clear);
})
If you're trying to stick with vanilla JavaScript, you can set the onclick attribute in JavaScript, when the DOM tree is mostly ready.
<body onmousedown="mouseDown=true" onmouseup="mouseDown=false">
<div id="box" onmouseover="document.getElementById('box').style.cursor='crosshair'"
onmousemove="draw(event)"></div>
<!-- button has an id to make it selectable with getElementById() -->
<button id="button">Clear</button>
<!-- Placed at the bottom so we have a chance of getting button -->
<script>
document.getElementById("button").onclick = clear;
</script>
</body>

Related

Place cursor at the end of text in an iframe body element with JavaScript

I'll start by apologising as this may seem like or actually be a duplicate, but I've tried every solution I've encountered and none seem to be working for me.
In my HTML I have an iframe referencing another HTML document. With JavaScript, at the press of a list of buttons I insert text into the body of that iframe. I also use JavaScript to maintain focus on the iframe body. The problem is that nothing appears to work for me to get the cursor to move to the end of the text each time I press those buttons, it always moves to the beginning.
One of the solutions I've tried was to add this code to the function that handles my button presses:
iFrameBody.focus();
var content = iFrameBody.innerHTML;
iFrameBody.innerHTML = content;
so the function looks like this:
function typeIn(buttonId) {
var iFrameBody = document.getElementById("iFrame").contentWindow.document.body;
iFrameBody.innerHTML += buttonId;
iFrameBody.focus();
var content = iFrameBody.innerHTML;
iFrameBody.innerHTML = content;
}
Something else I tried was, in the HTML file referenced by my iframe I did:
<body onfocus="this.innerHTML = this.innerHTML;"></body>
I tried several other more complicated solutions that frankly I didn't even quite understand to be honest, all to no avail. Any help would be much appreciated.
I figured it out. The issue was that I was using a body element, writing to it's innerHTML and trying to set focus on the body. By simply using a textarea inside my iFrame instead it became very simple and it only required the simplest code.
This to set focus when the page loads:
window.onload = function () {
var iFrameTextArea = document.getElementById("iFrame").contentWindow.document.getElementById("iFrameTextArea");
iFrameTextArea.focus();
}
And then this to set the button to write to the textarea while maintaining focus:
function typeIn(buttonId) {
var iFrameTextArea = document.getElementById("iFrame").contentWindow.document.getElementById("iFrameInput");
iFrameTextArea.focus();
iFrameTextArea.value += buttonId;
}
Super easy!!
Instead of again using textarea in iframe, u can also solve this by using the following code.
var iframeElement = document.getElementById("iFrame").contentWindow.document.body;
iframeElement.focus();
var len = iframeElement.length ;
iframeElement.setSelectionRange(len, len);

Precise Drag and Drop within a contenteditable

The Setup
So, I have a contenteditable div -- I'm making a WYSIWYG editor: bold, italics, formatting, whatever, and most lately: inserting fancy images (in a fancy box, with a caption).
<a class="fancy" href="i.jpg" target="_blank">
<img alt="" src="i.jpg" />
Optional Caption goes Here!
</a>
The user adds these fancy images with a dialog I present them with: they fill out the details, upload the image, and then much like the other editor functions, I use document.execCommand('insertHTML',false,fancy_image_html); to plop it in at the user's selection.
Desired Functionality
So, now that my user can plop in a fancy image -- they need to be able to move it around.
The user needs to be able to click and drag the image (fancy box and all) to place it anywhere that they please within the contenteditable. They need to be able to move it between paragraphs, or even within paragraphs -- between two words if they want.
What gives me hope
Keep in mind -- in a contenteditable, plain old <img> tags are already blessed by the user-agent with this lovely drag-and-drop capability. By default, you can drag and drop <img> tags around wherever you please; the default drag-and-drop operation behaves as one would dream.
So, considering how this default behavior already works so smashingly on our <img> buddies -- and I only want to extend this behaviour a little bit to include a tad more HTML -- this seems like something that should be easily possible.
My Efforts Thus Far
First, I set up my fancy <a> tag with the draggable attribute, and disabled contenteditable (not sure if that's necessary, but it seems like it may as well be off):
<a class="fancy" [...] draggable="true" contenteditable="false">
Then, because the user could still drag the image out of the fancy <a> box, I had to do some CSS. I'm working in Chrome, so I'm only showing you the -webkit- prefixes, though I used the others too.
.fancy {
-webkit-user-select:none;
-webkit-user-drag:element; }
.fancy>img {
-webkit-user-drag:none; }
Now the user can drag the whole fancy box, and the little partially-faded click-drag representation image reflects this -- I can see that I'm picking up the entire box now :)
I've tried several combinations of different CSS properties, the above combo seems to make sense to me, and seems to work best.
I was hoping that this CSS alone would be enough for the browser to use the entire element as the draggable item, automagically granting the user the functionality I've been dreaming of... It does however, appear to be more complicated than that.
HTML5's JavaScript Drag and Drop API
This Drag and Drop stuff seems more complicated than it needs to be.
So, I started getting deep into DnD api docs, and now I'm stuck. So, here's what I've rigged up (yes, jQuery):
$('.fancy')
.bind('dragstart',function(event){
//console.log('dragstart');
var dt=event.originalEvent.dataTransfer;
dt.effectAllowed = 'all';
dt.setData('text/html',event.target.outerHTML);
});
$('.myContentEditable')
.bind('dragenter',function(event){
//console.log('dragenter');
event.preventDefault();
})
.bind('dragleave',function(event){
//console.log('dragleave');
})
.bind('dragover',function(event){
//console.log('dragover');
event.preventDefault();
})
.bind('drop',function(event){
//console.log('drop');
var dt = event.originalEvent.dataTransfer;
var content = dt.getData('text/html');
document.execCommand('insertHTML',false,content);
event.preventDefault();
})
.bind('dragend',function(event){
//console.log('dragend');
});
So here's where I'm stuck: This almost completely works. Almost completely. I have everything working, up until the very end. In the drop event, I now have access to the fancy box's HTML content that I'm trying to have inserted at the drop location. All I need to do now, is insert it at the correct location!
The problem is I can't find the correct drop location, or any way to insert to it. I've been hoping to find some kind of 'dropLocation' object to dump my fancy box into, something like dropEvent.dropLocation.content=myFancyBoxHTML;, or perhaps, at least, some kind of drop location values with which to find my own way to put the content there?
Am I given anything?
Am I doing it completely wrong? Am I completely missing something?
I tried to use document.execCommand('insertHTML',false,content); like I expected I should be able to, but it unfortunately fails me here, as the selection caret is not located at the precise drop location as I'd hope.
I discovered that if I comment out all of the event.preventDefault();'s, the selection caret becomes visible, and as one would hope, when the user prepares to drop, hovering their drag over the contenteditable, the little selection caret can be seen running along between characters following the user's cursor and drop operation -- indicating to the user that the selection caret represents the precise drop location. I need the location of this selection caret.
With some experiments, I tried execCommand-insertHTML'ing during the drop event, and the dragend event -- neither insert the HTML where the dropping-selection-caret was, instead it uses whatever location was selected prior to the drag operation.
Because the selection caret is visible during dragover, I hatched a plan.
For awhile, I was trying, in the dragover event, to insert a temporary marker, like <span class="selection-marker">|</span>, just after $('.selection-marker').remove();, in an attempt for the browser to constantly (during dragover) be deleting all selection markers and then adding one at the insertion point -- essentially leaving one marker wherever that insertion point is, at any moment. The plan of course, was to then replace this temporary marker with the dragged content which I have.
None of this worked, of course: I couldn't get the selection-marker to insert at the apparently visible selection caret as planned -- again, the execCommand-insertedHTML placed itself wherever the selection caret was, prior to the drag operation.
Huff. So what have I missed? How is it done?
How do I obtain, or insert into, the precise location of a drag-and-drop operation?
I feel like this is, obviously, a common operation among drag-and-drops -- surely I must have overlooked an important and blatant detail of some kind? Did I even have to get deep into JavaScript, or maybe there's a way to do this just with attributes like draggable, droppable, contenteditable, and some fancydancy CSS3?
I'm still on the hunt -- still tinkering around -- I'll post back as soon as I find out what I've been failing at :)
The Hunt Continues (edits after original post)
Farrukh posted a good suggestion -- use:
console.log( window.getSelection().getRangeAt(0) );
To see where the selection caret actually is. I plopped this into the dragover event, which is when I figure the selection caret is visibily hopping around between my editable content in the contenteditable.
Alas, the Range object that is returned, reports offset indices that belong to the selection caret prior to the drag-and-drop operation.
It was a valiant effort. Thanks Farrukh.
So what's going on here? I am getting the sensation that the little selection caret I see hopping around, isn't the selection caret at all! I think it's an imposter!
Upon Further Inspection!
Turns out, it is an imposter! The real selection caret remains in place during the entire drag operation! You can see the little bugger!
I was reading MDN Drag and Drop Docs, and found this:
Naturally, you may need to move the insertion marker around a dragover event as well. You can use the event's clientX and clientY properties as with other mouse events to determine the location of the mouse pointer.
Yikes, does this mean I'm supposed to figure it out for myself, based on clientX and clientY?? Using mouse coordinates to determine the location of the selection caret myself? Scary!!
I'll look into doing so tomorrow -- unless myself, or somebody else here reading this, can find a sane solution :)
Dragon Drop
I've done a ridiculous amount of fiddling. So, so much jsFiddling.
This is not a robust, or complete solution; I may never quite come up with one. If anyone has any better solutions, I'm all ears -- I didn't want to have to do it this way, but it's the only way I've been able to uncover so far. The following jsFiddle, and the information I am about to vomit up, worked for me in this particular instance with my particular versions of Firefox and Chrome on my particular WAMP setup and computer. Don't come crying to me when it doesn't work on your website. This drag-and-drop crap is clearly every man for himself.
jsFiddle: Chase Moskal's Dragon Drop
So, I was boring my girlfriend's brains out, and she thought I kept saying "dragon drop" when really, I was just saying "drag-and-drop". It stuck, so that's what I call my little JavaScript buddy I've created for handling these drag-and-drop situations.
Turns out -- it's a bit of a nightmare. The HTML5 Drag-and-Drop API even at first glance, is horrible. Then, you almost warm up to it, as you start to understand and accept the way it's supposed to work.. Then you realize what a terrifying nightmare it actually is, as you learn how Firefox and Chrome go about this specification in their own special way, and seem to completely ignore all of your needs. You find yourself asking questions like: "Wait, what element is even being dragged right now? How to do I get that information? How do I cancel this drag operation? How can I stop this particular browser's unique default handling of this situation?"... The answers to your questions: "You're on your own, LOSER! Keep hacking things in, until something works!".
So, here's how I accomplished Precise Drag and Drop of Arbitrary HTML Elements within, around, and between multiple contenteditable's. (note: I'm not going fully in-depth with every detail, you'll have to look at the jsFiddle for that -- I'm just rambling off seemingly relevant details that I remember from the experience, as I have limited time)
My Solution
First, I applied CSS to the draggables (fancybox) -- we needed user-select:none; user-drag:element; on the fancy box, and then specifically user-drag:none; on the image within the fancy box (and any other elements, why not?). Unfortunately, this was not quite enough for Firefox, which required attribute draggable="false" to be explicitly set on the image to prevent it from being draggable.
Next, I applied attributes draggable="true" and dropzone="copy" onto the contenteditables.
To the draggables (fancyboxes), I bind a handler for dragstart. We set the dataTransfer to copy a blank string of HTML ' ' -- because we need to trick it into thinking we are going to drag HTML, but we are cancelling out any default behavior. Sometimes default behavior slips in somehow, and it results in a duplicate (as we do the insertion ourselves), so now the worst glitch is a ' ' (space) being inserted when a drag fails. We couldn't rely on the default behavior, as it would fail to often, so I found this to be the most versatile solution.
DD.$draggables.off('dragstart').on('dragstart',function(event){
var e=event.originalEvent;
$(e.target).removeAttr('dragged');
var dt=e.dataTransfer,
content=e.target.outerHTML;
var is_draggable = DD.$draggables.is(e.target);
if (is_draggable) {
dt.effectAllowed = 'copy';
dt.setData('text/plain',' ');
DD.dropLoad=content;
$(e.target).attr('dragged','dragged');
}
});
To the dropzones, I bind a handler for dragleave and drop. The dragleave handler exists only for Firefox, as in Firefox, the drag-drop would work (Chrome denies you by default) when you tried to drag it outside the contenteditable, so it performs a quick check against the Firefox-only relatedTarget. Huff.
Chrome and Firefox have different ways of acquiring the Range object, so effort had to be put in to do it differently for each browser in the drop event. Chrome builds a range based on mouse-coordinates (yup that's right), but Firefox provides it in the event data. document.execCommand('insertHTML',false,blah) turns out to be how we handle the drop. OH, I forgot to mention -- we can't use dataTransfer.getData() on Chrome to get our dragstart set HTML -- it appears to be some kind of weird bug in the specification. Firefox calls the spec out on it's bullcrap and gives us the data anyways -- but Chrome doesn't, so we bend over backwards and to set the content to a global, and go through hell to kill all the default behavior...
DD.$dropzones.off('dragleave').on('dragleave',function(event){
var e=event.originalEvent;
var dt=e.dataTransfer;
var relatedTarget_is_dropzone = DD.$dropzones.is(e.relatedTarget);
var relatedTarget_within_dropzone = DD.$dropzones.has(e.relatedTarget).length>0;
var acceptable = relatedTarget_is_dropzone||relatedTarget_within_dropzone;
if (!acceptable) {
dt.dropEffect='none';
dt.effectAllowed='null';
}
});
DD.$dropzones.off('drop').on('drop',function(event){
var e=event.originalEvent;
if (!DD.dropLoad) return false;
var range=null;
if (document.caretRangeFromPoint) { // Chrome
range=document.caretRangeFromPoint(e.clientX,e.clientY);
}
else if (e.rangeParent) { // Firefox
range=document.createRange(); range.setStart(e.rangeParent,e.rangeOffset);
}
var sel = window.getSelection();
sel.removeAllRanges(); sel.addRange(range);
$(sel.anchorNode).closest(DD.$dropzones.selector).get(0).focus(); // essential
document.execCommand('insertHTML',false,'<param name="dragonDropMarker" />'+DD.dropLoad);
sel.removeAllRanges();
// verification with dragonDropMarker
var $DDM=$('param[name="dragonDropMarker"]');
var insertSuccess = $DDM.length>0;
if (insertSuccess) {
$(DD.$draggables.selector).filter('[dragged]').remove();
$DDM.remove();
}
DD.dropLoad=null;
DD.bindDraggables();
e.preventDefault();
});
Okay, I'm sick of this. I've wrote all I want to about this. I'm calling it a day, and might update this if I think of anything important.
Thanks everybody. //Chase.
Since I wanted to see this in a native JS solution I worked a bit to remove all jQuery dependencies. Hopefully it can help someone.
First the markup
<div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
WAITING FOR STUFF
</div>
<div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Block 1
</span>
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Second Blk
</span>
</div>
Then some helpers
function addClass( elem, className ){
var classNames = elem.className.split( " " )
if( classNames.indexOf( className ) === -1 ){
classNames.push( className )
}
elem.className = classNames.join( " " )
}
function selectElem( selector ){
return document.querySelector( selector )
}
function selectAllElems( selector ){
return document.querySelectorAll( selector )
}
function removeElem( elem ){
return elem ? elem.parentNode.removeChild( elem ) : false
}
Then the actual methods
function nativeBindDraggable( elems = false ){
elems = elems || selectAllElems( '.native_drag' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection already (as good as array)
for( let i = 0 ; i < elems.length ; i++ ){
// For every elem in list, attach or re-attach event handling
elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
elems[i].ondragstart = function(e){
if (!e.target.id){
e.target.id = (new Date()).getTime();
}
window.inTransferMarkup = e.target.outerHTML;
window.transferreference = elems[i].dataset.transferreference;
addClass( e.target, 'dragged');
};
};
}
function nativeBindWriteRegion( elems = false ){
elems = elems || selectAllElems( '.native_receiver' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection
for( let i = 0 ; i < elems.length ; i++ ){
elems[i].ondragover = function(e){
e.preventDefault();
return false;
};
elems[i].ondrop = function(e){
receiveBlock(e);
};
}
}
function receiveBlock(e){
e.preventDefault();
let content = window.inTransferMarkup;
window.inTransferMarkup = "";
let range = null;
if (document.caretRangeFromPoint) { // Chrome
range = document.caretRangeFromPoint(e.clientX, e.clientY);
}else if (e.rangeParent) { // Firefox
range = document.createRange();
range.setStart(e.rangeParent, e.rangeOffset);
}
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange( range );
e.target.focus();
document.execCommand('insertHTML',false, content);
sel.removeAllRanges();
// reset draggable on all blocks, esp the recently created
nativeBindDraggable(
document.querySelector(
`[data-transferreference='${window.transferreference}']`
)
);
removeElem( selectElem( '.dragged' ) );
return false;
}
And lastly instantiate
nativeBindDraggable();
nativeBindWriteRegion();
Below is the functioning snippet
function addClass( elem, className ){
var classNames = elem.className.split( " " )
if( classNames.indexOf( className ) === -1 ){
classNames.push( className )
}
elem.className = classNames.join( " " )
}
function selectElem( selector ){
return document.querySelector( selector )
}
function selectAllElems( selector ){
return document.querySelectorAll( selector )
}
function removeElem( elem ){
return elem ? elem.parentNode.removeChild( elem ) : false
}
function nativeBindDraggable( elems = false ){
elems = elems || selectAllElems( '.native_drag' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection already (as good as array)
for( let i = 0 ; i < elems.length ; i++ ){
// For every elem in list, attach or re-attach event handling
elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
elems[i].ondragstart = function(e){
if (!e.target.id){
e.target.id = (new Date()).getTime();
}
window.inTransferMarkup = e.target.outerHTML;
window.transferreference = elems[i].dataset.transferreference;
addClass( e.target, 'dragged');
};
};
}
function nativeBindWriteRegion( elems = false ){
elems = elems || selectAllElems( '.native_receiver' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection
for( let i = 0 ; i < elems.length ; i++ ){
elems[i].ondragover = function(e){
e.preventDefault();
return false;
};
elems[i].ondrop = function(e){
receiveBlock(e);
};
}
}
function receiveBlock(e){
e.preventDefault();
let content = window.inTransferMarkup;
window.inTransferMarkup = "";
let range = null;
if (document.caretRangeFromPoint) { // Chrome
range = document.caretRangeFromPoint(e.clientX, e.clientY);
}else if (e.rangeParent) { // Firefox
range = document.createRange();
range.setStart(e.rangeParent, e.rangeOffset);
}
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange( range );
e.target.focus();
document.execCommand('insertHTML',false, content);
sel.removeAllRanges();
// reset draggable on all blocks, esp the recently created
nativeBindDraggable(
document.querySelector(
`[data-transferreference='${window.transferreference}']`
)
);
removeElem( selectElem( '.dragged' ) );
return false;
}
nativeBindDraggable();
nativeBindWriteRegion();
<div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
WAITING FOR STUFF
</div>
<div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Block 1
</span>
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Second Blk
</span>
</div>
event dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
event drop:
var me = this; setTimeout(function () {
var el = me.element.getElementsByClassName("whatever")[0];
if (el) { //do stuff here, el is your location for the fancy img
}
}, 0);
To summarise above answers, the keys are event.parentNode and event.rangeOffset (firefox) and caretRangeFromPoint(event.clientX, event.clientY) (chrome).
Here is a minimal example:
span {
border: 1px solid red;
}
span:before {
content: "grab ";
background-color: #0f0;
}
<p contenteditable="true"
ondrop="sel = window.getSelection();
if (document.caretRangeFromPoint)
range = document.caretRangeFromPoint(event.clientX, event.clientY)
else {
sel.collapse(event.rangeParent,event.rangeOffset)
range = sel.getRangeAt(0)
}
range.insertNode(sp1)"
ondragover="
return false"
>This is a contenteditable paragraph. Grab the green field in the following span
<span draggable="True" id="sp1" ondragstart="
event.dataTransfer.setData('text/plain', this.innerText)">span</span>
and drag it inside this paragraph.
</p>

JavaScript Click Events - Why do I need two addEventListeners?

I'm trying my best to learn unobtrusive JavaScript. The code below is an attempt to change an existing practice piece I had with the getArrays function called from within the HTML tag. This is the only version I could get to work. While it's nice that it works, I feel like there may be some unnecessary parts and don't fully understand why I need the last line of code in there (kind of new to the addEventListener stuff). I was working off of the Mozilla DOM reference example and know how to do this in jQuery already. Thanks!
JavaScript:
<script>
function getArrays() {
var ddlValArray = new Array();
var ddlTextArray = new Array();
var ddl = document.getElementById("ddlFlavors");
for (i=0; i<ddl.options.length; i++) {
ddlValArray[i] = ddl.options[i].value;
ddlTextArray[i] = ddl.options[i].text;
alert(ddlValArray[i] + ' ' + ddlTextArray[i]);
}
}
function showArrays() {
var link = document.getElementById("clickHandler");
link.addEventListener("click", getArrays, false);
}
document.addEventListener("DOMContentLoaded", showArrays, false);
</script>
HTML
Select Flavor:
<select id='ddlFlavors'>
<option value="1">Vanilla</option>
<option value="2">Chocolate</option>
<option value="3">Strawberry</option>
<option value="4">Neopolitan</option>
</select>
<br/><br/>
<a id="clickHandler" href="#">Show Flavors</a>
You don't if you structure your HTML / JS correctly.
Move the script tag to the bottom of the body just before the </body> tag and you no longer need to wait for DOMContentLoaded event. The html above it will be parsed prior to the JS being executed.
This CAN cause other caveats but for your example it will work.
<body>
Select Flavor:
<select id='ddlFlavors'>
<option value="1">Vanilla</option>
<option value="2">Chocolate</option>
<option value="3">Strawberry</option>
<option value="4">Neopolitan</option>
</select>
<br/><br/>
<a id="clickHandler" href="#">Show Flavors</a>
<script>
function getArrays() {
var ddlValArray = new Array();
var ddlTextArray = new Array();
var ddl = document.getElementById("ddlFlavors");
for (i=0; i<ddl.options.length; i++) {
ddlValArray[i] = ddl.options[i].value;
ddlTextArray[i] = ddl.options[i].text;
alert(ddlValArray[i] + ' ' + ddlTextArray[i]);
}
}
function showArrays() {
var link = document.getElementById("clickHandler");
link.addEventListener("click", getArrays, false);
}
showArrays();
</script>
</body>
There are many other benefits to placing the scripts at the bottom as well. Read more about them here.
One thing to note: Even though the tags for iframes and images are parsed, the image itself might not be finished downloading, so you will have to wait for that. Also iframe content might not be loaded yet either. However DOMContentLoaded doesn't wait for these either. I just thought it would be nice to note.
Event listeners are crucial to JS. Every time a user interacts with your page, you need to pick-up on that event, and respond to it.
Of course, in this snippet, your functions are assuming there is an element with a given id in the DOM, ready, set and waiting. This might not be the case, that's why you have to wait until JS receives the OK (the DOMReady event).
That's just one, very simple, example of why might use addEventListener. But IMO, event-listeners really come into their own when you start using Ajax calls to add new content to your page as you go along:
Suppose the DOM is ready, but some elements might, or might not be requested later on (depending on user input). In jQuery, you'd use things like this:
$('#foo').on('click',functionRef);
This doesn't require the foo element to be available when this code is run, but as soon as the element is added to the dom, the click events will be dealt with as you desire.
In non-jQ, you'll use addEventListener for that. Because of the event model (see quirksmode for details on propagation and bubbling), the listener needn't be attached to the element directly:
document.body.addEventListener('click',function(e)
{
if ((e.target || e.srcElemet).id === 'foo')
{
//function that deals with clicks on foo element
}
},false);//last param is for bubbling/propagating events
This way, you can bind all events for elements that might be added to the dom asynchronously, without having to check each response of each ajax call...
Let's take it one step further even: delegation. As you can see, in the snippet above, we're listening for all click events that occure somewhere inside the body. That's as near as makes no difference: all clicks, so you don't need to add 101 distinct click listeners for 101 elements, just the one. The only thing you need to do, is write the handler in such a way that it deals with each element accordingly:
document.body.addEventListener('click',function(e)
{
e = e || window.event;
var elem = e.target || e.srcElement;
switch (true)
{
case elem.id === 'foo':
//either code here or:
return fooCallback.apply(elem,[e]);//use another function as though it were the handler
case elem.id.className.match(/\bsomeClass\b/):
return someClassCallback.apply(elem,[e]);//~= jQuery's $('.someClass').on('click',function);
case elem.type === 'text':
//and so on
}
},false);
Pretty powerful stuff. There is, of course, a lot more too it, and there are downsides too (mainly X-browser stuff), but the main benefits are:
Handling events for elements that are dynamically added/removed
A single event listener that deals with all events for all elements makes your code more efficient, sometimes boosting performance by a lot. here's a nice example of when delegation is in order, sadly, it also shows the pains that X-browser development brings to the party...
This line of code: document.addEventListener("DOMContentLoaded", showArrays, false); ensures that your function showArrays() doesn't start executing before the DOM is ready. In other words, the function showArrays() will start executing after the (X)HTML document has been loaded and parsed.
I'd recommend that you put your JavaScript code in a separate file and use the following HTML code in the head section: <script type="text/javascript" src="yourfilepath.js"></script>. Then you could also use window.onload and onclick events instead of the document.addEventListener() method.
When you do that, you can put the code within the showArrays function outside its body and remove the function.
For example:
window.onload = function()
{
var link = document.getElementById("clickHandler");
link.onclick = getArrays;
}
That is considered a much better practice.
Full Example
In script.js file:
//This is the beginning of the script...
function getArrays() {
var ddlValArray = new Array();
var ddlTextArray = new Array();
var ddl = document.getElementById("ddlFlavors");
for (i=0; i<ddl.options.length; i++) {
ddlValArray[i] = ddl.options[i].value;
ddlTextArray[i] = ddl.options[i].text;
alert(ddlValArray[i] + ' ' + ddlTextArray[i]);
}
}
window.onload = function()
{
var link = document.getElementById("clickHandler");
link.onclick = getArrays;
}
Inside your HTML head:
<script type="text/javascript" src="script.js"></script>
It's better to write JavaScript separate from the HTML file in most cases, just like CSS. That way, you make your HTML code look tidier and more organised.

Delay onmouseover javascript

im new to most web dev stuff so im kindly asking you for some support.
i have an image map in which i have assigned several areas triggering different contents in a separate div. i would now like to add a delay to the onmouseover trigger so that the content in the div is only updated if the user positions the curser on the area instead of accidentally hovering over it.
this is the js i use for toggling the div contents:
function showHideDivs(indx){
hideDivs();
oShowHideDivs[indx].style.display = 'block';
}
function hideDivs(){
for(i=0; i < oShowHideDivs.length; i++){
oShowHideDivs[i].style.display = 'none';
}
}
window.onload=function(){
oShowHideDivs = document.getElementById('container').getElementsByTagName('div');
var oMap = document.getElementById('myMap');
for(i=0; i < oMap.areas.length; i++){
oMap.areas[i].indx = i;
oMap.areas[i].onmouseover=function(){
showHideDivs(this.indx);
}
}
}
so how do i implement the delay and where? thx in advance!
jan
EDIT:
i used this approach now:
oMap.areas[i].onmouseover=function(){
var area=this;
var delay=setTimeout(function(){showHideDivs(area.indx);},100);
area.onmouseout=function(){clearTimeout(delay);};
}
seemed the easiest to me. thx for the hint!
The easiest way is to include a timeout on mouseover, and clear it on mouseout.
oMap.areas[i].onmouseover=function(){
var area=this;
var delay=setTimeout(function(){showHideDivs(area.indx);},100);
area.onmouseout=function(){clearTimeout(delay);};
}
For more complex scenarios, use a plugin like hoverintent.
You need to use setTimeout() to call your function showHideDivs() after a certain delay. And you stop this function from being called if the user moves its mouse before the end of your delay.
Look here for a concrete example : https://stackoverflow.com/a/6231142/1606729

How do I make the entire div clickable using plain javascript?

I don't need to write this in jQuery but I'm not versed enough in plain javascript to figure it out. Chris Coyier wrote a nice explanation of what I'm talking about here.
The reason I want to convert it is because I don't need to include an entire jQuery library for this one piece of code. I can save that extra request by using plain old javascript.
This is the example code that I want to convert:
$(document).ready(function() {
$(".featured").click(function(){
window.location=$(this).find("a").attr("href"); return false;
});
});
Here's what I've come up with so far:
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("div.feature").click(function(){
window.location=$(this).find("a").setAttribute("href");
return false;
});
});
One thing which isn't correct in this, as far as I know, are the querySelectorAll, which is looking for just a div element, right? The other thing is the $(this), which I don't know how to translate into plain javascript.
Assuming...
you know the browser support for querySelectorAll and yet you still use it
that addEventListener only works for standards compliant browsers
I believe you meant:
//get all a's inside divs that have class "featured"
var feat = document.querySelectorAll("div.featured a"),
featlen = feat.length,
i;
//loop through each
for(i=0;i<featlen;++i){
//add listeners to each
feat[i].addEventListener('click',function(){
window.location = this.href;
},false);
}
Or you can have the <div> wrapped in <a>. No JS required. It's perfectly valid HTML and browsers do work as intended despite the rule that inline elements should not contain block elements. Just make sure to have display:block on <a> as well as adjust its size.
<a href="location">
<div> content </div>
</a>
You can select with this.querySelectorAll(...):
IE8:
window.onload = function() {
// get all dom elements with class "feature"
var aFeatures = document.querySelectorAll(".feature");
// for each selected element
for (var i = 0; i < aFeatures.length; i++) {
// add click handler
aFeatures[i].onclick = function() {
// get href of first anchor in element and change location
window.location = this.querySelectorAll("a")[0].href;
return false;
};
}
};
IE9 and other current browser:
document.addEventListener("DOMContentLoaded", function() {
// get all dom elements with class "feature"
var aFeatures = document.querySelectorAll(".feature");
// for each selected element
for (var i = 0; i < aFeatures.length; i++) {
// add click handler
aFeatures[i].addEventListener('click', function() {
// get href of first anchor in element and change location
window.location = this.querySelectorAll("a")[0].href;
return false;
});
}
});
=== UPDATE ===
For IE7 support you should add following (untested) script before (also see here):
(function(d){d=document,a=d.styleSheets[0]||d.createStyleSheet();d.querySelectorAll=function(e){a.addRule(e,'f:b');for(var l=d.all,b=0,c=[],f=l.length;b<f;b++)l[b].currentStyle.f&&c.push(l[b]);a.removeRule(0);return c}})()
It is possible that it only supports document.querySelectorAll not element.querySelectorAll.

Categories

Resources