jQuery Slider, Combined Values - Having difficulty with Small Increments - jquery-ui

I have designed a jQuery Slider Control that takes a 'pool' of points, and allows them to be distributed between multiple sliders. It works pretty well, except that I am having some problems with very small overflow increments.
Basically, it is possible to make adjustments in large quantities based on mouse movement, and so it lets someone 'spend' more in a slider than intended. I am at a loss as to how to deal with this. Posted below is my entire code.
To test it, build a simple HTML page with this code and try sliding the first two sliders all the way to 500, then try sliding the third. It won't slide (intended behavior).
Then, slide the first or second slider back a little bit (subtracting), and slide the third forward. You are able to occasionally go over the intended 'spendable' bounds.
Sample will require jQuery UI, latest version from google CDN.
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.js"></script>
Javascript
<style type="text/css">
#eq > div {
width:300px;
}
#eq > div .slider {
width: 200px; float: left;
}
#eq > div > span {
float: right;
}
</style>
<script type="text/javascript">
$(document).ready(function () {
var spendable = 1000;
var spent = 0;
function spend(quantity) {
spent += quantity;
$('#attemptedToSpend').text(spent);
}
function change(previous, current) {
var adjustment = (current - previous);
$('#change').text(adjustment);
return adjustment;
}
function calculateSpent() {
var totalled = 0;
$("#eq .slider").each(function () {
totalled += parseInt($(this).parent('div:eq(0)').find('.spent').text());
});
$('#spent').text(totalled);
}
$("#eq .slider").each(function () {
var current = 0;
var previous = 0;
var adjustment = 0;
$(this).slider({
range: "min",
value: 0,
min: 0,
max: 500,
step: 1,
animate: true,
orientation: "horizontal",
start: function (event, ui) {
},
stop: function (event, ui) {
},
slide: function (event, ui) {
// set the current value to whatever is selected.
current = ui.value;
// determine the adjustment being made relative to the last
// adjustment, instead of just the slider's value.
adjustment = change(previous, current);
if (spent >= spendable) {
if (adjustment > 0)
return false;
}
// spend the points, if we are able to.
spend(adjustment);
// set the previous value
previous = current;
$(this).parent('div:eq(0)').find('.spent').text(current);
calculateSpent();
}
});
});
});
</script>
Html
<p class="ui-state-default ui-corner-all" style="padding: 4px; margin-top: 4em;">
<span style="float: left; margin: -2px 5px 0 0;"></span>Distribution
</p>
<strong>Total Points Spendable: </strong><div id="spendable">1000</div>
<strong>Total Points Spent (Attempted): </strong><div id="attemptedToSpend">0</div>
<strong>Total Points Spent: </strong><div id="spent">0</div>
<strong>Change: </strong><div id="change">0</div>
<div id="status"></div>
<div id="eq">
<div style="margin: 15px;" id="test1">Test1</div>
<br />
<div class="slider"></div><span class="spent">0</span>
<div style="margin: 15px;" id="test2">Test2</div>
<br />
<div class="slider"></div><span class="spent">0</span>
<div style="margin: 15px;" id="test3">Test3</div>
<br />
<div class="slider"></div><span class="spent">0</span>
<div style="margin: 15px;" id="test4">Test4</div>
<br />
<div class="slider"></div><span class="spent">0</span>
<div style="margin: 15px;" id="test5">Test5</div>
<br />
<div class="slider"></div><span class="spent">0</span>
</div>

