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://stackoverflow.com/a/51327702/3891834
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

Guerric P
  • 30,447
  • 6
  • 48
  • 86
Radek
  • 13,813
  • 52
  • 161
  • 255
  • 1
    From https://stackoverflow.com/a/51327702/3891834 couldn't you just replace the references to `.draggable` bit with the `Draggable` as described at https://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. – ADyson May 09 '21 at 22:48
  • The first one from 2015 seems entirely workable though too, if technically not very efficient. – ADyson May 09 '21 at 22:49
  • 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. – Radek May 10 '21 at 06:54
  • `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. – ADyson May 10 '21 at 07:04
  • 1
    `Also I have no idea how to dymanically change FullCalendar options`...it's right there in the documentation. https://fullcalendar.io/docs/dynamic-options – ADyson May 10 '21 at 07:05
  • Ops. thank you for that. I find FC doc hard to follow. Thanks. I will try – Radek May 10 '21 at 07:27

1 Answers1

7

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/fullcalendar@5.6.0/main.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/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:

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • This solution (https://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. – Radek May 13 '21 at 06:49
  • @Radek 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 library – Guerric P May 13 '21 at 17:34
  • 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. – Radek May 13 '21 at 18:30
  • I have updated my answer with a workaround for Fullcalendar's specificity about the `Ctrl` key – Guerric P May 13 '21 at 20:50
  • 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? – Radek May 13 '21 at 21:10
  • 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. – Guerric P May 13 '21 at 21:12
  • 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. – Radek May 13 '21 at 21:16
  • 1
    OK then let me edit my answer so you can copy it in your own project. – Guerric P May 13 '21 at 21:16
  • Can I use your solution with official FullCalendar code? – Radek May 13 '21 at 21:17
  • I am getting `SyntaxError: redeclaration of import Calendar` https://jsfiddle.net/radek/wrn8gxcm/21/ – Radek May 13 '21 at 21:28
  • Remove duplicate imports – Guerric P May 13 '21 at 21:30
  • I am using main.js so the two plugins you used should be available and this should be working https://jsfiddle.net/radek/wrn8gxcm/29/ but it is not – Radek May 13 '21 at 22:37
  • or here https://jsfiddle.net/radek/8v90ra74/11/ almost exact copy of your code – Radek May 13 '21 at 22:59
  • 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: https://jsfiddle.net/euvnsbat/ – Guerric P May 14 '21 at 09:17
  • cool. it looks very good. One more thing.. is could the event stay in place in case use presses CTRL key? – Radek May 14 '21 at 10:15
  • 1
    You are the Champion. This is exactly what I wanted. Thank you for that. – Radek May 14 '21 at 14:03
  • 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? – Matheus Simon Jun 15 '22 at 15:22
  • @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. – Guerric P Jun 15 '22 at 16:19