How to copy events via ctrl, drag & drop in FullCalendar v5 - pure javascript only
Asked Answered
E

1

7

I would like to implement CTRL + Drag & Drop in FullCalendar v5 using PURE JavaScript only.

I did my reseach on topic and found that this was discussed as a new feature UI request on FC github. There are few suggestions how to do that, even working ones.

arshaw posted on Aug 19, 2015

You can use eventDrop to create this feature. jsEvent has a ctrlKey
on which you can test. Copy the event, received as a parameter, in a new variable.
revertFunc to make go back, and then apply renderEvent with the new variable created.

chris-verclytte posted on Apr 11, 2016 does nothing for me

If it can help, here is a little trick I use waiting for this new feature to be integrated.
In eventDrop callback

    // If no alt key pressed we move the event, else we duplicate it
    if (!jsEvent.altKey) {
        // Handle drag'n'drop copy
    } else {
        // Handle drag'n'drop duplication
        // Here I add the event to event source
        _addEventToSelectedSource(event.start, event.end);
        // "Disable" the default behavior using revertFunc to avoid event moving
         revertFunc();
    }
The only problem with this is that the copied event disappears during drag'n'drop due to https://github.com/fullcalendar/fullcalendar/blob/master/src/common/Grid.events.js#L273

I like the best solution by AllyMurray posted on Jul 13, 2018

For anyone that comes across this issue, I have created a solution that should give you a starting point to work from. It uses the same approach as external events and leaves the original event in place.

https://mcmap.net/q/1422871/-javascript-fullcalendar-copying-events
https://codepen.io/ally-murray/full/JBdaBV/

But I do not know how to implement this solution in pure javascript.

Could anyone help? I prefer the copy to work the way that CTRL press means copy so the original event stays in original position.

jsFiddle

Erfert answered 8/5, 2021 at 16:12 Comment(6)
From https://mcmap.net/q/1422871/-javascript-fullcalendar-copying-events couldn't you just replace the references to .draggable bit with the Draggable as described at fullcalendar.io/docs/external-dragging - it's used there for external events, but in principle it appears there's nothing stopping you making any element draggable using it.Raama
The first one from 2015 seems entirely workable though too, if technically not very efficient.Raama
Did you mean to reaplace .draggable with Draggable or .Draggable? Also I have no idea how to dymanically change FullCalendar options once it is set. What to replace $("#calendar").fullCalendar("option", "eventStartEditable", !isCopyable); with.Erfert
Did you mean to reaplace .draggable with Draggable or .Draggable... it's not just an find-and-replace task. The link I provided shows the syntax for creating a Draggable.Raama
Also I have no idea how to dymanically change FullCalendar options...it's right there in the documentation. fullcalendar.io/docs/dynamic-optionsRaama
Ops. thank you for that. I find FC doc hard to follow. Thanks. I will tryErfert
W
8

I have a minimal solution that works. It consists in cloning the moved event at its original date if the Ctrl key is being held.

To test this snippet, just click into the input at the top of the page before testing, otherwise the result iframe doesn't have the focus and doesn't fire keydown and keyup events.

// Beginning of the workaround for this: https://github.com/fullcalendar/fullcalendar/blob/3e89de5d8206c32b6be326133b6787d54c6fd66c/packages/interaction/src/dnd/PointerDragging.ts#L306
const ctrlKeyDescriptor = Object.getOwnPropertyDescriptor(
  MouseEvent.prototype,
  'ctrlKey'
);

// Always return false for event.ctrlKey when event is of type MouseEvent
ctrlKeyDescriptor.get = function() {
  return false;
};

Object.defineProperty(MouseEvent.prototype, 'ctrlKey', ctrlKeyDescriptor);
// End of the workaround

let calendarEl = document.getElementById('calendar-container');

// Hold the ctrlKey state, emit events to the subscribers when it changes
const ctrlKeyChanged = (function() {
  let ctrlHeld = false;
  let subscriptions = [];
  ['keydown', 'keyup'].forEach(x =>
    document.addEventListener(x, e => {
      // emit only when the key state has changed
      if (ctrlHeld !== e.ctrlKey) subscriptions.forEach(fun => fun(e.ctrlKey));

      ctrlHeld = e.ctrlKey;
    })
  );

  function subscribe(callback) {
    subscriptions.push(callback);
    callback(ctrlHeld); // Emit the current state (case when Ctrl is already being held)
  }

  function unsubscribe(callback) {
    const index = subscriptions.indexOf(callback);
    subscriptions.splice(index, 1);
  }

  return { subscribe, unsubscribe };
})();

