JQuery UI Drag/Droppable with multiple scopes? - javascript

I want to have several classes of draggables, each one corresponding to a class of droppables. But in addition, I want to have a separate "waste bin", where all the draggables can be dropped until a suitable droppable can be found for them.
Now, this can be easily achieved with an accept function. However, I may have up to 20 classes, each one with 30-40 draggables/droppables. So if I use an "accept" function for this, the moment I pick up a draggable, my chrome freezes as it runs tests for every droppable on the screen :(
This can be solved if I use the 'scope' property, since it seems to be using some different way. However, when I use a scope, I can't seem to implement the "waste bin" concept, since it can only have one scope!
Is there some way to by-pass this problem? Give the draggables more than one scope, or giving the waste bin many scopes? Or maybe some other solution I can't think of?

Internally jQuery UI will run the following code whenever you start dragging a draggable to determine which droppables are eligible to receive the draggable.
var m = $.ui.ddmanager.droppables[t.options.scope] || [];
var type = event ? event.type : null; // workaround for #2317
var list = (t.currentItem || t.element).find(":data(droppable)").andSelf();
droppablesLoop: for (var i = 0; i < m.length; i++) {
if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) continue; //No disabled and non-accepted
for (var j=0; j < list.length; j++) { if(list[j] == m[i].element[0]) { m[i].proportions.height = 0; continue droppablesLoop; } }; //Filter out elements in the current dragged item
m[i].visible = m[i].element.css("display") != "none"; if(!m[i].visible) continue; //If the element is not visible, continue
if(type == "mousedown") m[i]._activate.call(m[i], event); //Activate the droppable if used directly from draggables
m[i].offset = m[i].element.offset();
m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };
}
As you can see the code is non trivial and would explain why you're seeing slow performance every time you start dragging.
One thing to note is that the very first thing checked in the droppablesLoop is whether the droppable is disabled.
Therefore, to increase performance you could always manually disable the appropriate droppable widgets which will make you quickly jump out of the code block above. You can do this by using the start event on the draggable, which will fire first.
$('.draggable').draggable({
start: function() {
$('.invalid-droppable-elements').droppable('option', 'disabled', true);
},
stop: function() {
$('.invalid-droppable-elements').droppable('option', 'disabled', false);
}
});
This essentially makes you implement the accept / scope logic yourself and the performance impact is up to your algorithm. It shouldn't be that bad to implement though. The reason the plugins are as slow as they are is because they have to handle for a LOT of different situations.
jQuery UI does not support adding multiple scopes to individual draggable / droppable elements but you could roll that functionality on your own.
I put an example together to show this here - http://jsfiddle.net/tj_vantoll/TgQTP/1/.

Related

Throttle displaying of div and children, using recursive function Vanilla Javascript