I tried keeping your script intact, but ended up largely rewriting it. It should be solid now. Good news: I have only changed the JS, everything else is intact, though I don't update all your monitoring fields any more.
DEMO
$(
function()
{
var
maxValueSlider = 500,
maxValueTotal = 1000,
$sliders = $("#eq .slider"),
valueSliders = [],
$displaySpentTotal = $('#spent');
function arraySum(arr)
{
var sum = 0, i;
for(i in arr) sum += arr[i];
return sum;
}
$sliders
.each(
function(i, slider)
{
var
$slider = $(slider),
$spent = $slider.next('.spent');
valueSliders[i] = 0;
$slider
.slider(
{
range: 'min',
value: 0,
min: 0,
max: maxValueSlider,
step: 1,
animate: true,
orientation: "horizontal",
slide:
function(event, ui)
{
var
sumRemainder = arraySum(valueSliders) - valueSliders[i],
adjustedValue = Math.min(maxValueTotal - sumRemainder, ui.value);
valueSliders[i] = adjustedValue;
// display the current total
$displaySpentTotal.text(sumRemainder + adjustedValue);
// display the current value
$spent.text(adjustedValue);
// set slider to adjusted value
$slider.slider('value', adjustedValue);
// stop sliding (return false) if value actually required adjustment
return adjustedValue == ui.value;
}
}
);
}
);
}
);

Related

jQuery Droppable: Only allow element to drop, if there is no other element in that dropzone

