I'm currently working on a drag and drop plugin. I want to add a feature so the user can limit the amount of times the drggable object can be dragged and dropped. I will call this feature, dragLimitation.
This is what I have so far:
var limit = 0;
$(document).ready(function() {
$(oj).mouseup(function() {
if (o.dragLimitation !== false) {
if (limit > (o.dragLimitation-1)) {
//Code to Stop Drag Here
} else {
limit++;
$('#back').html(limit);
}
}
});
});
About the Code: There are couple of things I want to get clear to you guys so I can get an answer.
The var, oj in: $(oj).mouseup(function() { is just referring to this. In this case this would be: $('#drag').jDrag();, which is just to get my plugin running.
#drag = this
One thing I want to point out is o.dragLimitation. This is to get the amount of times the drag and drop item/object can be dropped(mouseup).
Example:
$('#drag').jDrag({
dragLimitation: 20
});
This would make #drag be able to be dragged and dropped 20 times.
I got a lot of the code but I just don't know how to stop the element from dragging. I don't to break the code either using:
$('body').append('<span>');
So the users can still use the rest of the page.
Here is where the code for the stop dragging should be:
if (limit > (o.dragLimitation - 1)) {
//Code to Stop Drag Here
}
I really hope someone can help me with the details I gave.
Thanks for any help.
(function($) {
$.extend($.fn, {
jDrag: function() {
var dragCount = this.data("jDrag.dragCount"),
limitation = this.data("jDrag.limitation");
if(typeof dragCount !== "number" || !isFinite(dragCount))
{
/*
* Drag count isn't a valid number.
* Give it a 0 value, and save it to the target.
*/
dragCount = 0;
this.data("jDrag.dragCount", dragCount);
}
if(typeof limiation !== "number" || !isFinite(limitation))
{
/*
* Limitation isn't a valid number.
* Load default limitation from plugin defaults.
*/
limitation = $.data("jDrag.defaults").limitation;
}
if(dragCount <= limitation)
{
/*
* Drag limitation isn't yet exceeded, increment count
* and save it to the target.
*/
this.data("jDrag.dragCount", ++dragCount);
/* Continue code here. */
}
}
});
})(jQuery);
Refer to jQuery.data method for internally storing data for your plugins. As you can probably see, the code above simply loads the amount of times the target has been dragged, and the limitation placed on the target. If the dragCount isn't a valid number, it gives it a dragCount of zero (, later changed to 1). If the limitation isn't a valid number, it loads the plugin's default limitation stored in the plugin's data.
Notice that we used this.data, but we later used $.data. When using this.data, this information is stored on the specific element. Using $.data will load data for the plugin itself. Since we use this.data to store information on the specific element, you can easily store different drag limitations on different elements, instead of making them all share the same limitations. If you follow this outline, you shouldn't have any problems.
Note: I didn't test this script. It is just a basic idea.
Update: Added comments to code for better understanding.
You don't necessarily need any code to stop the drag/drop functionality, you should just wrap the code for the drag/drop functionality in an if that checks if the limitation has been met. If it has then the dragging/dropping code wont execute.
Related
I want to use either a slider question or a draggable bar chart in Qualtrics to present to respondents how they answered in former questions. More specifically, I compute a value out of then answers with weighting, and want the slider or bar to be positioned at this value.
Notably, as each respondent has a value (stored in an embedded data field), the position will thereby be individual for each respondent. Piping only works for text fields, as far as I understood the support page.
Based on this question/answer I came to the following code for the bar graph:
Qualtrics.SurveyEngine.addOnload(function()
{
var result = "${q://Field/result}";
var qwidth = $('QID1936~1~track').offsetWidth;
var resrec = ((qwidth*result)/100);
$('QID1936').select('.bar').each(function(name, index) {
name.setStyle({ width: resrec +"px"});
});
});
Basically, I get the result for each respondent out of the embedded data, get the width of the full bar graph, compute the ratio that should be colored based on the result, and update the position of the bar graph (following the mentioned answer).
Funny enough, everything works when done in the console. Also, the embedded data is correctly loaded, qwidth as well.
Two problems arise: it seems resrec could be computed wrongly, as a console.log() spits out 0 instead of the correct value. I assumed this could be somehow as a variable is not recognized as number, but several tries with Number() or 0+var did not change how this works in Qualtrics. In the console, it works just fine.
Also, no update of the bar (or slider with similar code) happens, neither with the correct value nor with the 0 that is produced by Qualtrics.
I search for two things: either a solution to the Javascript problem as described, basically how I can update the bar or slider with embedded data. Or another solution how to directly get embedded data into one of those two question formats as a starting value for each respondent individually.
Thanks for your help or ideas!
Try this:
Qualtrics.SurveyEngine.addOnload(function()
{
var qid = this.questionId;
var result = parseFloat("${e://Field/result}");
var qwidth = $(qid+'~1~track').offsetWidth;
var resrec = ((qwidth*result)/100);
$(qid).select('.bar').each(function(name, index) {
name.style.width = resrec + "px";
});
});
Notes:
It is best not to use a hardcoded QID
In a pipe use e: to refer to an embedded variable. q: is for questions.
Use parseFloat to convert the string to a number
No need to use setStyle if you are only setting one value
One solution proposed by Qualtrics support: when you use bars and/or sliders, piped values are actually possible.
The trick is to have the value of the bar/slider shown (a thing we do not use in the whole survey elsewhere). Then, you can access over the Advanced Question Options > Add Default Choices the blue arrow for piping text behind the value. Through this, the value is individually set to either embedded data or another answer.
Note, however, to tick "Show value" before accessing the default choices, else you will only be able to drag around the bar and set it for all simultaneously.
Here is a solution using the Qualtrics Question API method setChoiceValue that does not require you to compute the ratio and update the length of the bars manually.
Below is an example code for the result of ten respondents saved in embedded data from previous questions.
Qualtrics.SurveyEngine.addOnload(function()
{
var embedded = ["${e://Field/r1}", "${e://Field/r2}",
"${e://Field/r3}", "${e://Field/r4}", "${e://Field/r5}",
"${e://Field/r6}", "${e://Field/r7}", "${e://Field/r8}",
"${e://Field/r9}", "${e://Field/r10}"];
for (var i = 0; i < 11; i++) {
var index = i + 1;
var choiceInput = embedded[i];
this.setChoiceValue(index, choiceInput);
}
});
For one respondent:
Qualtrics.SurveyEngine.addOnload(function()
{
var value = "${e://Field/r1}";
this.setChoiceValue(1, value);
});
Inside of a module I'm writing (its kind of a slider / timeline interface component) I've got a method that updates the controls which are a set of clickable elemetns along the bottom that are updated on click and when the user scrolls.
I'm doing the following to attach classes to the items up until the active one. While the approach I'm using works, its feels very inefficient as I'm looping over a set of DOM elements each time.
updateTimeLine : function(pos, cb) {
var p = pos;
var timeline = $('.timer').toArray();
if (p > 15)
p = 15;
$.each(timeline, function(index,value) {
var that = $(this);
if (index >= p) {
if (that.children('span').hasClass('active'))
that.children('span').removeClass('active');
} else {
that.children('span').addClass('active');
}
});
if (cb && typeof(cb) === "function") {
cb();
}
return this;
},
Is there a better way to do this? If so, how?
Is this a good use case for something like the observer pattern? which I don't fully get, having not spent any time with it yet, so if it is, I'd really like to know how to apply this pattern properly.
Observer patterns notify subscribed objects by looping through and invoking listeners on each subscriber when a relevant change occurs. Because of that, you'd probably end up using $.each anyways. I think what you have is equally efficient.
If you feel bad about iterating over the dom each time, consider this: there exists no such algorithm that can update each dom element without iterating through them. Caching the DOM array theoretically would improve performance, but my money says the browser's already doing that. Try it yourself on this jsperf...
Love the Twitter Bootstrap typeahead functionality but I have a small tinkering I'd like to do - I want to have the best match (or first match is probably simpler) result in the drop-down be autofill in the input box.
The functionality I'm looking for is exactly the way the Google Chrome Omnibox works
Anyone know of a solution for this?
Playing a bit, I come up with that code.
Live demo (jsfiddle)
$('.omnitype').each(function() {
var $omnitype = $(this);
$omnitype.typeahead({
source: mySource,
items: 4,
sorter: function(items) {
// Bootstrap code
/* ... */
// Modified code (delay the return of sorted)
var sorted = beginswith.concat(caseSensitive, caseInsensitive);
// if there is a first element, we fill the input select what we added
var first = sorted.length && sorted[0];
if (first) {
var origin = this.$element.val();
this.$element.val(first);
createSelection(this.$element.get(0), origin.length, first.length);
}
// back to the intended behavior
return sorted;
}
});
});
I used this answer to provide the createSelection function (not sure if really needed).
Still needs quite some tweaking, as the keys doesn't work as expected. It may be easier to recode the whole plugin with some checking that would give naturally the intuitive behavior.
Or you may be able to catch the key events before the plugin and do what is appropriate.
I have some JavaScript that I wrote in a pinch, but I think it could be optimized greatly by someone smarter than me. This code runs on relatively small objects, but it runs a fair amount of times, so its worth getting right:
/**
* Determine the maximum quantity we can show (ever) for these size/color combos
*
* #return int=settings.limitedStockThreshold
*/
function getMaxDefaultQuantity() {
var max_default_quantity = 1;
if (inventory && inventory.sizes) {
sizecolor_combo_loop:
for (var key in inventory.sizes) {
if (inventory.sizes[key].combos) {
for (var key2 in inventory.sizes[key].combos) {
var sizecolor_combo = inventory.sizes[key].combos[key2];
if (isBackorderable(sizecolor_combo)) {
//if even one is backorderable, we can break out
max_default_quantity = settings.limitedStockThreshold;
break sizecolor_combo_loop;
} else {
//not backorderable, get largest quantity (sizecolor_combo or max_default_quantity)
var qoh = parseInt(sizecolor_combo.quantityOnHand || 1);
if (qoh > max_default_quantity) {
max_default_quantity = qoh;
};
};
};
};
};
};
return Math.min(max_default_quantity, settings.limitedStockThreshold);
};
First, inventory is a object returned via JSON. It has a property inventory.sizes that contain all of the available sizes for a product. Each size has a property inventory.sizes.combos which maps to all of the available colors for a size. Each combo also has a property quantityOnHand that tells the quantity available for that specific combo. (the JSON structure returned cannot be modified)
What the code does is loop through each size, then each size's combos. It then checks if the size-color combo is backorderable (via another method). If it any combo is backorderable, we can stop because the default quantity is defined elsewhere. If the combo isn't backorderable, the max_default_quantity is the largest quantityOnHand we find (with a maximum of settings.limitedStockThreshold).
I really don't like the nested for loops and my handling of the math and default values feels overly complicated.
Also, this whole function is wrapped in a much larger jQuery object if that helps clean it up.
Have you considered using map-reduce? See a live example of a functional approach.
This particular example uses underscore.js so we can keep it on a elegant level without having to implement the details.
function doStuff(inventory) {
var max = settings.limitedStockThreshold;
if (!(inventory && inventory.sizes)) return;
var quantity = _(inventory.sizes).chain()
.filter(function(value) {
return value.combos;
})
.map(function(value) {
return _(value.combos).chain()
.map(function(value) {
return isBackorderable(value) ? max : value.quantityOnHand;
})
.max().value();
})
.max().value();
return Math.min(quantity, max);
}
As for an explanation:
We take the inventory.sizes set and remove any that don't contain combos. We then map each size to the maximum quantity of it's colour. We do this mapping each combo to either its quantity or the maximum quantity if backordable. We then take a max of that set.
Finally we take a max of set of maxQuantities per size.
We're still effectily doing a double for loop since we take two .max on the set but it doesn't look as dirty.
There are also a couple of if checks that you had in place that are still there.
[Edit]
I'm pretty sure the above code can be optimized a lot more. but it's a different way of looking at it.
Unfortunately, JavaScript doesn't have much in the way of elegant collection processing capabilities if you have to support older browsers, so without the help of additional libraries, a nested loop like the one you've written is the way to go. You could consider having the values precomputed server-side instead, perhaps cached, and including it in the JSON to avoid having to run the same computations again and again.
Context: HTML widgets generated using a Django ModelForm and template, jQuery 1.3.2, JavaScript on IE8, Firefox 3.5 or Safari 4. Procedures: An ordinary JavaScript function with some jQuery inside, or jQuery Enlightenment, Cody Lindley, "Adding new functions to the jQuery namespace," p. 116.
I have a jQuery construct that is repeated several times with different variables and so has been begging to be turned into a function. Basically some of the widgets need to be enabled or disabled based on the values of other widgets. I wrote this function two ways. The first way, with jQuery code inside an ordinary JavaScript function, is as follows:
function enable_or_disable_by_selection(master_id, master_id_value, dependent_ids) {
/*
* The master_id argument is the id attribute string of the master select element in the form
* "#id_select_element_name".
* The master_id_value argument is the selection value, a string, that causes the dependent
* elements to be enabled when it is selected. In all other cases, they are disabled.
* The dependent_ids argument is an array of dependent id attribute strings, such as
* ["#id_element_1", "#id_element_2", "#id_element_3"]
*/
/* ON CHANGE OF master_id SELECTION ELEMENT: */
$(master_id).change(function() {
/* If master_id_value is chosen, enable inputs for elements in dependent_ids: */
if ($(master_id).val() == master_id_value) {
for (var i = 0; i < dependent_ids.length; i++) {
$(dependent_ids[i]).removeAttr("disabled");
}
}
/* Otherwise disable inputs for elements in dependent_ids: */
else {
for (var i = 0; i < dependent_ids.length; i++) {
$(dependent_ids[i]).attr("disabled", true);
}
}
});
}
This works. The second way, recommended by the very able Mr. Lindley, puts my new function in the jQuery namespace. His basic recommended construct may be seen here. This helps me "avoid creating global code that could potentially create conflicts." Here's the code for my function following these recommendations:
(function($){
$.enable_or_disable_by_selection = function(master_id, master_id_value, dependent_ids){
/*
* The master_id argument is the id attribute string of the master select element in the form
* "#id_select_element_name".
* The master_id_value argument is the selection value, a string, that causes the dependent
* elements to be enabled when it is selected. In all other cases, they are disabled.
* The dependent_ids argument is an array of dependent id attribute strings, such as
* ["#id_element_1", "#id_element_2", "#id_element_3"]
*/
/* CHANGE OF master_id SELECTION ELEMENT: */
$(master_id).change(function() {
/* If master_id_value is chosen, enable inputs for elements in dependent_ids: */
if ($(master_id).val() == master_id_value) {
for (var i = 0; i < dependent_ids.length; i++) {
$(dependent_ids[i]).removeAttr("disabled");
}
}
/* Otherwise disable inputs for elements in dependent_ids: */
else {
for (var i = 0; i < dependent_ids.length; i++) {
$(dependent_ids[i]).attr("disabled", true);
}
}
});
};
})(jQuery);
This works too. It's the same logic wrapped up in jQuery.
I know that jQuery makes use of anonymous functions and closures in order to maintain a hermetic namespace. See "Using (function(){})()" by John Resig, here. But, as I study these two snippets, I am having a hard time seeing the risk in the first, simpler version. Also, am I wrong in thinking that this second method is a bit slower? Please help me see the advantages in the second version. I want to know why this is done.
Useful observations not pertinent to the question are always welcome.
1 - The risk on your first version comes with the fact that your function is global, for example, some other random library is added to your page, and it overrides your function, it will have disastrous effects with the rest of the API of your widget.
I think you want to create redistributable widgets, and you must be defensive with your code, keeping as much your of your library code private as possible and you should be very selective and careful when you introduce global objects.
2 - The second example is not more slower, you are simply adding a member to the jQuery object, the time of name resolution of the property is really insignificant.
I would recommend you to read about namespacing and library design:
JavaScript Namespacing
Namespacing your JavaScript
Best Practices in JavaScript Library Design (Video and Slides)