Difference in event change in Internet Explore and Chrome - javascript

I have an website which uses an input type=file to upload some files.
After uploading I don't want to display the file selection, so I am going to delete it by calling event.target.value = null.
function viewModel() {
var self = this;
self.addAudioFile = function(data, event) {
event.preventDefault();
var context = ko.contextFor(event.target);
var selectedFile = event.target.files[0];
var targetPrompt = data.prompt;
console.log("aa");
event.target.value = null;
}
}
$(document).ready(function() {
var newModel = new viewModel();
ko.applyBindings(newModel);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="btn-link prompt-audio-control">
<div class="btn-file">
<input type="file" data-bind="event: {change: $root.addAudioFile}" accept="audio/wav|audio/pcm|audio|vox">
<span> Upload </span>
</div>
</div>
What confuses me here is when I used Chrome to test, the function addAudioFile is executed only one time, but when I used IE, it executed this function 2 times.
I used the console.log("aa") in oder to test it. Can anyone explain me why this happens?

It was triggered twice due to this call.
event.target.value = null;
After that line, the change event is fired again.
Try putting another console.log after that line so you can see the effect.
Something like:
event.target.value = null;
console.log("bb");
So in order to stop the function from continuing its second call you can trap the value of event.target.value.
self.addAudioFile = function(data, event) {
if(event.target.value === ""){
return;
}
... rest of code here ...
}
Why is it checked as a string and not a null? That part i'm not sure but the browsers engine are converting it into a blank string hence the condition.
And on the part where chrome doesn't fire twice, i'll just leave this to browser experts, but my guess is chrome engine is set to not change the file if a null value is given (just my wild guess though).
==================
And lastly, you can use knockout this way but you "should" not do it that way :D
See Tomalak's answer on how to separate knockout's DOM concerns using custom bindings.

As a matter of principle, whenever you find that you access DOM elements, events or even the binding context in a knockout viewmodel then you are doing something wrong.
The only place in a knockout application where you interact with the DOM is the binding handler. If there is no binding handler that does what you need, write one.
// exemplary binding handler ---------------------------------------------------------
ko.bindingHandlers.file = {
init: function (element, valueAccessor) {
if ( !ko.isWriteableObservable(valueAccessor()) ) return;
// alternatively $(element).on("change", ...
ko.utils.registerEventHandler(element, "change", function (e) {
var observable = valueAccessor();
observable(e.target.files[0]);
element.value = null;
e.preventDefault();
});
}
};
// usage -----------------------------------------------------------------------------
function FileUploader() {
var self = this;
self.audioFile = ko.observable();
}
$(function() {
var viewModel = new FileUploader();
ko.applyBindings(viewModel);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="btn-link prompt-audio-control">
<div class="btn-file">
<input type="file" data-bind="file: audioFile" accept="audio/wav|audio/pcm|audio|vox">
<span> Upload </span>
</div>
</div>
<hr>
View Model:
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
If setting the <input type="file">'s value to null is a good idea is another matter, since the event's files collection contains information about the files the user selected, it does not contain the files themselves. Setting the form control to null effectively destroys the user selection. If you want to actually upload the file at some point, more plumbing will be necessary.

What i undestand:
You want to "hide" the text about "what file you upload" after upload the file... (what redundant phrase lol), maybe this could help you instead "erasing" the selection.
Creating a HTML Button:
.fileUpload {
position: relative;
overflow: hidden;
margin: 10px;
}
.fileUpload input.upload {
position: absolute;
top: 0;
left: 0;
margin: 0;
padding: 0;
font-size: 20px;
cursor: pointer;
opacity: 0;
filter: alpha(opacity=0);
}
<div class="fileUpload btn btn-primary">
<span>Upload</span>
<input type="file" class="upload" />
</div>

Related

Why is the jquery object not returning its value?

I've been trying to figure this problem out for the last 2 days and have not found any solution so far.
Im trying to attach a .click() listener to all elements of a list, but any time I use this or $(this) none of the jquery functions work, for example using .val() returns undefined even though it has a value.
I'm using fomantic-ui but I've also tried the same code without and it doesn't work either. I'm also using NodeJS and Express, in case that makes a difference.
Further testing showed me that for some reason this doesn't work:
$('#first_name').on('input', () => {
const name = $(this)
const field = name.parent()
if (!name.val().match(/^\p{L}{1,16}$/u)) {
field.attr('class', 'field error')
name.prop('valid', false)
} else {
field.attr('class', 'field success')
name.prop('valid', true)
}
})
But if I change it to this, everything is fine:
$('#first_name').on('input', () => {
const name = $('#first_name') //Only Change...
const field = name.parent()
if (!name.val().match(/^\p{L}{1,16}$/u)) {
field.attr('class', 'field error')
name.prop('valid', false)
} else {
field.attr('class', 'field success')
name.prop('valid', true)
}
})
And also this both return false
console.log($(this) === $('#first_name'), $(this) == $('#first_name'))
//false false
I have tried all sorts of combinations but nothing I can think of works, and nothing I found anywhere online has either. Maybe I just don't understand how this is supposed to work but I've tried reading the jquery documentation but it didn't help me.
Can anyone help me?
You're using an arrow function, so the value of this will be inherited from the parent context. A console.log should show you what that is.
You probably want to use a regular anonymous function, assuming jQuery calls the function with the HTML element set to the context of this.
$('#first_name').on('input', function() {
// ...
});
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
Arrow functions don't have their own bindings to this or super, and
should not be used as methods.
Here I have an example where I use a class instead of an id on the input group.
Your scope issue is resolved by using function(event){ form for the function
I use the .closest('.wrapper') to get the "owner" of the group.
I hooked the event handler using the container ID: $('#inputs-container')
I use a data attribute and set a value for that to do some "creative" css depending upon the condition
IF for some reason you need to get the container, you can use the event.delegateTarget - this is the container element with the id id="inputs-container"
I added the change event also in case someone does a "paste" or you change the value programmatically
I would suggest you use semi-colons on the ends of the lines in the script; at some point not doing so will cause a very hard to find bug
I admit this is a bit of overkill but perhaps someone can get some use of the example even though it is admittedly a bit "verbose". Try it out by entering in text, numbers and spaces in each of the three inputs.
$('#inputs-container').on('input change', '.first-name',function(event) {
const $input = $(this);
const field = $input.closest('.wrapper');
//console.log(field.data('goodness'), field.get(0).dataset);
let val = $input.val();
const regex = /^\p{L}{1,16}$/u;
const isGood = val.match(regex) == val;
//console.log('Good:', val, isGood);
field.get(0).dataset.goodness = isGood ? "success" : "error";
$input.prop('valid', isGood);
});
.wrapper {
border: 1px solid #d0d0d0;
border-width: 0.5em;
padding: 0.5em;
}
.wrapper[data-goodness="error"] {
border-color: #FF0000;
border-width: 0.5em;
}
.wrapper[data-goodness="error"] .err-message:before {
content: "Do Better";
padding-left: 1em;
}
.wrapper[data-goodness="success"] {
border-color: #00FFdd;
}
.wrapper[data-goodness="success"] .err-message:before {
content: "Doing well so far!";
padding-left: 1em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="inputs-container">
<div class="wrapper" data-goodness="cheers">
<div class="field-things">
<div class="field-name-thing">We are happy to have:</div>
<label>First Name</label>
<input type="text" class="first-name" /><span class="err-message"></span>
</div>
</div>
<div class="wrapper" data-goodness="cheers">
<div class="field-things">
<div class="field-name-thing">We are happy to have:</div>
<label>First Name</label>
<input type="text" class="first-name" /><span class="err-message"></span>
</div>
</div>
</div>
<div class="wrapper" data-goodness="cheers">
<div class="field-things">
<div class="field-name-thing">OUT OF SCOPE</div>
<label>First Name</label>
<input type="text" class="first-name" /><span class="err-message"></span>
</div>
</div>

event doesn't clone after deleting first element

I'm supposed to clone some elements with Jquery and it works well but when i delete the first element which i'm cloning the others with it, after that the new cloned elements don't have the events which supposed to have!
i tried .clone(true, true) method and it clone event but not after the deleting the first element.
var card = $(".newCard"); //class name of first element
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true)); //it works well but...
});
$("[class^=btnDelete]").click(function() {
$(this).closest(".newCard").remove(); //it works too but not after deleting first element and creating again
});
I don't know why this is happening, actually every element should have the events even after deleting the first element and recreate.
The problem is the click event is being bound to that first element, and as a result that binding is removed along with the element. When dealing with dynamic elements you should use event delegation by using the .on method on a static element. Such as the document itself.
EDIT: You won't notice any performance issues on a small document like this, but using the document as your event delegator can cause performance issues on larger documents. You can read more about event delegation performance here.
var card = $(".newCard");
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true));
});
$(document).on('click', '.btnDelete', function() {
$(this).closest(".newCard").remove();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="addBtn0">Add</button>
<div class="row">
<div class="newCard">Card <button class="btnDelete">Delete</button></div>
</div>
This happens because of
$("[class^=btnDelete]").click(function() {
the above line will target the existing (!!!) element and it's inner button.
Since you're cloning that existing element, you're also cloning the Event bound to it's button on-creation.
Once you delete that card (stored in variable), you're also destroying the Event bound to it.
To fix that issue use .on() with dynamic event delegation:
$(".row").on('click', '[class^=btnDelete]', function() {
var card = $(".newCard"); //class name of first element
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true));
});
$(".row").on('click', '[class^=btnDelete]', function() {
$(this).closest(".newCard").remove();
});
<div class="row">
<div class="newCard">CARD <button class="btnDelete">DELETE</button></div>
</div>
<button id="addBtn0">ADD</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
https://api.jquery.com/on/#direct-and-delegated-events
Other issues and solution
Other issues you have in your code are mainly naming stuff. [class^=btnDelete] is just waiting for you one day adding a style class to that poor button and see your JavaScript fail miserably. Also, why btnAdd0 what's the 0? Why .clone(true, true) at all?
Here's a better rewrite:
const $Cards = $('.Cards');
const $Cards_add = $('.Cards-btn--add');
const $Cards_item = (html) => {
const $self = $('<div/>', {
appendTo: $Cards,
class: `Cards-item`,
html: html || `New Card`,
});
$('<button/>', {
appendTo: $self,
class: `Cards-btn Cards-btn--delete`,
text: `Delete`,
on: {
click() {
$self.remove();
}
},
});
}
let card_id = 0;
$Cards_add.on('click', () => $Cards_item(`This is Card ${++card_id}`));
// Create some dummy cards
$Cards_item(`This is Card ${++card_id}`);
$Cards_item(`This is Card ${++card_id}`);
$Cards_item(`This is Card ${++card_id}`);
/**
* Cards component styles
*/
.Cards {} /* scss extend .row or rather define styles directly */
.Cards-item { padding: 5px; margin: 5px 0; background: #eee; }
.Cards-btn { }
.Cards-btn--add { color: blue; }
.Cards-btn--delete { color: red; }
<div class="Cards"></div>
<button class="Cards-btn Cards-btn--add">ADD</button>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>
$(".row").append(card.clone(true, true));
You are still using the original result of $('.newCard') that does not include any new cards you've added.
$(".row").append($(this).parent().clone(true, true));
this works

How to set file value from datatransfer object [duplicate]

Both examples work in Chrome and Opera, but fail in Firefox 56.0.
I want to set the files FileList of a form's file input.[Codepen]
HTML
<form>
<input type="file" id="input1" multiple>
<br>
<input type="file" id="input2" multiple>
</form>
JAVASCRIPT
var input1 = document.getElementById("input1");
var input2 = document.getElementById("input2");
input1.onchange = function () {
console.log(input1.files);
input2.files = input1.files;
};
On Chrome and Opera, selecting files in the first input, will also change the second. In Firefox, the second input doesn't change, even though the filelist appears to be correct in the console's output.
The overall goal is to create a drag-drop upload interface.
Prototype here.
The ability to set a FileList as the input.files property programmatically has been added as an PR to the specs three months ago, even if webkit allow this for years. Firefox has landed a patch in its next stable version, 57 and Edge is probably still working on it (I don't have an account to see the progress).It seems it has now landed in Edge too.
The main use case for this feature is to allow DataTransfer.files from e.g a drag&drop event or a paste one to be added to an <input> field. As such, only a FileList is allowed (and null to clear the input).
So in the case exposed in the body of your question, I don't really see the point of using this feature between two <input> fields.
If you want to keep in memory selected FileList, you can always convert it as an Array of files.
If you want to be able to move your filled input in a <form> later on, you can do it directly with the inputElement and DOM methods.
And if you need to workaround the limitations this new feature leverages, you can always fill an FormData with the DataTransfer's files and send this FormData through xhr instead of using the default HTML form method.
And Since I first missed the real use-case, in the codepen, here is an possible implementation to workaround the drag&drop issue you are facing, even on older browsers that didn't support this new feature.
This uses an hidden input in the dropZone, which will catch the dropped files directly.
// called when the input hidden in the dropZone changes
function handleDroppedChange(evt) {
this.removeEventListener('drop', handleDroppedChange); // only once
// create a new hidden input
var clone = this.cloneNode();
clone.addEventListener('change', handleDroppedChange);
clone.addEventListener('change', handleBasicChange);
this.parentNode.insertBefore(clone, this);
// replace the visible one with the current hidden one
var form = document.querySelector('form');
var previous = form.querySelector('input[type=file]');
form.insertBefore(this, previous);
form.removeChild(previous);
this.id = previous.id; // for the <label>
}
// add first listeners
var hiddenTarget = dropzone.querySelector('input[type="file"]');
hiddenTarget.addEventListener('change', handleDroppedChange);
hiddenTarget.addEventListener('change', handleBasicChange);
file_input.addEventListener('change', handleBasicChange);
// handle drop over enter leave as usual on the parent
dropzone.ondragover = dropzone.ondragenter = function(evt) {
evt.preventDefault();
dropzone.className = "drag";
};
dropzone.ondragleave = function(evt) {
evt.preventDefault();
dropzone.className = "";
};
dropzone.ondrop = function(evt) {
dropzone.className = "";
console.log("drop");
};
// will trigger for any kind of changes (dropped or manual)
function handleBasicChange(evt) {
var file_names = Array.prototype.map.call(this.files, function(f){return f.name;});
label.innerHTML = "Changed " + file_names.join('<br>');
// start upload process
};
#dropzone {
display: inline-block;
padding: 25px;
border: 8px dashed #b11;
position: relative;
}
#dropzone.drag {
border-color: #f74;
}
#dropzone>input{
opacity: 0;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
/* below rules avoid clicks on hidden input */
pointer-events: none;
}
#dropzone.drag>input{
pointer-events: all;
}
<form>
<input type="file" id="file_input" multiple>
</form><br><br>
<div id="dropzone">
<label id="label" for="file_input">Drop here.</label>
<!-- we use an hidden file input to catch the dropped files -->
<input type="file" multiple>
</div>