I'm writing a puzzle, where you have to drag an item into the correct dropzone.
Problem: I want that you can only drag an item into a dropzone, if that dropzone does not contain any other items. How can I check, whether there are no other items in that dropzone?
Here is a gif of my current puzzle:
Here is a gif which shows the problem:
As you can see, I can drag multiple items into the same dropzone.
If a dropzone already contains an item, the user should not be able to drop another item into that dropzone. How do I achieve that?
My script so far:
$( ".draggable" ).draggable({ revert: 'invalid', snap: ".dropfield", snapTolerance: 30, snapMode: "inner"});
$( ".dropfield" ).droppable({
accept: ".dropling",
drop: function( event, ui ) {
if(some-condition){ // if correct word got dragged into the correct dropzone
var id = ui.draggable.attr('id');
$("#" + id).draggable( 'disable' );
$(this).droppable( 'disable' );
$("#" + id).css( "background-color", "#7FFF00");
}
});
Html-excerpt:
<div id="liebe" class="dropling draggable text-center">
Liebe
</div>
<span class="dropfield" value="scheitern">
</span>
PS: There are already several topics on Stack-Overflow with the same question. However, I'm not intelligent enough to apply the suggested answers to my case. Please help me.
Edit1
Here is a gif which shows my preferred behavior:
I dragged a wrong word into a dropzone. But as long that dropzone is occupied by a word, no other words should be able to be dropped into that dropzone.
My current code:
if(some-condition){ //correct word
$("#" + id).draggable( 'disable' );
$(this).droppable( 'disable' );
$("#" + id).css( "background-color", "#7FFF00");
}
} else { //wrong word
console.log("wrong word dropped");
$(this).droppable( 'disable' );
}
As soon as I drag the wrong word out of the dropzone, the dropzone should become enabled again. But how can I achieve that?
I would advise breaking this into their own functions. This way you can enable and disable drop repeatedly. Not sure what you want to trigger the item to become draggable and droppable again based on the example you have supplied. Based on what you have supplied, I can offer this the following example.
$(function() {
function enableDrop($target) {
console.log("Enabled Drop");
$target.droppable({
accept: ".dropling",
classes: {
"ui-droppable-hover": "drop-target"
},
drop: function(event, ui) {
var $that = $(this),
dragWord = ui.draggable.text().trim(),
$item = ui.draggable;
if (checkWord(dragWord)) {
console.log("Accepted: " + $item.attr("id"));
$item.
removeClass("draggable")
.draggable('disable')
.attr("style", "")
.appendTo($that);
disableDrop($that);
$that.css("background-color", "#7FFF00");
} else {
return false;
}
}
});
}
function disableDrop($target) {
console.log("Disabling Drop on " + $target.attr("class"));
$target.droppable("destroy");
}
function checkWord(w) {
var result = false;
console.log("Checked Word: " + w);
if (w == "Liebe") {
result = true;
}
return result;
}
$(".draggable").draggable({
revert: 'valid',
snap: ".dropfield",
snapTolerance: 30,
snapMode: "inner"
});
enableDrop($(".dropfield"));
});
p .dropfield {
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
width: 4em;
height: 1.5em;
margin-bottom: -.25em
}
p .drop-target {
border: 1px dashed #ccc;
background-color: #ccc;
}
.text-center {
text-align: center;
}
.draggable {
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
width: 4em;
height: 1em;
padding: .25em 0;
margin-bottom: -.25em
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<p>Diese Schlussfolgerung ist <span class="dropfield" value="scheitern"></span>: Ee kann doch nicht sein, dass es gut ist, </p>
<div id="liebe" class="dropling draggable text-center">Liebe</div>
<div id="absurd" class="dropling draggable text-center">absurd</div>
The easiest way here is probably to solve the whole thing in a more generic way. For this I would add an attribute to the respective Dom element (data-count) and then check how many characters are contained and how many are still allowed:
See /** ADDED **/ for the things i did:
$(function() {
function textWrapper(str, sp, btn) {
if (sp == undefined) {
sp = [0, 0];
}
var txt = "";
if (btn) {
txt = "<span class='w b'>" + str + "</span>";
} else {
txt = "<span class='w'>" + str + "</span>";
}
if (sp[0]) {
txt = " " + txt;
}
if (sp[1]) {
txt = txt + " ";
}
return txt;
}
function chunkWords(p) {
var words = p.split(" ");
words[0] = textWrapper(words[0], [0, 1]);
var i;
for (i = 1; i < words.length; i++) {
var re = /\[.+\]/;
if (re.test(words[i])) {
var b = makeTextBox(words[i].slice(1, -1));
words[i] = " " + b.prop("outerHTML") + " ";
} else {
if (words[0].indexOf(".")) {
words[i] = textWrapper(words[i], [1, 0]);
} else {
words[i] = textWrapper(words[i], [1, 1]);
}
}
}
return words.join("");
}
function unChunkWords(tObj) {
var words = "";
$(tObj).contents().each(function(i, el) {
if ($(el).hasClass("b")) {
words += "[" + $(el).text() + "]";
} else {
words += $(el).text();
}
});
return words.replace(/\s+/g, " ").trim();
}
function makeBtn(tObj) {
var btn = $("<span>", {
class: "ui-icon ui-icon-close"
}).appendTo(tObj);
}
function makeTextBox(txt) {
var sp = $("<span>", {
class: "w b"
}).html(txt);
makeBtn(sp);
return sp;
}
function makeDropText(obj) {
return obj.droppable({
drop: function(e, ui) {
var txt = ui.draggable.text();
var newSpan = textWrapper(txt, [1, 0], 1);
$(this).after(newSpan);
makeBtn($(this).next("span.w"));
makeDropText($(this).next("span.w"));
$("span.w.ui-state-highlight").removeClass("ui-state-highlight");
update()
},
over: function(e, ui) {
$(this).add($(this).next("span.w")).addClass("ui-state-highlight");
},
out: function() {
$(this).add($(this).next("span.w")).removeClass("ui-state-highlight");
}
});
}
$("p.given").html(chunkWords($("p.given").text()));
$("p.given").on("click", ".b > .ui-icon", function() {
$(this).parent().remove();
});
$("p.given").blur(function() {
var w = unChunkWords($(this));
$(this).html(chunkWords(w));
makeDropText($("p.given span.w"));
});
$("span.given").draggable({
helper: "clone",
revert: "invalid"
});
makeDropText($("p.given span.w"));
/** ADDED **/
// update at beginning
update();
// register update events
$("p.given").on('click keydown keyup drag drop', update);
function update(e) {
var templateText = unChunkWords($("p.given"));
var templateTextWithoutParameters = templateText.replace(/\[(.+?)\]/g, "");
var templateTextWithoutParametersLenght = templateTextWithoutParameters.length;
// calc total length
var totalLength = templateTextWithoutParametersLenght;
// since 'helper: clone' we have to ignore it!
$("[data-count]:not(.ui-draggable-dragging)").each(function(index, item) {
var count = $(item).attr("data-count")
var text = "[" + $(item).text() + "]";
var length = templateText.split(text).length - 1;
totalLength += count * length;
});
// 46,8 keycodes for delete & backspace
var maxLength = 200;
if (totalLength >= maxLength && e && e.keyCode !== 46 && e.keyCode !== 8) {
e.preventDefault();
}
// disable data counts
var remaining = maxLength - totalLength;
$("[data-count]:not(.ui-draggable-dragging)").each(function(index, item) {
var count = $(item).attr("data-count");
if (parseInt(count) > remaining) {
$(item).attr("disabled", true);
$(item).draggable().draggable('disable');
} else {
$(item).attr("disabled", false);
$(item).draggable().draggable('enable');
}
})
$(".output").text(totalLength);
}
});
p.given {
display: flex;
flex-wrap: wrap;
}
p.given span.w span.ui-icon {
cursor: pointer;
}
div.blanks {
display: inline-block;
min-width: 50px;
border-bottom: 2px solid #000000;
color: #000000;
}
div.blanks.ui-droppable-active {
min-height: 20px;
}
span.answers>b {
border-bottom: 2px solid #000000;
}
span.given {
margin: 5px;
}
/** ADDED **/
[disabled] {
color: grey
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div class="row">
<p class="given" contenteditable="true">Lorem Ipsum is [Test] Ipsum has been the industry's [America] standard dummy text ever since the 1500s, </p>
</div>
<div class="divider"></div>
<div class="section">
<section>
<div class="card blue-grey ">
<div class="card-content white-text">
<div class="row">
<div class="col s12">
<span class="given btn-flat white-text red lighten-1" rel="1" data-count="50">Test</span>
<span class="given btn-flat white-text red lighten-1" rel="2" data-count="30">America</span>
<span class="given btn-flat white-text red lighten-1" rel="3" data-count="20">Qatar</span>
<span class="given btn-flat white-text red lighten-1" rel="4" data-count="10">Philippines</span>
</div>
</div>
</div>
</div>
</section>
</div>
<div class="divider"></div>
Count: <span class="output"></span>

Revert draggable after a different draggable is dropped

I have a 2x2 grid of droppable areas [[A,B][C,D]] and under the grid is a 1x4 grid of draggables. I only want certain draggables next to each other. So for example, if there is a draggable in B, and I drag a different draggable into A, is there a way to make the draggable in B revert? The draggables have data-row and data-col so that I can grab the draggable in the prev/next column if I need to.
$(".draggable").draggable({
scroll: false,
snap: ".snaptarget",
snapMode: "inner",
stack: ".draggable",
revert: function (event, ui) {
var $draggable = $(this);
$draggable.data("uiDraggable").originalPosition = {
top: 0,
left: 0
};
return !event;
}
});
$(".snaptarget").droppable({
accept: ".draggable",
drop: function (event, ui) {
var $draggable = $(ui.draggable);
var $droppable = $(this);
// This droppable is taken, so don't allow other draggables
$droppable.droppable('option', 'accept', ui.draggable);
ui.draggable.position({
my: "center",
at: "center",
of: $droppable,
using: function (pos) {
$draggable.animate(pos, "fast", "linear");
}
});
// Disable prev or next droppable if the pagewidth == 1
if ($droppable.data("col") == 1) {
$droppable.next().droppable("option", "disabled", true);
var nextDrag = $(".draggable[data-row='" + $droppable.data("row") + "'][data-col='2']");
if (nextDrag.length) {
// I need to revert nextDrag if there is one.
// I've tried this but it doesn't seem to work
nextDrag.data("uiDraggable").originalPosition = {
top: 0,
left: 0
}
}
}
},
tolerance: "pointer"
});
Took a little bit of work, I am never good with offsets and positioning. Here's the key:
function returnItem(item, target) {
// Get Origin
var oPos = item.data("uiDraggable").originalPosition;
// Adjust Postion using animation
item.position({
my: "top left",
at: "top left+" + oPos.left,
of: target,
using: function(pos) {
item.animate(pos, "fast", "linear");
}
});
}
Here is a working example based on the Draggable Snap to element grid example:
https://jsfiddle.net/Twisty/a4ucb6y3/6/
HTML
<div id="target">
<div class="snaptarget ui-widget-header" data-col="1" data-row="1" style="top: 0; left: 0;">
</div>
<div class="snaptarget ui-widget-header" data-col="2" data-row="1" style="top: 0; left: 80px;">
</div>
<div class="snaptarget ui-widget-header" data-col="1" data-row="2" style="top: 80px; left: 0;">
</div>
<div class="snaptarget ui-widget-header" data-col="2" data-row="2" style="top: 80px; left: 80px;">
</div>
</div>
<br style="clear:both">
<div id="source">
<div id="drag-A" class="draggable ui-widget-content" style="left: 0;">
<p>Drag A</p>
</div>
<div id="draggable2" class="draggable ui-widget-content" style="left: 80px;">
<p>Drag B</p>
</div>
<div id="draggable3" class="draggable ui-widget-content" style="left: 160px;">
<p>Drag C</p>
</div>
<div id="draggable4" class="draggable ui-widget-content" style="left: 240px;">
<p>Drag D</p>
</div>
</div>
CSS
.draggable {
width: 80px;
height: 80px;
font-size: .9em;
position: absolute;
top: 0;
}
.draggable p {
text-align: center;
height: 1em;
margin-top: 30px;
}
#source {
width: 320px;
height: 80px;
position: relative;
}
#target {
width: 160px;
height: 160px;
position: relative
}
.snaptarget {
width: 80px;
height: 80px;
position: absolute;
}
jQuery
$(function() {
function returnItem(item, target) {
// Get Origin
var oPos = item.data("uiDraggable").originalPosition;
// Adjust Postion using animation
di.position({
my: "top left",
at: "top left+" + oPos.left,
of: target,
using: function(pos) {
item.animate(pos, "fast", "linear");
}
});
}
$(".draggable").draggable({
scroll: false,
snap: ".snaptarget",
snapMode: "inner",
stack: ".draggable",
revert: "invalid",
start: function(e, ui) {
var off = $("#source").position();
ui.helper.data("uiDraggable").originalPosition = {
top: ui.position.top,
left: ui.position.left
};
}
});
$(".snaptarget").droppable({
accept: ".draggable",
drop: function(event, ui) {
var $draggable = $(ui.draggable);
var $droppable = $(this);
// This droppable is taken, so don't allow other draggables
$droppable.droppable('option', 'accept', ui.draggable);
// Disable prev or next droppable if the pagewidth == 1
if ($droppable.data("col") == 1) {
$droppable.next().droppable("option", "disabled", true);
var nextDrag = $(".draggable[data-row='" + $droppable.data("row") + "'][data-col='2']");
if (nextDrag.length) {
// I need to revert nextDrag if there is one.
returnItem(nextDrag, $("#source"));
}
}
},
tolerance: "pointer"
});
});
In draggable, when we start to drag, we want to record the original position (in case we need to later revert). The revert option is set to invalid in case the user drags it off some other place weird.
We add the position to data of the dragged item so that it can be read later.
When that item is dropped is when the magic happens. You had done all the checking, just needed to return the item if it didn't fit. if nextDrag exists, we return it to it's source.
Going forward, you may want to consider appending, cloning, and removing the elements in the start/stop events. As it is now, we're really only adjust the positioning of the elements, not their hierarchy in the DOM. Depending on what your needs are, this may not matter.