const extractEventProperties = ({ title, start, end, allDay }) => ({
  title,
  start,
  end,
  allDay
});

const callbackKey = Symbol();

let calendar = new FullCalendar.Calendar(calendarEl, {
  editable: true,
  droppable: true,
  eventDragStart: e => {
    let event;
    const callback = ctrlKey => {
      if (ctrlKey) {
        event = calendar.addEvent(extractEventProperties(e.event));
      } else {
        event && event.remove();
      }
    };
    ctrlKeyChanged.subscribe(callback);
    // store the callback for further unsubscribe
    e.event.setExtendedProp(callbackKey, callback); 
  },
  // stop listening when the event has been dropped
  eventDragStop: e => ctrlKeyChanged.unsubscribe(e.event.extendedProps[callbackKey]),
  events: [
    {
      title: 'event1',
      start: new Date,
      allDay: true // will make the time show
    },
    {
      title: 'event2',
      start: new Date().setDate(new Date().getDate() + 1),
      end: new Date().setDate(new Date().getDate() + 1)
    },
    {
      title: 'event3',
      start: new Date().setDate(new Date().getDate() - 1),
      allDay: true
    }
  ]
});

calendar.render();
<link href="https://cdn.jsdelivr.net/npm/[email protected]/main.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/main.min.js"></script>

<input placeholder="Click here to give the focus to the result iframe">
<div id="calendar-container"></div>

The main difficulty is that Fullcalendar disables the drag behavior when the Ctrl key is being held:

// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
function isPrimaryMouseButton(ev: MouseEvent) {
  return ev.button === 0 && !ev.ctrlKey
}

You can see this code in the official repository here which is being called here

The workaround consists in mutating MouseEvent.prototype in order to always return false when accessing ctrlKey.

If you're interested, I've made the solution available:

Warnerwarning answered 12/5, 2021 at 21:32 Comment(18)
This solution (codepen.io/ally-murray/full/JBdaBV) works ONLY when pressing CTRL key BEFORE dragging. I like it better. But I would prefer to have solution that it does not matter when the CTRL key is pressed.Erfert
@Erfert the codepen you've linked is made with fullcalendar 3.9. I'll try to find a v5 compliant solution but it might be hacky, because the limitation is intrinsic to the libraryWarnerwarning
you mean the FC library? I know that the version of FC is sometimes a limitation that way I included that even in the question title.Erfert
I have updated my answer with a workaround for Fullcalendar's specificity about the Ctrl keyWarnerwarning
Is it possible to try in jfFiddle or somewhere online? I am not sure how I would use your solution in my project? Did you modify FC source? Can we ask to merge it into offcial code?Erfert
For some reason it didn't work in the Stackblitz sandbox so I created a full project. If you have Node.js installed you can test pretty quickly. You can also copy paste in your project, and remove the two TypeScript types in order to make it plain JS. I didn't modify Fullcalendar source at all.Warnerwarning
You are too technical for me ;-) I am not that nerdy. I do not have installed Node.js, have no idea what you meant by "remove the two TypeScript types". I dont know how to use npm.Erfert
OK then let me edit my answer so you can copy it in your own project.Warnerwarning
Can I use your solution with official FullCalendar code?Erfert
I am getting SyntaxError: redeclaration of import Calendar jsfiddle.net/radek/wrn8gxcm/21Erfert
Remove duplicate importsWarnerwarning
I am using main.js so the two plugins you used should be available and this should be working jsfiddle.net/radek/wrn8gxcm/29 but it is notErfert
or here jsfiddle.net/radek/8v90ra74/11 almost exact copy of your codeErfert
Just found out why it didn't work in Stackblitz and in JSfiddle, it's because the iframe where the results display doesn't have the focus. It shouldn't happen in a real use case, but as a workaround I added an input at the top of the page. Click inside the input then observe the drag and drop behavior: jsfiddle.net/euvnsbatWarnerwarning
cool. it looks very good. One more thing.. is could the event stay in place in case use presses CTRL key?Erfert
You are the Champion. This is exactly what I wanted. Thank you for that.Erfert
For some reason it only works after I right-click somewhere on the viewport or if I navigate with the timeline. I tested with the stackblitz demo given but changed the event dates for dates in my current month (so I wouldn't have to search for the events). Any idea how to fix that?Fung
@MatheusSimon that's why I added an input at the top of the page with Click here to give the focus to the result iframe in the placeholder. This is a StackOverflow related issue you won't have any problem with implementing this in your own page.Warnerwarning

© 2022 - 2024 — McMap. All rights reserved.