Make all elements of a html class react on a click, which would modify the element itself

I am trying to write a tutorial for my students, in the form of a webpage with hidden "spoilers" that the student can unhide, presumably after thinking about the answer. So, long story short, the behavior I am looking for is:
in the beginning, the text appears with a lot of hidden words;
when a piece of text is clicked, it appears, and stays uncovered afterwards;
this should work with minimal overhead (not forcing me to install a complex framework) and on all my students' machines, even if the browser is outdated, even if jquery is not installed.
I searched for off the shelf solutions, but all those I checked were either too complicated or not doing exactly what I wanted. So I decided to do my own.
What I have so far is this:
<HTML>
<STYLE>
span.spoil {background-color: black;}
span.spoiled {background-color: white;}
</STYLE>
<HEAD>
<TITLE>SPOIL</TITLE>
<META http-equiv="Content-Type" content="text/html;charset=UTF-8">
<!--LINK rel="Stylesheet" type="text/css" href=".css"-->
</HEAD>
<BODY>
This is a text with <span class="spoil" onclick="showspoil(this)">spoil data</span>.
<br>
<span class="spoil" onclick="showspoil(this)">Unspoil me.</span>
<br>
<span class="spoil" onclick="showspoil(this)">And me.</span>
<script>
function showspoil(e) {
e.className="spoiled";
}
// var classname = document.getElementsByClassName("spoil");
// for (var i = 0; i < classname.length; i++) {
// classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
// }
</script>
</BODY>
</HTML>
It does the job, except that I find it annoying to have to write explicitly the "onclick..." for each element. So I tried adding an event listener to each member of the class, by imitating similar resources found on the web: unfortunately, this part (the commented code above) does not work. In particular, I do not see which parameter I should pass to the function to transmit "the element itself".
Can anyone help? If I may play it lazy, I am more looking for an answer to this specific query than for pointers to a series of courses I should take: I admit it, I have not been doing html for a loooooong time, and I am sure I would need a lot of readings to be efficient again: simply, I do not have the time for the moment, and I do not really need it: I just need to solve this issue to set up a working solution.
Problem here is you are calling the method and assigning what it returns to be bound as the event listener
classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
You can either use a closure or call the element directly.
classname[i].addEventListener('click', function () { showspoil(this); }, false);
or
classname[i].addEventListener('click', showspoil, false);
If you call it directly, you would need to change the function to
function showspoil(e) {
this.className="spoiled";
}
Another option would be to not bind click on every element, just use event delegation.
function showspoil(e) {
e.className="spoiled";
}
document.addEventListener("click", function (e) { //list for clcik on body
var clicked = e.target; //get what was clicked on
if (e.target.classList.contains("spoil")) { //see if it is an element with the class
e.target.classList.add("spoiled"); //if it is, add new class
}
});
.spoil { color: red }
.spoiled { color: green }
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
function unspoil() {
this.className = "spoiled"; // "this" is the clicked object
}
window.onload = function() {
var spoilers = document.querySelectorAll(".spoil"); // get all with class spoil
for (var i = 0; i < spoilers.length; i++) {
spoilers[i].onclick = unspoil;
}
}
span.spoil {
background-color: black;
}
span.spoiled {
background-color: white;
}
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
An additional approach could be to add the click-listener to the document and evaluate the event target:
document.addEventListener("click", function(e){
if (e.target.className == "spoil"){
e.target.className = "spoiled";
}
});
That way
you only need one event listener in the whole page
you can also append other elements dynamically with that class without the need for a new event handler
This should work, because the event's target is always the actual element being clicked. If you have sub-elements in your "spoil" items, you may need to traverse up the parent chain. But anyway I think this is the least resource-wasting way.
var spoilers = document.getElementsByClassName('spoil');
for(i=0;i<spoilers.length;i++){
spoilers[i].addEventListener('click',function(){
this.className = "spoiled";
});
}