Ionic Framework textarea keyboard scroll issue after Apple Testflight processing

We are using the ionic framework with iOS.
In the iOS emulator and in Safari browser, for one of our pages, clicking in a textarea shows the keyboard, and scrolls the textarea upwards so it is still viewable.
When the app is archived and processed through Apple iOS TestFlight, the behaviour is changed. Now, clicking in a textarea shows the keyboard, but the textarea no longer scrolls upwards so it is hidden.
Looks like something in the archival process is causing an issue.
Here's the code (there's another div above it). There's only the one textarea.
<div ng-if="!dy_compl">
<div class="wi-bottom item ">
<div ng-repeat="(key, dy) in element.dys">
<div id="wi-scroll-div" ng-if="key == dySel" style="height: {{scroller_height}}; overflow: scroll;">
<div>
<style>
p.wi-icon:before {
background: url("img/old_building.png") no-repeat !important;
}
</style>
<p class="wi-icon" ng-bind-html="dy.intro | to_trusted"></p>
</div>
<div ng-if="dy.ref">
<p class="wi-intro-my3" ng-bind-html="dy.ref.intro | to_trusted"></p>
<div ng-repeat="data in dy.ref.data track by $index">
<p class="wi-intro-my3-table" style="margin-left: 5%;" ng-bind-html="data | to_trusted"></p>
</div>
</div>
<label id="wi-input" class="item item-input item-stacked-label">
<span class="input-label" style="width:100%; max-width: 100%;">
<div class="wi-bottom-input-label" ng-bind-html="dy.notelabel | to_trusted"></div>
</span>
<textarea class="wi-bottom-input" ng-model="dy.note" type="text" placeholder="{{dy.note}}" ng-style="{'background-color': textAreaBackgroundColor}"></textarea>
</label>
<button class="wi-bottom-button button button-assertive col text-center" ng-click="dy.saved=true;saveNow()">
Save Notes
</button>
</br>
</div>
</div>
</div>
If you can't make it work with the plugin, check out this code I used on one project.. Looks a bit overhead, but it works:
Template:
<ion-content scroll-handle="user-profile-scroll">
<textarea maxlength="160" ng-model="currentUser.bio" ng-readonly="!editMode || focusAddInterestInput" placeholder="Write your bio..." class="user-bio">< </textarea>
</ion-content>
Controller:
$scope.windowHeight = window.innerHeight;
$scope.keyboardHeight = 0;
$scope.$on('$ionicView.loaded', function() {
var scrollView = {scrollTo: function() { console.log('Could not resolve scroll delegate handle'); }};
$timeout(function() {
var instances = $ionicScrollDelegate.$getByHandle('user-profile-scroll')._instances;
instances.length && (scrollView = instances[instances.length - 1]);
}).then(function() {
$scope.unbindShowKeyboardHandler = $scope.$on('KeyboardWillShowNotification', function(evt, info) {
$scope.keyboardHeight = info.keyboardHeight;
var input = angular.element(document.activeElement);
var body = angular.element(document.body);
var top = input.prop('offsetTop');
var temp = angular.element(input.prop('offsetParent'));
var tempY = 0;
while (temp && typeof(temp.prop('offsetTop')) !== 'undefined') {
tempY = temp.prop('offsetTop');
top += tempY;
temp = angular.element(temp.prop('offsetParent'));
}
top = top - (scrollView.getScrollPosition().top || 0);
var inputHeight = input.prop('offsetHeight');
var requiredSroll = $scope.windowHeight - $scope.keyboardHeight > top + inputHeight + 11 ? 0 : $scope.windowHeight - $scope.keyboardHeight - top - inputHeight - 12;
$timeout(function(){ scrollView.scrollTo(0, - requiredSroll || 0, true); });
});
$scope.unbindHideKeyboardHandler = $scope.$on('KeyboardWillHideNotification', function(evt, info) {
console.log(evt, info);
$scope.keyboardHeight = 0;
$timeout(function() { scrollView.scrollTo(0, 0, true); });
});
$scope.$on('$destroy', function() {
$scope.unbindShowKeyboardHandler();
$scope.unbindHideKeyboardHandler();
});
});
});
andm finally in app.js:
window.addEventListener('native.keyboardshow', keyboardShowHandler);
window.addEventListener('native.keyboardhide', keyboardHideHandler);
function keyboardShowHandler(info){
$rootScope.$broadcast('KeyboardWillShowNotification', info);
}
function keyboardHideHandler(info){
$rootScope.$broadcast('KeyboardWillHideNotification', info);
}
Turns out that we had one view that was manually disabling the keyboard scroll using:
cordova.plugins.Keyboard.disableScroll(true)
We weren't re-enabling this on a switch to another view.
The result is that in the emulator, the scroll disabling didn't traverse the scope to the new page, whereas after archival and TestFlight, it did.
Thanks for the other answers, comments.