Originally I had a huge div with many child elements that was display: none and then I would simply set it to display: '' and the entire div would be visible. This created some noticable lag. I want to throttle it by displaying the elements one by one with a timeout but the function I created causes strange behavior. It actually works fine if you remove the setTimeout but without setTimeout there is still the same lag.
function throttleDisplay(page) {
page.style.display = '';
var children = page.children;
if (!children.length) return;
for (var i = 0; i < children.length; i++) {
var child = children[i];
setTimeout(function() {
throttleDisplay(child);
}, 100);
}
}
Several problems.
Revealing the top-level <div> all-at-once by setting display:block (or display:'') will trigger just one page re-flow and re-paint, and will therefore create less "lag" than recursively revealing children, which will thrash your layout with exponential re-flows and re-paints.
setTimeout (and therefore its callback) is called for each child in the for loop (at one recursion tier) more or less simultaneously, so this throttles the reveal of descendant elements, not sibling elements.
Unless every element in the tree begins with display:none, setting the top-level element to display:'' will reveal the tree all-at-once, anyway.
Are you certain that revealing the top-level <div> is the cause of your lag? A code sample might help the community find the source of your problem. A first suggestion would be to wrap the code that changes display inside a requestAnimationFrame. (MDN on rAF)
Note 1A: I say "exponential" because you are revealing each child separately versus one container element, but of course the number of operations is linear with respect to the total number of descendants, ignoring their relative "container"/"contained" status.
Note 1B: It is not necessarily the case that this code will "thrash" your layout; you are performing a sequence of "writes" to the layout which will probably be automatically batched at the end of a frame by a modern browser, provided all the function calls can be processed within the space of a frame (~17ms), and we are speaking only about the non-throttled sibling reveals. The asynchronous throttling would allow "reads" from other parts of your code, forcing a re-flow, but since the delay is already the length of 5 frames, this is irrelevant. The point is that this code will not reduce "lag" of any kind.
Thanks to this-vidor for explaining some of the problems with the function I had. I don't know what exactly was causing the very strange behavior in my particular situation because I tried to reproduce it with fake data on jsbin and did not have the same problems. I deciding on building a custom function for my particular situation it looks like this
Messages: function (page) {
var messageCount = 0;
var curThrottle = 0;
page.style.display = '';
var children = page.children;
var lastChild = children[children.length - 1];
var lastChildsChildren = lastChild.children;
for (var i = 0; i < lastChildsChildren.length; i++) {
var child = lastChildsChildren[i];
child.style.display = '';
var messages = child.children[child.children.length - 1].children;
for (var j = 0; j < messages.length; j++) {
if (++messageCount%40 === 0) curThrottle += 30;
var message = messages[j];
(function (message) {
setTimeout(function() {
message.style.display = '';
}, curThrottle);
})(message);
}
}
}

stopping mousewheel event from happening twice in OSX