knockout js not updating from view model

So I have a the following textarea in my view that I am attempting to update via a knockout binding.
Here is the code from the View:
<textarea disabled id="CCHPISentence" style="width:99.7%; height:75px; resize: none; font-family:Verdana; overflow: auto;" data-bind="text: fullSymptomTextObservable"> </textarea>
Here is the Jquery function that applies the bindings, I am wondering if my issue is here:
$(function () {
ko.applyBindings(symptomTextViewModel, document.getElementById("CCHPISentence"))
})
Here is my ViewModel:
function symptomTextViewModel(fullText) {
if (fullText === undefined)
{ fullText = ""}
this.fullSymptomTextObservable = ko.observable(fullText.toString())
}
Here is a snip from the js function that calls my ViewModel. I am building the fullText variable in this 2nd js function:
//FINAL PARAGRAPH KNOCKOUT VM MAPPING
fullText = sentence1 + sentence2 + sentence3 + sentence4 + sentence5
var symptSentViewModel = new symptomTextViewModel(fullText)
symptomTextViewModel(fullText);
Thanks so much to anybody in advance who can assist me here. I feel like I am missing something stupid, I have tried this every different way that I can think of with no luck.
it would be easier to make fullSymptomTextObservable a pureComputed observable. That way as the various sentences change so will the full sentence. This way you are taking advantage of knockoutjs.
function SymptomTextViewModel(fullText) {
var self = this;
if (fullText === undefined) {
fullText = ""
}
self.fullSymptomTextObservable = ko.observable(fullText.toString())
}
var vm = new SymptomTextViewModel('This is a test paragraph. Watch for the alert();');
ko.applyBindings(vm,document.getElementById("CCHPISentence"));
alert("about to use the vm variable to access the text.");
vm.fullSymptomTextObservable('Changing it to something else');
alert("about to use the ko.dataFor function");
var testVm = ko.dataFor(document.getElementById("CCHPISentence"));
testVm.fullSymptomTextObservable("Maybe this would be better suited to what you need.");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<textarea disabled id="CCHPISentence" style="width:99.7%; height:75px; resize: none; font-family:Verdana; overflow: auto;" data-bind="text: fullSymptomTextObservable"></textarea>
First of all I would fix all missing semicolons in your code. Than check if scope of 'this' in your view models function is correct. Some info from browser console would be also helpful. Check if there isn't any errors which knockout would throw.

Categories