Scenario: user can drag an item on the web page, and drop on other item(s). When user drags item onto another object, live feedback is shown to the user about what will happen if he drops item X on item Y.
When user start to drag data must be fetched from backend for current item and this might take 1-2 seconds.
So basically while this happens, we must show to the user some kind of loader, and wait before giving feedback, even though he already may have dragged item X onto item Y.
I'm using dragula library which has the following signature:
dragula(dragulaConfig)
.on('drag', function(el, source ) { /* do on drag */ })
.on('cancel', function(el, container, source ) { /* do on cancel */ })
.on('drop', function(el, target, source ) { /* do on drop */ })
.on('over', function(el, container, source ) { /* do on over */ })
.on('out', function(el, container, source ) { /* do on out */ });
So basically it does not care if i have the data or not, it just fires the events.
What i'm thinking is to use Rx to limit events to over and out events until i have data for given draggable item.
My attempt at this:
var instance = dragula(dragulaConfig);
var pauser = new Rx.Subject();
var rxo = Rx.Observable;
var transform = function(el, target, source) {
return {
el: el,
target: target,
source: source
};
};
var pause = function() {
pauser.onNext(false);
};
//subscribe to drag event, and release pause once we have the data./
rxo
.fromEvent(instance, 'drag', transform)
.subscribe(function(event) {
thisReturnsAPromise(event).then(function() {
pauser.onNext(true);
});
});
// subscribe to 'over' event that will not be fired until we have our item data
rxo
.fromEvent(instance, 'over', transform)
.pausable(pauser)
.subscribe(function() { /* show feedback to the user */});
// subscribe to 'out' event that will not be fired until we have our item data
rxo
.fromEvent(instance, 'out', transform)
.pausable(pauser)
.subscribe(function() { /* remove feedback to the user */});
// subscribe to 'cancel' event - this will put pause to over/out events
var cancel = rxo
.fromEvent(instance, 'cancel', transform);
cancel.subscribe(pause);
cancel.subscribe(function() { /* show feedback to the user if he cancels */});
// subscribe to 'drop' event - this will put pause to over/out events
var drop = rxo
.fromEvent(instance, 'drop', transform);
drop.subscribe(pause);
drop.subscribe(function() { /* add dropped item */});
Which works, but it just does not feel right (and I'm newbie to RxJS) - too much boilerplate.
How could I improve this?
Related
I'm using fabric.js to create shapes on canvas . on right click on the shapes i want to show a context menu based on the shape selected. I'm able to capture the right click event and find which object the right click is done. but i don know how to show a context menu from a javascript (something like contextmenu.show). below is the code which im using to find the object. Any one please help.
$('.upper-canvas').bind('contextmenu', function (e) {
var objectFound = false;
var clickPoint = new fabric.Point(e.offsetX, e.offsetY);
e.preventDefault();
canvas.forEachObject(function (obj) {
if (!objectFound && obj.containsPoint(clickPoint)) {
objectFound = true;
// here need to set a customized context menu and show it
// but dont now how to do so.
}
});
});
Using jquery-ui-contextmenu you could instantiate a context menu on the canvas and modify the menu entries depending on the target.
(Note that the code is untested, but it should show the idea. Have a look at the API docs for details.)
$(document).contextmenu({
delegate: ".upper-canvas",
menu: [...], // default menu
beforeOpen: function (event, ui) {
var clickPoint = new fabric.Point(event.offsetX, event.offsetY);
// find the clicked object and re-define the menu or
// optionally return false, to prevent opening the menu:
// return false;
// En/disable single entries:
$(document).contextmenu("enableEntry", ...);
// Show/hide single entries:
$(document).contextmenu("showEntry", ...);
// Redefine the whole menu:
$(document).contextmenu("replaceMenu", ...);
},
select: function(event, ui) {
// evaluate selected entry...
}
});
Using PanZoom JS, I am trying to handle a simple click event for any SVG that was clicked. According to docs, it should be as simple as this:
$panzoom.on('panzoomend', function(e, panzoom, matrix, changed) {
if (changed) {
// deal with drags or touch moves
} else {
// deal with clicks or taps
}
});
This never fires and it never hits the if test.
First you have to create the $panzoom variable and call the panzoom.
You can enter something like this
var $panzoom = $('.panzoom').panzoom({
contain: 'invert',
minScale: 0.5,
increment: 0.5,
});
after you can enter your code
$panzoom.on('panzoomend', function(e, panzoom, matrix, changed) {
if (changed) {
// deal with drags or touch moves
alert("changed");
} else {
// deal with clicks or taps
alert("not changed");
}
});
$(document).on('click', '.entity', function (e) {
...
})
I'm using the above to handle clicking of any "entity" anywhere on my site. It works great everywhere except on maps. I'm creating custom overlays that are each an element with the entity class. This is confirmed in the DOM. All CSS I apply works, however the click event does not seem to propagate up to the document level. I put the alert in for testing, and it does fire. With the click function blank, nothing happens. How can I keep it from blocking?
mymap.gmap3({
map:{
options:{
...
}
},
overlay:{
values: [
...
],
events:{
click: function () {
alert('clicked map entity');
},
...
Checked the following plugin:
/*!
* GMAP3 Plugin for JQuery
* Version : 5.1.1
* Date : 2013-05-25
Line 343: there is function OverlayView(map, opts, latLng, $div) with the following code:
...
this.onAdd = function() {
var panes = this.getPanes();
if (opts.pane in panes) {
$(panes[opts.pane]).append($div);
}
$.each("dblclick click mouseover mousemove mouseout mouseup mousedown".split(" "), function(i, name){
listeners.push(
google.maps.event.addDomListener($div[0], name, function(e) {
$.Event(e).stopPropagation();
google.maps.event.trigger(that, name, [e]);
that.draw();
})
);
});
listeners.push(
google.maps.event.addDomListener($div[0], "contextmenu", function(e) {
$.Event(e).stopPropagation();
google.maps.event.trigger(that, "rightclick", [e]);
that.draw();
})
);
}; ...
which calls $.Event(e).stopPropagation(); for click and some other events. That obviously stops propagation of events.
Commenting those two lines could resolve this specific issue. But, question is what are the possible consequences.
When I'm trying to delete an external event from my calendar, if I add say 3 external events then drag one to the bin, rather than removing just the one event it deletes all events (even the ones from the separate feeds I am doing.
Any idea why this is and how to fix it? Here is the code:
$(document).ready(function () {
//listens for drop event
$("#calendarTrash").droppable({
tolerance: 'pointer',
drop: function (event, ui) {
var answer = confirm("Delete Event?")
if (answer) {
$('#calendar').fullCalendar('removeEvents', event.id);
}
}
});
/* initialize the external events ------------*/
$('#external-events div.external-event').each(function () {
// create an Event Object (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
// it doesn't need to have a start or end
var eventObject = {
title: $.trim($(this).text()) // use the element's text as the event title
};
// store the Event Object in the DOM element so we can get to it later
$(this).data('eventObject', eventObject);
// make the event draggable using jQuery UI
$(this).draggable({
zIndex: 999,
revert: true, // will cause the event to go back to its
revertDuration: 0 // original position after the drag
});
});
});
The event.id in your drop function does not refer to the FullCalendar event. It refers to the drop event that was just triggered.
You will need to use ui.draggable to access your draggable - in this case the FullCalendar event.
Hope this helps! Cool concept BTW!
Update: Check this fiddle for a proof-of-concept
For anyone in the same circumstances,..
eventDragStop: function(event, jsEvent, ui, view) {
if (x) {
$('#calendar').fullCalendar('removeEvents', event._id);
}
Please notice I am using event._id, x is the result of checking which div the item is dragged into returning a true or false. checks which div the event is being dropped into. I also had to change some code in fullcalendar.js
the function eachEventElement, was causing me an issue with the above code, so I changed it too.
function eachEventElement(event, exceptElement, funcName) {
try{
var elements = eventElementsByID[event._id],
i, len = elements.length;
for (i=0; i<len; i++) {
if (!exceptElement || elements[i][0] != exceptElement[0]) {
elements[i][funcName]();
}
}
}
catch(err)
{}
}
Problem (jsFiddle demo of the problem)
I'm having some trouble with the revert setting when used in conjunction with the cancel method in the jQuery sortable. The cancel method, as documented in the jQuery Sortable documentation states:
Cancels a change in the current sortable and reverts it back to how it
was before the current sort started. Useful in the stop and receive
callback functions.
This works fine in both the stop and receive callbacks, however if I add a revert duration to the sortable connected list, it starts to act funny (see jsFiddle here).
Ideally, upon cancelling, the revert could simply not happen, or alternatively in a more ideal world, it would gracefully revert to it's original location. Any ideas how I can get the revert and cancel to play nice?
Expected
Drag from left list to right list
Drop item
Item animates to original location - or - immediately shifts to original location
Actual
Drag from left list to right list
Drop item
Item animates to new location, assuming sortable is successful
Item immediately shifts to original location, as sortable was cancelled
Clarification
The revert property moves the item to the location where the item would drop if successful, and then immediately shifts back to the original location due to the revert occurring before the cancel method. Is there a way to alter the life-cycle so if the cancel method is executed, revert isn't, and instead the item is immediately return to it's original location?
i created a demo for you here:
the jsfiddle code
it seems to produce the output you expect.
i changed the receive callback method from this:
$(ui.sender).sortable('cancel');
to this:
$(ui.sender).sortable( "option", "revert", false );
hopefully, this is what you expected.
After many hours for searching for a solution I decided the only way to achieve what I was trying to do was to amend the way in which the jQuery sortable plugin registered the revert time. The aim was to allow for the revert property to not only accept a boolean or integer, but also accept a function. This was achieved by hooking into the prototype on the ui.sortable with quite a lot of ease, and looks something like this.
jQuery Sortable Hotfix
$.ui.sortable.prototype._mouseStop = function(event, noPropagation)
{
if (!event) return;
// if we are using droppables, inform the manager about the drop
if ($.ui.ddmanager && !this.options.dropBehaviour)
$.ui.ddmanager.drop(this, event);
if (this.options.revert)
{
var self = this;
var cur = self.placeholder.offset();
// the dur[ation] will not determine how long the revert animation is
var dur = $.isFunction(this.options.revert) ? this.options.revert.apply(this.element[0], [event, self._uiHash(this)]) : this.options.revert;
self.reverting = true;
$(this.helper).animate({
left: cur.left - this.offset.parent.left - self.margins.left + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollLeft),
top: cur.top - this.offset.parent.top - self.margins.top + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollTop)
}, !isNaN(dur) ? dur : 500, function ()
{
self._clear(event);
});
} else
{
this._clear(event, noPropagation);
}
return false;
}
Implementation
$('ul').sortable({
revert: function(ev, ui)
{
// do something here?
return 10;
}
});
I ended up creating a new event called beforeRevert which should return true or false. If false then the cancel function is called and the item is animated back to its original position. I didn't code this with the helper option in mind, so it would probably need some additional work to support that.
jQuery Sortable Hotfix with animation
var _mouseStop = $.ui.sortable.prototype._mouseStop;
$.ui.sortable.prototype._mouseStop = function(event, noPropagation) {
var options = this.options;
var $item = $(this.currentItem);
var el = this.element[0];
var ui = this._uiHash(this);
var current = $item.css(['top', 'left', 'position', 'width', 'height']);
var cancel = $.isFunction(options.beforeRevert) && !options.beforeRevert.call(el, event, ui);
if (cancel) {
this.cancel();
$item.css(current);
$item.animate(this.originalPosition, {
duration: isNaN(options.revert) ? 500 : options.revert,
always: function() {
$('body').css('cursor', '');
$item.css({position: '', top: '', left: '', width: '', height: '', 'z-index': ''});
if ($.isFunction(options.update)) {
options.update.call(el, event, ui);
}
}
});
}
return !cancel && _mouseStop.call(this, event, noPropagation);
};
Implementation
$('ul').sortable({
revert: true,
beforeRevert: function(e, ui) {
return $(ui.item).hasClass('someClass');
}
});