jquery ui sortables connect lists: copy items

I have two lists, I want both of them to be sortable and want to be able to copy (drag) items from list1 to list2 and vice versa.
http://jqueryui.com/demos/sortable/#connect-lists
Is what I want, but the items are moved, not copied.
I did a few experiments with draggables and droppables, but I can't get to to work keeping them sortable. For example: http://jsfiddle.net/tunafish/dvXmf/
OK here is my app; two lists of images, sortable and you can copy over from the connected list.
If an item already exists in the target it's disabled.
Hopefully useful to someone...
JSFiffle here: http://jsfiddle.net/tunafish/VBG5V/
CSS:
.page { width: 410px; padding: 20px; margin: 0 auto; background: darkgray; }
.album { list-style: none; overflow: hidden;
width: 410px; margin: 0; padding: 0; padding-top: 5px;
background: gray;
}
.listing { margin-bottom: 10px; }
.album li { float: left; outline: none;
width: 120px; height: 80px; margin: 0 0 5px 5px; padding: 5px;
background: #222222;
}
li.placeholder { background: lightgray; }
JS:
$("ul, li").disableSelection();
$(".album, .favorites").sortable({
connectWith: ".album, .favorites",
placeholder: "placeholder",
forcePlaceholderSize: true,
revert: 300,
helper: "clone",
stop: uiStop,
receive: uiReceive,
over: uiOver
});
$(".album li").mousedown(mStart);
var iSender, iTarget, iIndex, iId, iSrc, iCopy;
var overCount = 0;
/* everything starts here */
function mStart() {
// remove any remaining .copy classes
$(iSender + " li").removeClass("copy");
// set vars
if ($(this).parent().hasClass("listing")) { iSender = ".listing"; iTarget = ".favorites"; }
else { iSender = ".favorites"; iTarget = ".listing"; }
iIndex = $(this).index();
iId = $(this).attr("id");
iSrc = $(this).find("img").attr("src");
iCopy = $(iTarget + " li img[src*='" + iSrc + "']").length > 0; // boolean, true if there is allready a copy in the target list
// disable target if item is allready in there
if (iCopy) { $(iTarget).css("opacity","0.5").sortable("disable"); }
}
/* when sorting has stopped */
function uiStop(event, ui) {
// enable target
$(iTarget).css("opacity","1.0").sortable("enable");
// reset item vars
iSender = iTarget = iIndex = iId = iSrc = iCopy = undefined;
overCount = 0;
// reinit mousedown, live() did not work to disable
$(".album li").mousedown(mStart);
}
/* rolling over the receiver - over, out, over etc. */
function uiOver(event, ui) {
// only if item in not allready in the target
if (!iCopy) {
// counter for over/out (numbers even/uneven)
overCount++;
// if even [over], clone to original index in sender, show and fadein (sortables hides it)
if (overCount%2) {
if (iIndex == 0) { ui.item.clone().addClass("copy").attr("id", iId).prependTo(iSender).fadeIn("slow"); }
else { ui.item.clone().addClass("copy").attr("id", iId).insertAfter(iSender + " li:eq(" + iIndex + ")").fadeIn("slow"); }
}
// else uneven [out], remove copies
else { $(iSender + " li.copy").remove(); }
}
// else whoooooo
}
/* list transfers, fix ID's here */
function uiReceive(event, ui) {
(iTarget == ".favorites") ? liPrefix = "fli-" : liPrefix = "lli-";
// set ID with index for each matched element
$(iTarget + " li").each(function(index) {
$(this).attr("id", liPrefix + (index + 1)); // id's start from 1
});
}
HTML:
<div class="page">
<div class="container">
<h2>Photo Album</h2>
<ul class="listing album">
<li id="li-1"><img src="tn/001.jpg" /></li>
<li id="li-2"><img src="tn/002.jpg" /></li>
<li id="li-3"><img src="tn/003.jpg" /></li>
<li id="li-4"><img src="tn/004.jpg" /></li>
<li id="li-5"><img src="tn/005.jpg" /></li>
</ul>
</div>
<div style="clear:both;"></div>
<div class="container">
<h2>Favorites</h2>
<ul class="favorites album">
<li id="fli-1"><img src="tn/001.jpg" /></li>
<li id="fli-2"><img src="tn/002.jpg" /></li>
<li id="fli-3"><img src="tn/010.jpg" /></li>
</ul>
</div>
</div>
I have to say that FFish's answer to this has been incredibly helpful to me.
I'd make one suggestion; if the lists are being constantly changed the mousedown event seem to be called numerous times because of the re-registering of the event on all the child objects. It might be a bit of a kludge, but I've added an unbind first to ensure the mousedown event is only called once.
$(".album li").mousedown(mStart);
to
$(".album li").unbind('mousedown', mStart).mousedown(mStart);

