I'm building a Meteor app that let's the user organize lists of items in tags.
I use jQuery draggable and droppable to update a collection when a user drags an item from one tag to another.
I find it hard to understand how/where/when I should call the function. I've tried a few different options (including this "hacky way of doing it". The Blaze documentation mentions that functions can be called on DOM events, but lacks the drag and drop events that I'm looking for. I've currently settled on calling the function under Template.rendered, but that means the item can only be dropped once per render. I've tried to counter this with Tracker.autorun, but I don't think I understand how it works and the item can still only be dropped once per render.
How can I make the .item draggable several times per render?
Template.tag.rendered = function () {
//wait on subscriptions to load
if (Session.get('DATA_LOADED')) {
Tracker.autorun(function () {
$(".item").draggable({
revert: true,
start: function (event, ui) {
var movingItem = Blaze.getData(this)._id;
Session.set('movingItem', movingItem);
},
});
$(".tag").droppable({
hoverClass: 'droppable',
drop: function () {
var movingItem = Session.get('movingItem');
var acceptingTag = Blaze.getData(this)._id;
Items.update(movingItem, {
$set: {"parents": acceptingTag}
});
}
});
});
}
};
I found the solution.
By separating the .draggable and .droppable into two different Template.rendered the function is now correctly called each time an item is moved.
No need for the Tracker.autorun
Template.item.rendered = function () {
if (Session.get('DATA_LOADED')) {
$(".item").draggable({
revert: true,
start: function (event, ui) {
var movingItem = Blaze.getData(this)._id;
Session.set('movingItem', movingItem);
console.log('moving ' + movingItem);
},
});
}
};
Template.tag.rendered = function () {
if (Session.get('DATA_LOADED')) {
$(".tag").droppable({
hoverClass: 'droppable',
drop: function () {
var movingItem = Session.get('movingItem');
var acceptingTag = Blaze.getData(this)._id;
Items.update(movingItem, {
$set: {"parents": acceptingTag}
});
console.log('Dropped on ' + acceptingTag);
}
});
}
};
Related
I have a sortable list in React which is powered by jQuery UI. When I drag and drop an item in the list, I want to update the array so that the new order of the list is stored there. Then re-render the page with the updated array. i.e. this.setState({data: _todoList});
Currently, when you drag and drop an item, jQuery UI DnD works, but the position of the item in the UI does not change, even though the page re-renders with the updated array. i.e. in the UI, the item reverts to where it used to be in the list, even though the array that defines its placement has updated successfully.
If you drag and drop the item twice, then it moves to the correct position.
// Enable jQuery UI Sortable functionality
$(function() {
$('.bank-entries').sortable({
axis: "y",
containment: "parent",
tolerance: "pointer",
revert: 150,
start: function (event, ui) {
ui.item.indexAtStart = ui.item.index();
},
stop: function (event, ui) {
var data = {
indexStart: ui.item.indexAtStart,
indexStop: ui.item.index(),
accountType: "bank"
};
AppActions.sortIndexes(data);
},
});
});
// This is the array that holds the positions of the list items
var _todoItems = {bank: []};
var AppStore = assign({}, EventEmitter.prototype, {
getTodoItems: function() {
return _todoItems;
},
emitChange: function(change) {
this.emit(change);
},
addChangeListener: function(callback) {
this.on(AppConstants.CHANGE_EVENT, callback);
},
sortTodo: function(todo) {
// Dynamically choose which Account to target
targetClass = '.' + todo.accountType + '-entries';
// Define the account type
var accountType = todo.accountType;
// Loop through the list in the UI and update the arrayIndexes
// of items that have been dragged and dropped to a new location
// newIndex is 0-based, but arrayIndex isn't, hence the crazy math
$(targetClass).children('form').each(function(newIndex) {
var arrayIndex = Number($(this).attr('data-array-index'));
if (newIndex + 1 !== arrayIndex) {
// Update the arrayIndex of the element
_todoItems[accountType][arrayIndex-1].accountData.arrayIndex = newIndex + 1;
}
});
// Sort the array so that updated array items move to their correct positions
_todoItems[accountType].sort(function(a, b){
if (a.accountData.arrayIndex > b.accountData.arrayIndex) {
return 1;
}
if (a.accountData.arrayIndex < b.accountData.arrayIndex) {
return -1;
}
// a must be equal to b
return 0;
});
// Fire an event that re-renders the UI with the new array
AppStore.emitChange(AppConstants.CHANGE_EVENT);
},
}
function getAccounts() {
return { data: AppStore.getTodoItems() }
}
var Account = React.createClass({
getInitialState: function(){
return getAccounts();
},
componentWillMount: function(){
AppStore.addChangeListener(this._onChange);
// Fires action that triggers the initial load
AppActions.loadComponentData();
},
_onChange: function() {
console.log('change event fired');
this.setState(getAccounts());
},
render: function(){
return (
<div className="component-wrapper">
<Bank data={this.state.data} />
</div>
)
}
});
The trick is to call sortable('cancel') in the stop event of the Sortable, then let React update the DOM.
componentDidMount() {
this.domItems = jQuery(React.findDOMNode(this.refs["items"]))
this.domItems.sortable({
stop: (event, ui) => {
// get the array of new index (http://api.jqueryui.com/sortable/#method-toArray)
const reorderedIndexes = this.domItems.sortable('toArray', {attribute: 'data-sortable'})
// cancel the sort so the DOM is untouched
this.domItems.sortable('cancel')
// Update the store and let React update (here, using Flux)
Actions.updateItems(Immutable.List(reorderedIndexes.map( idx => this.state.items.get(Number(idx)))))
}
})
}
The reason jQuery UI Sortable doesn't work with React is because it directly mutates the DOM, which is a big no no in React.
To make it work, you would have to modify jQuery UI Sortable so that you keep the DnD functionality, but when you drop the element, it does not modify the DOM. Instead, it could fire an event which triggers a React render with the new position of the elements.
Since React uses a Virtual DOM, you have to use the function React.findDOMNode() to access an actual DOM element.
I would call the jQuery UI function inside the componentDidMount method of your component because your element has to be already rendered to be accessible.
// You have to add a ref attribute to the element with the '.bank-entries' class
$( React.findDOMNode( this.refs.bank_entries_ref ) ).sortable( /.../ );
Documentation - Working with the browser (everything you need to know is here)
Hope that makes sense and resolves your issue
i have created a directive to handle selectable provided by Jquery
mydirectives.directive('uiSelectable', function ($parse) {
return {
link: function (scope, element, attrs, ctrl) {
element.selectable({
stop: function (evt, ui) {
var collection = scope.$eval(attrs.docArray)
var selected = element.find('div.parent.ui-selected').map(function () {
var idx = $(this).index();
return { document: collection[idx] }
}).get();
scope.selectedItems = selected;
scope.$apply()
}
});
}
}
});
to use in html
<div class="margin-top-20px" ui-selectable doc-array="documents">
where documents is an array that get returned by server in ajax response.
its working fine i can select multiple items or single item
Issue: i want to clear selection on close button
http://plnkr.co/edit/3cSef9h7MeYSM0cgYUIX?p=preview
i can write jquery in controller to remove .ui-selected class but its not recommended approach
can some one guide me whats the best practice to achieve these type of issue
Update:
i fixed the issue by broadcasting event on cancel and listening it on directive
$scope.clearSelection=function() {
$scope.selectedItems = [];
$timeout(function () {
$rootScope.$broadcast('clearselection', '');
}, 100);
}
and in directive
scope.$on('clearselection', function (event, document) {
element.find('.ui-selected').removeClass('ui-selected')
});
is this the right way of doing it or what is the best practice to solve the issue.
http://plnkr.co/edit/3cSef9h7MeYSM0cgYUIX?p=preview
When I drag an element to a droppable area, it sends a socket to the server. Whenever I append a new element to the page, the drop: function() {} does not trigger when the appended element is dropped, it only works once the page has been refreshed, although the drag function works.
question
Is there a way to bind the drop function to the appending elements?
code
var UI = {
droppedArea: $('.dropArea'),
initialArea: $('#init-area')
};
UI.droppedArea.droppable({
drop: function(event, ui) {
var dropped = ui.draggable;
var droppedOn = $(this),
/* Rest of the code */
socket.emit('move', data);
}
});
// Element appended like this
function newItem(data) {
var html = '<li>'+data.content+'</li>';
UI.initialArea.append(html);
}
// From the server
socket.on('new', function(data) {
newItem(data);
});
Figured out why.
Under the drop: function(), I had an accept: option, which value were DOM elements, with a type of class, stored in a variable.
Therefore, any appended elements were not referenced by the variable, so they were not being captured by the function
var UI = {
var item = $('.the_item')
}
drop: function() {
accept: UI.item,
}
I'm new to jQuery UI.
I'm trying to create a selectable jQuery UI tooltip. The tooltip is associated with the links on a page.
When the link is surrounded by just text, it works fine. But when there are few links next to each other, the functionality overlaps and tooltips don't show smoothly anymore.
you can find the code on http://jsfiddle.net/zumot/Hc3FK/2/
Below the JavaScript code
$("[title][data-scan]").bind("mouseleave", function (event) {
event.stopImmediatePropagation();
var fixed = setTimeout('$("[title][data-scan]").tooltip("close")', 100);
$(".ui-tooltip").click(function () {
alert("I am clickable");
return false;
});
$(".ui-tooltip").hover(
function () {
clearTimeout(fixed);
},
function () {
$("[title][data-scan]").tooltip("close");
});}).tooltip({
items: "img, [data-scan], [title]",
content: function () {
var element = $(this);
if (element.is("[data-scan]")) {
var text = element.attr("href");
return "<a href='http://www.google.com'>You are trying to open a tooltip <span>" + text + "</span></a>";
}
if (element.is("[title]")) {
return element.attr("title");
}
if (element.is("img")) {
return element.attr("alt");
}
},
position: {
my: "right center",
at: "left center",
delay: 200,
using: function (position, feedback) {
$(this).css(position);
$("<div>")
.addClass(feedback.vertical)
.addClass(feedback.horizontal)
.appendTo(this);
}
}});
My attempt to fix the issue was by making the variable fixed global (to make it accessible by other jQuery UI properties), and on Open event, hide any other previously opened tooltips and clear the timeout id saved in fixed variable.
You can find the solution here http://jsfiddle.net/zumot/dVGWB/
, though to see the code working properly, you'll have to run it directly on your browser.
Here's the snapshort of the fixed code.
// Make the timeout id variable global
var fixed = 0;
$("[title][data-scan]").tooltip({
items: "img, [data-scan], [title]",
content: function () {
var element = $(this);
if (element.is("[data-scan]")) {
var text = element.attr("href");
return "<a href='http://www.google.com'>You are trying to open a tooltip <span>" + text + "</span></a>";
}
if (element.is("[title]")) {
return element.attr("title");
}
if (element.is("img")) {
return element.attr("alt");
}
},
open: function (event, ui) {
// When opening a new div, hide any previously opened tooltips first.
$(".ui-tooltip:not([id=" + ui.tooltip[0].id + "])").hide();
// clear timeout as well if there's any.
if (tf > 0) {
clearTimeout(tf)
};
},
position: {
my: "right center",
at: "left center",
delay: 200,
using: function (position, feedback) {
$(this).css(position);
$("<div>")
.addClass(feedback.vertical)
.addClass(feedback.horizontal)
.appendTo(this);
}
}
}).bind("mouseleave", function (event) {
// stop defeulat behaviour
event.stopImmediatePropagation();
fixed = setTimeout('$("[title][data-scan]").tooltip("close")', 100);
$(".ui-tooltip").hover(
function () {
clearTimeout(tf);
}, function () {
$("[title][data-scan]").tooltip("close");
})
});
Are there any events fired by an element to check whether a css3 transition has started or end?
W3C CSS Transitions Draft
The completion of a CSS Transition generates a corresponding DOM Event. An event is fired for each property that undergoes a transition. This allows a content developer to perform actions that synchronize with the completion of a transition.
Webkit
To determine when a transition completes, set a JavaScript event listener function for the DOM event that is sent at the end of a transition. The event is an instance of WebKitTransitionEvent, and its type is webkitTransitionEnd.
box.addEventListener( 'webkitTransitionEnd',
function( event ) { alert( "Finished transition!" ); }, false );
Mozilla
There is a single event that is fired when transitions complete. In Firefox, the event is transitionend, in Opera, oTransitionEnd, and in WebKit it is webkitTransitionEnd.
Opera
There is one type of transition event
available. The oTransitionEnd event
occurs at the completion of the
transition.
Internet Explorer
The transitionend event occurs at the completion of the transition. If the transition is removed before completion, the event will not fire.
Stack Overflow: How do I normalize CSS3 Transition functions across browsers?
Update
All modern browsers now support the unprefixed event:
element.addEventListener('transitionend', callback, false);
https://caniuse.com/#feat=css-transitions
I was using the approach given by Pete, however I have now started using the following
$(".myClass").one('transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd',
function() {
//do something
});
Alternatively if you use bootstrap then you can simply do
$(".myClass").one($.support.transition.end,
function() {
//do something
});
This is becuase they include the following in bootstrap.js
+function ($) {
'use strict';
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
function transitionEnd() {
var el = document.createElement('bootstrap')
var transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd',
'MozTransition' : 'transitionend',
'OTransition' : 'oTransitionEnd otransitionend',
'transition' : 'transitionend'
}
for (var name in transEndEventNames) {
if (el.style[name] !== undefined) {
return { end: transEndEventNames[name] }
}
}
return false // explicit for ie8 ( ._.)
}
$(function () {
$.support.transition = transitionEnd()
})
}(jQuery);
Note they also include an emulateTransitionEnd function which may be needed to ensure a callback always occurs.
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
var called = false, $el = this
$(this).one($.support.transition.end, function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
return this
}
Be aware that sometimes this event doesn’t fire, usually in the case
when properties don’t change or a paint isn’t triggered. To ensure we
always get a callback, let’s set a timeout that’ll trigger the event
manually.
http://blog.alexmaccaw.com/css-transitions
All modern browsers now support the unprefixed event:
element.addEventListener('transitionend', callback, false);
Works in the latest versions of Chrome, Firefox and Safari. Even IE10+.
In Opera 12 when you bind using the plain JavaScript, 'oTransitionEnd' will work:
document.addEventListener("oTransitionEnd", function(){
alert("Transition Ended");
});
however if you bind through jQuery, you need to use 'otransitionend'
$(document).bind("otransitionend", function(){
alert("Transition Ended");
});
In case you are using Modernizr or bootstrap-transition.js you can simply do a change:
var transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd',
'MozTransition' : 'transitionend',
'OTransition' : 'oTransitionEnd otransitionend',
'msTransition' : 'MSTransitionEnd',
'transition' : 'transitionend'
},
transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];
You can find some info here as well http://www.ianlunn.co.uk/blog/articles/opera-12-otransitionend-bugs-and-workarounds/
Just for fun, don't do this!
$.fn.transitiondone = function () {
return this.each(function () {
var $this = $(this);
setTimeout(function () {
$this.trigger('transitiondone');
}, (parseFloat($this.css('transitionDelay')) + parseFloat($this.css('transitionDuration'))) * 1000);
});
};
$('div').on('mousedown', function (e) {
$(this).addClass('bounce').transitiondone();
});
$('div').on('transitiondone', function () {
$(this).removeClass('bounce');
});
If you simply want to detect only a single transition end, without using any JS framework here's a little convenient utility function:
function once = function(object,event,callback){
var handle={};
var eventNames=event.split(" ");
var cbWrapper=function(){
eventNames.forEach(function(e){
object.removeEventListener(e,cbWrapper, false );
});
callback.apply(this,arguments);
};
eventNames.forEach(function(e){
object.addEventListener(e,cbWrapper,false);
});
handle.cancel=function(){
eventNames.forEach(function(e){
object.removeEventListener(e,cbWrapper, false );
});
};
return handle;
};
Usage:
var handler = once(document.querySelector('#myElement'), 'transitionend', function(){
//do something
});
then if you wish to cancel at some point you can still do it with
handler.cancel();
It's good for other event usages as well :)