I am looking for Drag And Drop File Upload component, using Knockout + .NET WebApi technologies.
I have found File Api project, it doesn't support old browsers, but I can live with it. The code is here: https://github.com/khayrov/khayrov.github.com/tree/master/jsfiddle/knockout-fileapi.
It creates custom Knockout Bindings, some code parts:
HTML:
<input type="file" accept="image/*" data-bind="file: imageFile, fileObjectURL: imageObjectURL, fileBinaryData: imageBinary"/>
Knockout JS:
ko.bindingHandlers.file = {
init: function(element, valueAccessor) {
$(element).change(function() {
var file = this.files[0];
if (ko.isObservable(valueAccessor())) {
valueAccessor()(file);
}
});
},
update: function(element, valueAccessor, allBindingsAccessor) {
var file = ko.utils.unwrapObservable(valueAccessor());
var bindings = allBindingsAccessor();
if (bindings.fileObjectURL && ko.isObservable(bindings.fileObjectURL)) {
var oldUrl = bindings.fileObjectURL();
if (oldUrl) {
windowURL.revokeObjectURL(oldUrl);
}
bindings.fileObjectURL(file && windowURL.createObjectURL(file));
}
if (bindings.fileBinaryData && ko.isObservable(bindings.fileBinaryData)) {
if (!file) {
bindings.fileBinaryData(null);
} else {
var reader = new FileReader();
reader.onload = function(e) {
bindings.fileBinaryData(e.target.result);
};
reader.readAsArrayBuffer(file);
}
}
}
Unfourtunately I do not understand if I can reuse this code and integrated it within some Drag And Drop File upload component?
Is there any existing DnD file upload component that can be used with knockout + webapi?
See this http://jsfiddle.net/3LT9d/
function noopHandler(evt) {
evt.preventDefault();
return false;
}
ko.bindingHandlers.dropUpload = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
element.addEventListener('dragenter', noopHandler, false);
element.addEventListener('dragover', noopHandler, false);
element.addEventListener('drop', function (evt) {
evt.preventDefault();
var value = valueAccessor();
for (var i = 0; i < evt.dataTransfer.files.length; i++) {
value.push(evt.dataTransfer.files[i]);
}
}, false);
}
};
How it works:
The dropUpload custom binding populates an observable array with the dragged files
To upload the files, the new API FormData is used since backward compatibility seems to be not an issue. FormData is easier to work with.
Follow this article to find out how to allow your webapi to accept FormData
http://www.asp.net/web-api/overview/working-with-http/sending-html-form-data,-part-2
Related
Using Fullcalendar 4.x, is it possible to add the "X-Requested-With: XMLHttpRequest" header when fetching events ?
I'm setting up the event source in this way :
calendar.addEventSource({ url: ev_url, id: 'default' });
Everything works and the request is sent correctly, but the header i mentioned is missing (on server side we require that header to be present).
I tried adding the following to addEventSource:
beforeSend: function (xhr) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
Another thing i tried was to add this in the js file (probably pointless since Fullcalendar 4 is not using jquery anymore ?):
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
});
Unfortunately neither solution worked.
In the past when using fullcalendar 3.x that header was present when requesting events. I guess that was because JQuery was adding it automatically.
I was looking for the answer to this as well. I modified the FullCalendar main.js file which is not ideal but does the trick!
I modified the file # around line 4242 here is the full source from that function:
/*! FullCalendar Core Package v4.3.1
function requestJson(method, url, params, successCallback, failureCallback) {
method = method.toUpperCase();
// Set Headers From Params to own varaible
var headers;
if(params.hasOwnProperty('headers') && Array.isArray(params.headers)){
headers = params.headers;
// Remove them from the params object
delete params.headers;
}
var body = null;
if (method === 'GET') {
url = injectQueryStringParams(url, params);
}
else {
body = encodeParams(params);
}
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
// Create Headers If Avaiable
if(typeof headers !== "undefined"){
for(var key in headers){
if (!headers.hasOwnProperty(key)) continue;
var obj = headers[key];
for(var prop in obj){
if (!obj.hasOwnProperty(prop)) continue;
xhr.setRequestHeader(prop, obj[prop]);
}
}
}
if (method !== 'GET') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 400) {
try {
var res = JSON.parse(xhr.responseText);
successCallback(res, xhr);
}
catch (err) {
failureCallback('Failure parsing JSON', xhr);
}
}
else {
failureCallback('Request failed', xhr);
}
};
xhr.onerror = function () {
failureCallback('Request failed', xhr);
};
xhr.send(body);
}
To Use it I simply add a headers key to the extraParams object like so:
extraParams = {
action: 'get_event',
headers: [
{"X-Requested-With":"XMLHttpRequest"}
]
};
This way you can add as many extra headers as you need.
Is there any best way to resize and save files to s3 in meteor.
I though about using cfs packages, but they put too much load on server.
To directly upload images to the s3 I'm using slingshot which is very fine.
but slingshot takes only file objects as inputs it doesn't take streams to store the files.
Is there any option to resize the image in client side and pass it to the slingshot package
package: https://github.com/CulturalMe/meteor-slingshot
issue: https://github.com/CulturalMe/meteor-slingshot/issues/36
Yes it's possible with clientside-image-manipulation, here's my untested interpretation of the docs:
Template.upload.events({
'change #image-upload': function(event, target) {
var uploader = new Slingshot.Upload("myFileUploads");
var file = event.target.files[0];
var img = null;
processImage(file, 300, 300, function(data){
uploader.send(data, function (error, downloadUrl) {
if(error)
throw new Meteor.Error('upload', error);
Meteor.users.update(Meteor.userId(), {$push: {"profile.files": downloadUrl}});
});
});
}
});
There are other image related plugins worth investigating at atmosphere.js
I've found that the following works in order to integrate Clientside Image Manipulation with Slingshot (asynchronous code with ES6 promises):
var uploader;
function b64ToBlob(b64Data, contentType, sliceSize) {
var byteNumbers, i, slice;
var offset = 0;
var byteCharacters = atob(b64Data);
var byteArrays = [];
sliceSize = sliceSize || 512;
byteArrays = [];
while (offset < byteCharacters.length) {
slice = byteCharacters.slice(offset, offset + sliceSize);
byteNumbers = [];
for (i = 0; i < slice.length; ++i) {
byteNumbers.push(slice.charCodeAt(i));
}
byteArrays.push(new Uint8Array(byteNumbers));
offset += sliceSize;
}
return new Blob(byteArrays, {type: contentType});
}
uploader = new Slingshot.Upload("pictures");
new Promise(function (resolve) {
processImage(file, 300, 300, resolve);
}).then(function (dataUri) {
var match = /^data:([^;]+);base64,(.+)$/.exec(dataUri);
return [file.name, match[1], match[2]];
}).then(function (params) {
var name = params[0];
var type = params[1];
var b64 = params[2];
return new Promise(function (resolve, reject) {
var blob = b64ToBlob(b64, type);
blob.name = name;
uploader.send(blob, function (error, downloadUrl) {
if (error != null) {
reject(error.message);
} else {
resolve(downloadUrl);
}
});
});
});
Conversion from Base64 to blob is borrowed from https://stackoverflow.com/a/16245768/1238764.
There are a lot of image manipulation plugins. The one I use is: cropper
It's a jquery based plugin which basically does the thing you want + a bit more. After manipulation you can convert the image to a canvas and with the dataToUrl() method you can pass the data of the new manipulated image to any datasource of your choice.
I have an app that when you select an industry from a drop down list a collection is updated where the attribute equals the selected industry.
JavaScript:
Template.selector.events({
'click div.select-block ul.dropdown-menu li': function(e) {
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#industryPicker option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('currentIndustryOnet');
if(val != oldVal) {
Session.set('jobsLoaded', false);
Session.set('currentIndustryOnet', val);
Meteor.call('countByOnet', val, function(error, results){
if(results > 0) {
Session.set('jobsLoaded', true);
} else {
getJobsByIndustry(val);
}
});
}
}
});
var getJobsByIndustry = function(onet) {
if(typeof(onet) === "undefined")
alert("Must include an Onet code");
var params = "onet=" + onet + "&cn=100&rs=1&re=500";
return getJobs(params, onet);
}
var getJobs = function(params, onet) {
Meteor.call('retrieveJobs', params, function(error, results){
$('job', results.content).each(function(){
var jvid = $(this).find('jvid').text();
var job = Jobs.findOne({jvid: jvid});
if(!job) {
options = {}
options.title = $(this).find('title').text();
options.company = $(this).find('company').text();
options.address = $(this).find('location').text();
options.jvid = jvid;
options.onet = onet;
options.url = $(this).find('url').text();
options.dateacquired = $(this).find('dateacquired').text();
var id = createJob(options);
console.log("Job Created: " + id);
}
});
Session.set('jobsLoaded', true);
});
}
Template.list.events({
'click div.select-block ul.dropdown-menu li': function(e){
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#perPage option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('perPage');
if(val != oldVal) {
Session.set('perPage', val);
Pagination.perPage(val);
}
}
});
Template.list.jobs = function() {
var jobs;
if(Session.get('currentIndustryOnet')) {
jobs = Jobs.find({onet: Session.get('currentIndustryOnet')}).fetch();
var addresses = _.chain(jobs)
.countBy('address')
.pairs()
.sortBy(function(j) {return -j[1];})
.map(function(j) {return j[0];})
.first(100)
.value();
gmaps.clearMap();
$.each(_.uniq(addresses), function(k, v){
var addr = v.split(', ');
Meteor.call('getCity', addr[0].toUpperCase(), addr[1], function(error, city){
if(city) {
var opts = {};
opts.lng = city.loc[1];
opts.lat = city.loc[0];
opts.population = city.pop;
gmaps.addMarker(opts);
}
});
})
return Pagination.collection(jobs);
} else {
jobs = Jobs.find()
Session.set('jobCount', jobs.count());
return Pagination.collection(jobs.fetch());
}
}
In Template.list.jobs if you console.log(addresses), it is called 4 different times. The browser console looks like this:
(2) 100
(2) 100
Any reason why this would fire multiple times?
As #musically_ut said it might be because of your session data.
Basically you must make the difference between reactive datasources and non reactive datasources.
Non reactive are standard javascript, nothing fancy.
The reactive ones however are monitored by Meteor and when one is updated (insert, update, delete, you name it), Meteor is going to execute again all parts which uses this datasource. Default reactive datasources are: collections and sessions. You can also create yours.
So when you update your session attribute, it is going to execute again all helper's methods which are using this datasource.
About the rendering, pages were rendered again in Meteor < 0.8, now with Blaze it is not the case anymore.
Here is a quick example for a better understanding:
The template first
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>{{getSession}}</h1>
<h1>{{getNonReactiveSession}}</h1>
<h1>{{getCollection}}</h1>
<input type="button" name="session" value="Session" />
<input type="button" name="collection" value="Collection" />
</template>
And the client code
if (Meteor.isClient) {
CollectionWhatever = new Meteor.Collection;
Template.hello.events({
'click input[name="session"]': function () {
Session.set('date', new Date());
},
'click input[name="collection"]': function () {
CollectionWhatever.insert({});
}
});
Template.hello.getSession = function () {
console.log('getSession');
return Session.get('date');
};
Template.hello.getNonReactiveSession = function () {
console.log('getNonReactiveSession');
var sessionVal = null;
new Deps.nonreactive(function () {
sessionVal = Session.get('date');
});
return sessionVal;
};
Template.hello.getCollection = function () {
console.log('getCollection');
return CollectionWhatever.find().count();
};
Template.hello.rendered = function () {
console.log('rendered');
}
}
If you click on a button it is going to update a datasource and the helper method which is using this datasource will be executed again.
Except for the non reactive session, with Deps.nonreactive you can make Meteor ignore the updates.
Do not hesitate to add logs to your app!
You can read:
Reactivity
Dependencies
Now that I have found a way to initialize Google Maps with the help of Andy Joslin in this SO initialize-google-map-in-angularjs, I am looking for a way to asynchronous load a Google Map Object.
I found an example of how to do this in the phonecat project.
Notice how the JS files are loaded in this example: index-async.html
In my Jade Scripts partial that is loaded into my program I tried:
script(src='js/lib/angular/angular.js')
script(src='js/lib/script/script.min.js')
script
$script([
'js/lib/angular/angular-resource.min.js',
'js/lib/jquery/jquery-1.7.2.min.js',
'http://maps.googleapis.com/maps/api/js?key=AIzaSyBTmi_pcXMZtLX5MWFRQgbVEYx-h-pDXO4&sensor=false',
'js/app.js',
'js/services.js',
'js/controllers.js',
'js/filters.js',
'js/directives.js',
'bootstrap/js/bootstrap.min.js'
], function() {
// when all is done, execute bootstrap angular application
angular.bootstrap(document, ['ofm']);
});
When I do this and go to load the map page I get:
A call to document.write() from an asycrononously-loaded
external script was ignored.
This is how Google Maps is being loaded now as a service:
'use strict';
var app = angular.module('ofm.services', []);
app.factory('GoogleMaps', function() {
var map_id = '#map';
var lat = 46.87916;
var lng = -3.32910;
var zoom = 15;
var map = initialize(map_id, lat, lng, zoom);
return map;
});
function initialize(map_id, lat, lng, zoom) {
var myOptions = {
zoom : 8,
center : new google.maps.LatLng(lat, lng),
mapTypeId : google.maps.MapTypeId.ROADMAP
};
return new google.maps.Map($(map_id)[0], myOptions);
}
It appears that this should be returning a promise from what I recall reading. But this AngularJS is very new to me.
here's my solution I came up without using jQuery:
(Gist here)
angular.module('testApp', []).
directive('lazyLoad', ['$window', '$q', function ($window, $q) {
function load_script() {
var s = document.createElement('script'); // use global document since Angular's $document is weak
s.src = 'https://maps.googleapis.com/maps/api/js?sensor=false&callback=initialize';
document.body.appendChild(s);
}
function lazyLoadApi(key) {
var deferred = $q.defer();
$window.initialize = function () {
deferred.resolve();
};
// thanks to Emil Stenström: http://friendlybit.com/js/lazy-loading-asyncronous-javascript/
if ($window.attachEvent) {
$window.attachEvent('onload', load_script);
} else {
$window.addEventListener('load', load_script, false);
}
return deferred.promise;
}
return {
restrict: 'E',
link: function (scope, element, attrs) { // function content is optional
// in this example, it shows how and when the promises are resolved
if ($window.google && $window.google.maps) {
console.log('gmaps already loaded');
} else {
lazyLoadApi().then(function () {
console.log('promise resolved');
if ($window.google && $window.google.maps) {
console.log('gmaps loaded');
} else {
console.log('gmaps not loaded');
}
}, function () {
console.log('promise rejected');
});
}
}
};
}]);
If you using jQuery in your AngularJS app, check out this function which returns a promise for when the Google Maps API has been loaded:
https://gist.github.com/gbakernet/828536
I was able to use this in a AngularJS directive to lazy-load Google Maps on demand.
Works a treat:
angular.module('mapModule') // usage: data-google-map
.directive('googleMap', ['$window', function ($window) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
// If Google maps is already present then just initialise my map
if ($window.google && $window.google.maps) {
initGoogleMaps();
} else {
loadGoogleMapsAsync();
}
function loadGoogleMapsAsync() {
// loadGoogleMaps() == jQuery function from https://gist.github.com/gbakernet/828536
$.when(loadGoogleMaps())
// When Google maps is loaded, add InfoBox - this is optional
.then(function () {
$.ajax({ url: "/resources/js/infobox.min.js", dataType: "script", async: false });
})
.done(function () {
initGoogleMaps();
});
};
function initGoogleMaps() {
// Load your Google map stuff here
// Remember to wrap scope variables inside `scope.$apply(function(){...});`
}
}
};
}]);
Take a look of this i think its more reliable
var deferred = $q.defer();
var script = document.createElement('script');
$window.initMap = function() {
//console.log("Map init ");
deferred.resolve();
}
script.src = "//maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&libraries=places&callback=initMap";
document.body.appendChild(script);
return deferred.promise;
I'm trying to manually clean the HTML of a Telerik RadEditor with Javascript but I can't seem to find the correct place to store the value so that it gets saved on post back.
Here's the JS I have:
$(function () {
jQuery.fixHash = function ($html) {
// modify $html
return $html;
};
$("#adminEditingArea input[id$='SaveButton']").unbind("click").click(function () {
$("iframe[id$='_contentIframe']").trigger("save");
// call .net postback
return false;
});
});
var editorSaveEventInit = false;
function InitSaveEvent() {
if (!editorSaveEventInit) {
var $EditFrames = $("iframe[id$='_contentIframe']");
if ($EditFrames && $EditFrames.length > 0) {
$EditFrames.bind("save", function (e) {
var $thisFrame = $(this);
var thisFrameContents = $thisFrame.contents();
if (thisFrameContents) {
var telerikContentIFrame = thisFrameContents.get(0);
var $body = $("body", telerikContentIFrame);
var html = $.fixHash($body).html();
$body.html(html);
// also tried storing the modified HTML in the textarea, but it doesn't seem to save:
//$thisFrame.prev("textarea").html(encodeURIComponent("<body>" + html + "</body>"));
}
});
editorSaveEventInit = true;
}
}
};
$(window).load(function () {
InitSaveEvent();
});
Is there any way to access the Telerik RadEditor object with JavaScript (using OnClientCommandExecuted()?) so that I can access the .get_html() and .set_html(value) functions? If not, what values do I need to set before posting back?
Why don't you use custom content filters.
Ah, just discovered Telerik's built-in $find() function: http://www.telerik.com/help/aspnet-ajax/editor_getingreferencetoradeditor.html
Edit: here's the solution I came up with for my InitSaveEvent() function:
var editorSaveEventInit = false;
function InitSaveEvent() {
if (!editorSaveEventInit) {
var $EditFrames = $("iframe[id$='_contentIframe']");
if ($EditFrames && $EditFrames.length > 0) {
$EditFrames.bind("save", function (e) {
var $thisFrame = $(this);
var thisFrameContents = $thisFrame.contents();
if (thisFrameContents) {
var telerikContentIFrame = thisFrameContents.get(0);
var $body = $("body", telerikContentIFrame);
var html = $.fixHash($body).html();
// SOLUTION!
var $radeditor = $thisFrame.parents("div.RadEditor.Telerik:eq(0)");
var editor = $find($radeditor.attr("id"));
editor.set_html(html);
// ☺
}
});
editorSaveEventInit = true;
}
}
};