My fullcalendar duplicates events visually when i drag them to another timeslot. I have simplified my code down to the eventDrop to isolate the issue and yet I'm unable to understand the issue.
If I store the events to my localStorage I don't get a duplicate in the storage and the duplicate disappears when I reload the page. This means the problem is only visual and with Full Calendar itself.
However, this is obviously a huge issue as I don't want to reload the page: I want to stay in the current view changing what I need.
Here's my code for the eventDrop:
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
if (!confirm("Are you sure you want to change " + event.title + " ?")) {
/*If the user cancels the change, the event returns to its original position. Otherwise it saves the event.*/
revertFunc(); /*Revert changes using the predefined function revertFunc of fullCalendar.*/
$("#calendar").fullCalendar("refetchEvents");
/*FullCalendar Method to refetch events from all sources and rerender them on the screen.*/
} else {
updateConfirm(event); /*Use the logic to confirm the event update properties*/
Evento.prototype.Update(event); /*Modify the targeted event on the localStorage using the Update method of the Evento Class*/
$("#calendar").fullCalendar("updateEvent", event);
/*FullCalendar Method to report changes to an event and render them on the calendar.*/
$("#calendar").fullCalendar("refetchEvents");
/*FullCalendar Method to refetch events from all sources and rerender them on the screen.*/
}
}
And here's a gif of the issue:
https://i.imgur.com/rFPvvjE.gif
UPDATE: With slicedtoad's help I isolated the issue to my updateConfirm logic:
var updateConfirm = function(event) {
if (confirm("New title?")) { /*Check if the user wants to change the event title*/
event.title = prompt("Enter the new title: "); /*Set new title for the event*/
} else {
event.title = event.title;
}
if (confirm("New description?")) { /*Check if the user wants to change the event description*/
event.description = prompt("Enter the new description: "); /*Set new description for the event*/
} else {
event.description = event.description;
}
if (confirm("Is the event important?")) { /*Check if the user wants to change the event priority*/
event.overlap = false;
event.backgroundColor = "red"; /*Set new priority for the event*/
} else {
event.overlap = true;
event.backgroundColor = "blue"; /*Set new priority for the event*/
}
};
UPDATE 2:
console.log(event) before updateConfirm(event):
Object {id: "2015-01-27T15:29:11+00:00", title: "título", start: m, end: m, allDay: false…}_allDay: false_end: m_id: "2015-01-27T15:29:11+00:00"_start: mallDay: falsebackgroundColor: "blue"className: Array[0]description: "gt4"end: mid: "2015-01-27T15:29:11+00:00"overlap: truesource: Objectstart: mstoringId: "2015-01-27T15:29:11+00:00"title: "título"__proto__: Object
console.log(event) after updateConfirm(event):
Object {id: "2015-01-27T15:29:11+00:00", title: "título", start: m, end: m, allDay: false…}_allDay: false_end: m_id: "2015-01-27T15:29:11+00:00"_start: mallDay: falsebackgroundColor: "blue"className: Array[0]description: "gt4"end: mid: "2015-01-27T15:29:11+00:00"overlap: truesource: Objectstart: mstoringId: "2015-01-27T15:29:11+00:00"title: "título"__proto__: Object
Since the event is not locally sourced, calling updateEvent isn't necessary since the event will be refetched from the database when you call $("#calendar").fullCalendar("refetchEvents");
I'm not entirely sure why it would duplicate but the event modified by updateEvent seems to persist past the refetch. You must be changing it's ID or replacing it with another event object, but I wasn't able to reproduce it.
So try removing the update line
} else {
updateConfirm(event);
Evento.prototype.Update(event);
$("#calendar").fullCalendar("refetchEvents");
}
If that doesn't work, try deleting the event manually:
$("#calendar").fullCalendar( 'removeEvents', event._id ) //replace the updateEvent call with this
//or event.id if your events have an explicit id
Addendum
You likely want to actually figure out the cause of the problem since the above just patches it. Something in Evento.prototype.Update updateConfirm is modifying the event to the point that FC thinks it is a different event. Is it being copied and replacing itself? Are you playing with it's id?
Do singleton style solution:
This worked for me to stop the duplication which was caused by "new Draggable(container)"
every time reloaded the view
$scope.Draggable = null if ($scope.Draggable == null) {
$scope.Draggable = new Draggable(containerEl, {
itemSelector: '.item',
eventData: function (eventEl) {
return {
title: eventEl.innerText
};
}
});
}
Related
I'm using ag-grid (javascript) to display a large amount of rows (about 3,000 or more) and allow the user to enter values and it should auto-save them as the user goes along. My current strategy is after detecting that a user makes a change to save the data for that row.
The problem I'm running into is detecting and getting the correct values after the user enters a value. The onCellKeyPress event doesn't get fired for Backaspace or Paste. However if I attach events directly to DOM fields to catch key presses, I don't know how to know what data the value is associated with. Can I use getDisplayedRowAtIndex or such to be able to reliably do this reliably? What is a good way to implement this?
EDIT: Additional detail
My current approach is to capture onCellEditingStopped and then getting the data from the event using event.data[event.column.colId]. Since I only get this event when the user moves to a different cell and not just if they finish typing I also handle the onCellKeyPress and get the data from event.event.target (since there is no event.data when handling this event). Here is where I run into a hard-to-reproduce problem that event.event.target is sometimes undefined.
I also looked at using forEachLeafNode method but it returns an error saying it isn't supported when using infinite row model. If I don't use infinite mode the load time is slow.
It looks like you can bind to the onCellKeyDown event. This is sometimes undefined because on first keydown the edit of agGrid will switch from the cell content to the cell editor. You can wrap this around to check if there is a cell value or cell textContent.
function onCellKeyDown(e) {
console.log('onCellKeyDown', e);
if(e.event.target.value) console.log(e.event.target.value)
else console.log(e.event.target.textContent)
}
See https://plnkr.co/edit/XhpVlMl7Jrr7QT4ftTAi?p=preview
As been pointed out in comments, onCellValueChanged might work, however
After a cell has been changed with default editing (i.e. not your own custom cell renderer), the cellValueChanged event is fired.
var gridOptions = {
rowData: null,
columnDefs: columnDefs,
defaultColDef: {
editable: true, // using default editor
width: 100
},
onCellEditingStarted: function(event) {
console.log('cellEditingStarted', event);
},
onCellEditingStopped: function(event) {
console.log('cellEditingStopped', event);
},
onCellValueChanged: function(event) {
console.log('cellValueChanged', event);
}
};
another option could be to craft your own editor and inject it into cells:
function MyCellEditor () {}
// gets called once before the renderer is used
MyCellEditor.prototype.init = function(params) {
this.eInput = document.createElement('input');
this.eInput.value = params.value;
console.log(params.charPress); // the string that started the edit, eg 'a' if letter a was pressed, or 'A' if shift + letter a
this.eInput.onkeypress = (e) => {console.log(e);} // check your keypress here
};
// gets called once when grid ready to insert the element
MyCellEditor.prototype.getGui = function() {
return this.eInput;
};
// focus and select can be done after the gui is attached
MyCellEditor.prototype.afterGuiAttached = function() {
this.eInput.focus();
this.eInput.select();
};
MyCellEditor.prototype.onKeyDown = (e) => console.log(e);
// returns the new value after editing
MyCellEditor.prototype.getValue = function() {
return this.eInput.value;
};
//// then, register it with your grid:
var gridOptions = {
rowData: null,
columnDefs: columnDefs,
components: {
myEditor: MyCellEditor,
},
defaultColDef: {
editable: true,
cellEditor: 'myEditor',
width: 100
},
onCellEditingStarted: function(event) {
console.log('cellEditingStarted', event);
},
onCellEditingStopped: function(event) {
console.log('cellEditingStopped', event);
}
};
I have a client method that grays out a rendered document in the DOM by adding a class whose opacity is set to 0.4:
'click .detailCheckbox': function(ev){
var detail = $(ev.target).parent();
if(!detail.hasClass('toggle')){
detail.addClass('toggle');
} else {
detail.removeClass('toggle');
}
}
When I reload the page, though, the DOM element is no longer grayed out, because I never updated the document on the server.
Am I going to have to get super creative here, or am I missing a simple way to solve this?
You can use the meteor-persistent-session package.
Session.setPersistent(key, value) //store a persistent session variable (persistent)
For example.
if(!detail.hasClass('toggle')){
detail.addClass('toggle');
Session.setPersistent('opacity',0.4)
} else {
detail.removeClass('toggle');
Session.clear('opacity')
}
I solved it by creating a new field in the model, which is set (or unset) to the class name that has opacity 0.4
Here is the Meteor method insert method that contains the field checkboxStatus
addDetail: function(detailFormData){
if(! Meteor.userId()){
throw new Meteor.Error('not-authorized');
}
detailsCollection.insert({
detail: detailFormData.detail,
parentId: detailFormData.parentId,
checkboxStatus: detailFormData.checkboxStatus
});
}
I call it on the clicking of the checkbox:
'click .detailCheckbox': function(ev){
ev.preventDefault();
Meteor.call('setToggle', this._id);
}
And the setToggle Meteor method checks the toggle status, and then updates the document accordingly
setToggle: function(detailId){
var checked_detail = detailsCollection.findOne({_id: detailId});
if(checked_detail.checkboxStatus != 'toggle'){
detailsCollection.update(detailId, {
$set: {checkboxStatus: 'toggle'}
});
} else {
detailsCollection.update(detailId, {
$set: {checkboxStatus: 'untoggle'}
});
}
}
Which is then called by a template helper method
checkboxStatus: function(){
var checked_detail = detailsCollection.findOne({_id: this._id});
return checked_detail.checkboxStatus;
}
whose value is returned in the class tag of the template item itself
<li id={{_id}} class="detailViewEntry {{checkboxStatus}}">
I'm a bit lost where is the proper place to make this with the Full Calendar documentation and need some fiddle to point me into the right way. When my calendar loads the events, before them show up on the calendar, I need to make a ajax call into a wordpress db which should return a date of a post. Each post is an event on the calendar. Depending on the response of the database, if the post date is in future time than the current time, then the calendar should show this event in an specific color, if it is past or current time it should be another different specific color.
So basically, before each event renders on the calendar I need to trigger an ajax call for each of them and evaluate the data returned to apply them the right color for past/current events and future events.
Someone experienced with Full Calendar could point me a fiddle with an example how is this done within the Full Calendar documentation?
This is the code I went so far. I am looking to stay in the loop with the calendar refetchEvents and be able to fetch with ajax in the background data from the posts of a WordPress website to use it on the next refetchEvents trigger and so on.
$(function () {
var date = new Date();
var d = date.getDate();
var m = date.getMonth();
var y = date.getFullYear();
var webData = null; //array() data
$('#calendar-holder').fullCalendar({
eventRender: function(event, element, webData) {
var dataHoje = new Date();
/*
Use webData data taken with ajax on eventAfterAllRender callback option
inside this conditional statements to draw on the event box
colors and text values depending on the status and date of the post returned.
*/
if (event.start < dataHoje && event.end > dataHoje) {
element.css('background-color', '#FFB347');
element.find('.fc-event-inner').append('<span class="fc-event-status">ON AIR</span>');
} else if (event.start < dataHoje && event.end < dataHoje) {
element.css('background-color', '#77DD77');
element.find('.fc-event-inner').append('<span class="fc-event-status">Published</span>');
} else if (event.start > dataHoje && event.end > dataHoje) {
element.css('background-color', '#AEC6CF');
element.find('.fc-event-inner').append('<span class="fc-event-status">Schedued</span>');
}
},
eventAfterAllRender: function () {
webData = '(AJAX CALL TO WEBSITE POSTS I THINK SHOULD GO HERE)';
console.log(webData);
},
eventColor: '#378006',
complete: function() {
},
defaultView: 'basicDay',
googleCalendarApiKey: 'AIzaSyCtEQZsFtsY41kJ1Av5FftgX9kdfkHKH',
events: {
googleCalendarId: 'mywebsite.com_l84tadr5fulc7j0628g3g6oj3k#group.calendar.google.com'
},
header: {
left: 'prev, next',
center: 'title',
right: 'basicDay, basicWeek, month, '
},
lazyFetching: true,
timeFormat: {
agenda: 'h:mmt', // 5:00 - 6:30
'': 'h:mmt' // 7p
},
weekNumbers: false,
lang: 'en',
eventSources: [
{
url: Routing.generate('fullcalendar_loader'),
type: 'POST',
data: {
},
error: function() {
}
}
]
});
});
var refreshRate;
function reloadTime() {
refreshRate = setTimeout(reloadPage, 5000);
}
function reloadPage() {
$("#calendar-holder").fullCalendar("refetchEvents");
reloadTime();
}
$( document ).ready(function() {
reloadTime();
});
Changing the color:
The way you did it works, but the easiest way is to do it in eventDataTransform. Like so:
eventDataTransform: function(eventData){
if(eventData.end.isBefore(moment())){
eventData.color = "black";
}else{
eventData.color = "green";
}
return eventData;
},
Color Demo
Check if event exists
You didn't mention exactly what to do if the database returns false, but I'll assume you don't want nonexistent events rendered.
Since the source for your events is google calendar, this is actually kind of tricky. Normally, you would use the custom events function and do two ajax calls in it (one for the events and one for checking if they are valid). But you can't do this with google cal events.
So instead we will use eventDataTransform and only display the events after we know they exist.
eventDataTransform: function(eventData){
eventData.display = false; //Don't display until we check the server
eventData._uid = idCounter++; //unique ID. Don't need this if they already have unique IDs
ajaxCall(eventData); //check the server (will retroactively update the event to be displayed)
if(eventData.start.isBefore(moment())){ /*...*/ } //colors
return eventData;
},
The top of your eventRender callback should look like:
eventRender: function(event,element){
if(!event.display){ //Render only if the event exists
return false; //return false to stop the event from rendering.
}
/*...your other render code if you have any*/
}
Define your ajaxCall function outside of fullcalendar:
var ajaxCall = function(eventData){
$.get( "ajax/test.html", function( data ) {
setEvent(eventData._uid,data); //data should be a boolean
});
};
var setEvent = function(id,exists){
var fcEvent = $('#calendar').fullCalendar("clientEvents",function(event){ //get the associated event object
if(event._uid === id){
return true;
}
})[0];
if(typeof fcEvent !== "object")$.error("Event id "+id+" doesn't exist!"); //Throw error if it doesn't exist
fcEvent.display = exists; // Store the server response in the event
$('#calendar-holder').fullCalendar("updateEvent",fcEvent); // Updates and re-renders the event
}
JSFiddle Demo (using fake ajax calls)
Some Explanation
Couple of things that might be useful to know:
The word render in fullcalendar refers to actually displaying the events. It's done whenever the view changes (more often than events are fetched from the DB)
Event sources only fetch events when they are needed. They are stored client-side as data that can be rendered as needed.
eventDataTransform is called once after an event source retrieves an event.
So if you put your ajax call in eventAfterAllRender, the ajax call would be done everytime FC decided to render the calendar resulting in more ajax calls that necessary. It also means you would get a delay every time you change the view. It's much better to do it earlier than render-time.
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');
}
});