I noticed mousewheel event is happening multiple times in mac osx. Can be atributed to inertia feature.
Is there a way to fix this behaviour?
(self signed ssl no worries please!)
https://sandbox.idev.ge/roomshotel/html5_v2/
I'm using scrollSections.js https://github.com/guins/jQuery.scrollSections
And it uses mousewheel jquery plugin: https://github.com/brandonaaron/jquery-mousewheel
I'm seeing a lot of people having the same issue: https://github.com/brandonaaron/jquery-mousewheel/issues/36
There are some solutions but none works with scrollSections plugin.
Any ideas how to disable this inertia feature from JS?
My attempted fix:
// Fix for OSX inertia problem, jumping sections issue.
if (isMac) {
var fireEvent;
var newDelta = deltaY;
if (oldDelta != null) {
//check to see if they differ directions
if (oldDelta < 0 && newDelta > 0) {
fireEvent = true;
}
//check to see if they differ directions
if (oldDelta > 0 && newDelta < 0) {
fireEvent = true;
}
//check to see if they are the same direction
if (oldDelta > 0 && newDelta > 0) {
//check to see if the new is higher
if (oldDelta < newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
}
//check to see if they are the same direction
if (oldDelta < 0 && newDelta < 0) {
//check to see if the new is lower
if (oldDelta > newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
}
} else {
fireEvent = true;
}
oldDelta = newDelta;
} else {
fireEvent = true;
}
You can see fix implemented here: https://sandbox.idev.ge/roomshotel/html5_v2/ But it is a hit/miss.
The latest solution with timeouts had one major drawback: kinetic scrolling effect could last rather long (even 1s or so)... and disabling scrolling for 1-2 seconds wouldn't be the best decision.
Soooo, as promised, here's another approach.
Our goal is to provide one response for one user action, which in this case is scrolling.
What's 'one scrolling'? For the sake of solving this problem, let's say that 'one scrolling' is an event that lasts from the moment the page has started to move till the moment the movement has ended.
Kinetic scrolling effect is achieved by moving the page many times (say, every 20ms) for a small distance. It means that our kinetic scrolling consists of many-many little linear 'scrollings'.
Empirical testing has showed that this little 'scrollings' happen every 17-18ms in the middle of kinetic scroll, and about 80-90ms at the beginning and the end. Here's a simple test we can set up to see that:
var oldD;
var f = function(){
var d = new Date().getTime();
if(typeof oldD !== 'undefined')
console.log(d-oldD);
oldD = d;
}
window.onscroll=f;
Important! Every time this mini-scroll happens, scroll event is triggered. So:
window.onscroll = function(){console.log("i'm scrolling!")};
will be fired 15 to 20+ times during one kinetic scroll. BTW, onscroll has really good browser support (see compatibility table), so we can rely on it (except for touch devices, I'll cover this issue a bit later);
Some may say that redefining window.onscroll is not the best way to set event listeners. Yes, you're encouraged to use
$(window).on('scroll',function(){...});
or whatever you like, it's not the point of the problem (I personally use my self-written library).
So, with the help of onscroll event we can reliably say whether this particular mini-movement of the page belongs to one long-lasting kinetic scroll, or is it a new one:
var prevTime = new Date().getTime();
var f = function(){
var curTime = new Date().getTime();
if(typeof prevTime !== 'undefined'){
var timeDiff = curTime-prevTime;
if(timeDiff>200)
console.log('New kinetic scroll has started!');
}
prevTime = curTime;
}
window.onscroll=f;
Instead of "console.log" you can call your desired callback function (or event handler) and you're done!
The function will be fired only once on every kinetic or simple scroll, which was our goal.
You may have noticed that I've used 200ms as a criteria of whether it's a new scroll or a part of the previous scroll. It's up to you to set it to greater values to be 999% sure you prevent any extra calls. However, please keep in mind that it's NOT what we have used in my previous answer. It's just a period of time between any two page movements (whether it's a new scroll or a little part of a kinetic scroll). To my mind, there's a very little chance that there will be a lag more than 200ms between steps in kinetic scroll (otherwise it will be not smooth at all).
As I've mentioned above, the onscroll event works differently on touch devices. It won't fire during every little step of kinetic scroll. But it will fire when the movement of the page has finally ended. Moreover, there's ontouchmove event... So, it's not a big deal. If necessary, I can provide solution for touch devices too.
P.S. I understand that I've written a bit too much, so I'd be happy to answer all your questions and provide further code if you need one.
Provided solution is supported in all browsers, very lightweight and, what's more important, is suitable not only for macs, but for every device that might implement kinetic scrolling, so I think it's really a way to go.
You know, I think it's a better idea to use timeouts in this case. Why not write something like this:
// Let's say it's a global context or whatever...:
var fireEvent = true;
var newDelta, oldDelta, eventTimeout;
newDelta = oldDelta = eventTimeout = null;
// ... and the function below fires onmousewheel or anything similar:
function someFunc(){
if(!fireEvent) return; // if fireEvent is not allowed => stop execution here ('return' keyword stops execution of the function), else, execute code below:
newDelta = deltaY;
if(oldDelta!=null&&oldDelta*newDelta>0){ // (1.1) if it's not the first event and directions are the same => prevent possible dublicates for further 50ms:
fireEvent = false;
clearTimeout(eventTimeout); // clear previous timeouts. Important!
eventTimeout = setTimeout(function(){fireEvent = true},500);
}
oldDelta = newDelta;
someEventCallback(); // (1.2) fire further functions...
}
So, any mousewheel event fired within half a second after any previous mousewheel event call will be ignored, if it is made in the same direction as previous (see condition at 1.1). It will solve the problem and there's no way user would spot this. Delay amount may be changed to better meet your needs.
The solution is made on pure JS. You're welcome to ask any questions about integrating it in your environment, but then I'll need you to provide further code of your page.
P.S. I have not seen anything similar to eventCallback() call in your code (see 1.2 of my solution). there was only fireEvent flag. Were you doing something like:
if(fireEvent)
someEventCallback();
later on or something?
P.P.S.note that fireEvent should be in global scope in order to work here with setTimeout. If it's not, it's also quite easy to make it work fine, but the code needs to be altered a bit. If it's your case, tell me and I'll fix it for you.
UPDATE
After a brief search I found out, that similar mechanism is used in Underscore's _debounce() function. See Underscore documentation here
Have you though about using fullpage.js instead?
It has a delay between arriving to a section and the moment you are able to scroll to the next section which solves part of the problem Mac users experience with track-pads or Apple magic mouses.
It would also provide you some other benefits, such as much more options, methods and compatibility with touch devices and old browsers with no CSS3 support.
To have something to start with, let's make your solution shorter (therefore easier to understand & debug):
var fireEvent;
var newDelta = deltaY;
var oldDelta = null;
fireEvent = EventCheck();
oldDelta = newDelta;
function EventCheck(){
if(oldDelta==null) return true; //(1.1)
if(oldDelta*newDelta < 0) return true; // (1.2) if directions differ => fire event
if(Math.abs(newDelta)<Math.abs(oldDelta)) return true; // (1.3) if oldDelta exceeds newDelta in absolute values => fire event
return false; // (1.4) else => don't fire;
}
As you see, it does absolutely what your code does.
However, I can't understand this part of your code (which corresponds to (1.3) in my snippet):
//check to see if the new is lower
if (oldDelta > newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
from code provided it's unclear how deltaY is calculated. As one could assume, delta equals to endPosition - initialPosition. So, oldDelta>newDelta does not mean that the new position is lower, but that the new gap between these two values is bigger. If it's what it mean and you still use it, I suppose you try to track inertia with that. Then you should alter comparative operator (use less than, instead of greater then and vice-versa). In other words, I'd write:
if(Math.abs(newDelta)>Math.abs(oldDelta)) return true; // (1.3)
you see, now I've used 'greater than' operator, which means: newDelta exceeds oldDelta in absolute values => it's not inertia and you can still fire the event.
Is it what you're trying to achieve or have I misinterpreted your code? If so, please explain how deltaY is calculated and what was your goal by comparing old&new Deltas.
P.S. I'd suggest not to use if(isMac) in this step, while a problem can also potentially hide there.

How to get all the HTML elements affected by an event

I have assigned a "mousemove" event on some div elements. Those elements might overlap each other due to an animation process, so several "mousemove" events could be called at once by moving the mouse over the overlapping parts.
The problem is that two triggered "mousemove" events can lead to conflicting decisions. Hence, I would like to make a decision based on ALL elements that are concerned by the "mousemove" event, when such an event occur for at least one of them.
My question is : do you know an efficient way to do it ?
Thanks !
If I understand the q correctly, you want to synchronize the execution of mousemove events for each div. There are hacky ways to do it, though the best would be to change your HTML markup, so you don't have overlaps.
Anyway, for your case, you could do the following:
var g_focusDivId = "";
function onMouseMove(e)
{
if (g_focusDivId != "" && g_focusDivId != e.target.id)
return; // Deciding to not exec any other mouse moves
g_focusDivId = e.target.id;
// Do your stuff
g_focusDivId = "";
}
This, of course, assumes that JS event handling is single-threaded, which is not always true: Is JavaScript guaranteed to be single-threaded?
The alternative is to do this (I have not tried this). I am using a queue to run the events in sequence on a single method. Much more controlled, but it may lead to some events getting processed late.
var g_syncEventQueue = new Array();
function onMouseEvent(e)
{
g_syncEventQueue.push(e);
}
function queueListenerProc()
{
if (g_syncEventQueue.size() > 0)
{
var evt = g_syncEventQueue[0];
g_syncEventQueue = g_syncEventQueue.splice(0, 1);
return queueListenerProc(); // Immediately process the next event
}
setTimeout("queueListenerProc()", 1000);
}
queueListenerProc(); // Not ideal because it keeps running without an exit condition.

jquery/js too slow while operating over 2000 rows

I have 2000 rows of data as follows
<div class="rr cf">
<span>VLKN DR EXP</span>
<span>01046</span>
<span>VELANKANNI</span>
<span>20:30</span>
<span>DADAR</span>
<span>10:00</span>
</div>
On a button click I am checking for text within them and updating the display of each row to block or none. The code that does this is
$('.rr').each(function(){
this.style.display="block";
});
var nodes = $(".rr");
for(var i=0;i < nodes.length; i++) {
// if data found
nodes.get(i).style.display="block";
// else
nodes.get(i).style.display="none";
}
This seems to be possibly very slow. I get chrome alert box as to kill the page.
Any ideas? what optimization can I do here?
Local Variables and Loops
Another simple way to improve the performance of a loop is to
decrement the iterator toward 0 rather than incrementing toward the
total length. Making this simple change can result in savings of up to
50% off the original execution time, depending on the complexity of
each iteration.
Taken from: http://oreilly.com/server-administration/excerpts/even-faster-websites/writing-efficient-javascript.html
Try saving the nodes.length as a local variable so that the loop doesn't have to compute it each time.
Also, you can store nodes.get(i) into a local variable to save some time if you are accessing that data a lot.
If the order isn't important, consider decrementing your for loop towards 0.
jQuery's each() loop is a bit slower than looping through the set yourself as well. You can see here that there is a clear difference.
Very simple example
You'll see that in my example, I've condensed the loop into a while loop:
var nodes = $(".rr span");
var i = nodes.length;
while(i--){
if(i%2 === 0){
nodes.get(i).style.color = "blue";}
}​
Notice that the while loop decrements i through each iteration. This way when i = 0, the loop will exit, because while(0) evaluates to false.
"Chunking" the Array
The chunk() function is designed to process an array in small chunks
(hence the name), and accepts three arguments: a “to do” list of
items, the function to process each item, and an optional context
variable for setting the value of this within the process() function.
A timer is used to delay the processing of each item (100ms in this
case, but feel free to alter for your specific use). Each time
through, the first item in the array is removed and passed to the
process() function. If there’s still items left to process, another
timer is used to repeat the process.
Have a look at Nick Zakas's chunk method defined here, if you need to run the loop in sections to reduce the chance of crashing the browser:
function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
Using createDocumentFragment()
Since the document fragment is in memory and not part of the main DOM
tree, appending children to it does not cause page reflow (computation
of element's position and geometry). Consequently, using document
fragments often results in better performance.
DocumentFragment are supported in all browsers, even Internet Explorer
6, so there is no reason to not use them.
Reflow is the process by which the geometry of the layout engine's
formatting objects are computed.
Since you are changing the display property of these elements iteratively, the page mush 'repaint' the window for each change. If you use createDocumentFragment and make all the changes there, then push those to the DOM, you drastically reduce the amount of repainting necessary.
Firstly, where are the delays occurring - in the jquery code, or the data check? If it is the jquery, you could try detaching the data container element (ie the html element that contains all the .rr divs) from the DOM, make your changes, and then re-attach it. This will stop the browser re-processing the DOM after each change.
I would try
1) set display of the common parent element of all those divs, to "none"
2) loop through divs, setting display as appropriate
3) set parent element display back to block
I believe this will help because it gives the browser the opportunity to aggregate the rendering updates, instead of forcing it to fully complete each single time you change the display property. The visibility of all the child node are irrelevant if the parent isnt displayed, so the browser no longer has a need to render a change in each child until the parent becomes visible again.
ALso, I fail to see the purpose of you first looping through them all and setting them all to block before you loop again and set them to their intended value.
Don't use jQuery here, jQuery will just slow things down here.
var elements = document.getElementsByClassName('rr'),
len = elements.length;
for(var i = 0; i < len; i++)
{
var ele = elements[i];
if(ele.innerHTML.search(/01046/) != -1)
ele.style.display = "none";
}
This should be much faster.
I'm also having performance problems while looping through roughly 1500 items.
As you might have guessed, the loop itself isn't the bottleneck. It's the operation you do within it that's the problem.
So, what I migrated the load using setTimeout. Not the prettiest of solutions but it makes the browser responsive between the updates.
var _timeout_ = 0;
for(var i=0;i < nodes.length; i++)
{
setTimeout(
(function(i)
{
return function()
{
if(stuff)
{
nodes.get(i).style.display="block";
}
else
{
nodes.get(i).style.display="none";
}
}
})(i),
_timeout_
);
_timeout_ += 4;
}
This will delay every update with 4 milliseconds, if the operation takes longer, the browser will become unresponsive. If the operation takes only 2 milisecond on your slowest browser, you can set it to 3, etc. just play around with it.

Can I query/detect the double click speed for a webpage user?

It's OS/user dependant. Not the browser, not the website, but the OS decides how fast and slow a double click must be.
I'd like to use that number in my app. Is there a way to get that number with JS?
Simple question. Might not be possible.
Thanks
Simple answer: no, sorry.
The best you could do would be something like this (example uses jQuery simply because it was quicker to write, the principle holds if jQuery is unavailable. Also note that this could well be simplified, this is just what came to mind first):
var timer,
num = 0;
$("#example").click(function() {
/*This condition is required because 2 click events are fired for each
dblclick but we only want to record the time of the first click*/
if(num % 2 === 0) {
timer = (new Date()).getTime();
}
num++;
}).dblclick(function() {
var time2 = (new Date()).getTime(),
dblClickTime = time2 - timer;
});
Unfortunately, that's probably not very helpful. You may be able to record the dblClickTime values and check for the longest, but that still is very unlikely to be the actual value you're after. That sort of thing is just not available through JavaScript.
Answer 2021 - as far as I know - still not. There is a reason: we should not care.
In principle dblclick is somehow obsolete …
We have the not well known detail property. Maybe because of the name.
From MDN:
The MouseEvent object passed into the event handler for click has its detail property set to the number of times the target was clicked. In other words, detail will be 2 for a double-click, 3 for triple-click, and so forth. This counter resets after a short interval without any clicks occurring; the specifics of how long that interval is may vary from browser to browser and across platforms. The interval is also likely to be affected by user preferences; for example, accessibility options may extend this interval to make it easier to perform multiple clicks with adaptive interfaces.
With detail ie. click_count it is possible to stop propagation of CLICK when detail != 1
So pseudcode:
if evt.detail==1
do_click()
if evt.detail==2
do_dblclick()
...
if evt.detail!=1
evt.stopPropagation()
If someone really needs to distinguish between click, double-click, triple-click, … like an 'XOR', they should really rethink the design.
The DblClickTime can be very long, that means the app feels like not responding, if the user just wants the click-action.
The other problem is, that it is possible, that users intention is a double-click, but is to slow - then there are two click-actions, they should not be to different to dblclick.
I'd like to use that number in my app. Is there a way to get that number with JS?
Definitely not - stuff like this is outside JavaScript's scope.
You may be able to find out values that work for a double click by asking the user to double-click, listen to the click events and see whether the dblclick event is fired - I'm nnot sure whether event handling works that way, though. But even if that works, it is still a long way from actually finding out the actual value.
This is my 2015 solution, would like to see a pure js version tho.
var start;
var click = null;
$(document).click(function() {
var now = performance.now();
start = click ? click : now;
click = now;
}).dblclick(function() {
alert(performance.now()-start)
});
EDIT
Pure JS
var start;
var click = null;
var getStart = function() {
var now = performance.now();
start = click ? click : now;
click = now;
}
var getStop = function() {
alert(performance.now()-start)
}
if (window.addEventListener) {
window.addEventListener('click', getStart , false);
} else {
window.attachEvent('onclick', function() {
return(getStart.call(window, window.event));
});
}
if (window.addEventListener) {
window.addEventListener('dblclick', getStop , false);
} else {
window.attachEvent('ondblclick', function() {
return(getStop.call(window, window.event));
});
}
Adding on to James Allardice's answer:
Depending on your implementation and where you are looking for double clicks you may want to also check the users mouse location (or I guess tap location). This is to avoid a double click firing when the user is clicking things on different parts of your page (again depends on your event listener implementation -- if it is just on one button for example this probably isn't an issue).
When a click event fires the event listener in my example below has two variables e.clientX and e.clientY. This will give you the location of the mouse. You might want to check to see if the user has moved their mouse significantly since the first click (adapt accordingly to your code).
document.addEventListener("click", function(e){ console.log("Mouse X: " + e.clientX + ": Mouse Y: " + e.clientY); });
You don't want to have it be too tight or else a user may never be able to fire a double click, and you don't want it to be too loose so that double clicks fire seemingly randomly for the user. Maybe start with a 25px or so box around the first click (again this depends on your application). This is something you can test and adjust based on your user interface.
I am assuming you don't have jQuery or aren't using it, because I believe jQuery might already do this calculation to fire dblclick

Categories