Unable to get index from jQuery UI slider range

I'm having a hell of a time trying to get (what I thought was) a simple index from a collection of multiple sliders. The HTML is as follows:
<div id="left-values" class="line">
<span id="l1" style="padding: 0 1.8em;">0</span>
<span id="l2" style="padding: 0 1.8em;">0</span>
<span id="l3" style="padding: 0 1.8em;">0</span>
<span id="l4" style="padding: 0 1.8em;">0</span>
<span id="l5" style="padding: 0 1.8em;">0</span>
<span id="l6" style="padding: 0 1.8em;">0</span>
<span id="l7" style="padding: 0 1.8em;">0</span>
<span id="l8" style="padding: 0 1.8em;">0</span>
</div>
And the jQuery code is:
// setup audiometry sliders
$("#eq > span").each(function (e) {
// read initial values from markup and remove that
var value = parseInt($(this).text());
// var index = $(this).index; <- this didn't work.
$(this).empty();
$(this).slider({
value: value,
slide: function (event, ui) {
//console.log($(this).attr('id')); <- neither did this.
//console.log(index);
$('#left-values span:first').text(ui.value);
}
})
});
The problem is that jQuery UI - when creating a slider - replaces the existing HTML with its own markup. This includes any ID values and, for whatever reason, I can't get the index for a given slider to surface either. So I'm running out of ideas.
You can get the index like so:
$("#eq > span").each(function (index, Element) {
alert(index);
...
see http://api.jquery.com/each/
What you have works, maybe you have something else going on. Here's a stand-alone sample, watch the console for output: http://jsfiddle.net/FBh3a/1/
$("#eq > span").each(function (e) {
var value = parseInt($(this).text());
$(this).empty();
$(this).slider({
value: value,
min: -10,
max: 10,
slide: function (event, ui) {
console.log($(this).attr('id')); //<- works here, outputs l1, l2, etc
console.log($(this).index()); //outputs 0, 1 .... 7 (0-based index)
}
});
});​

Resources