neuronetio / gantt-schedule-timeline-calendar

Gantt Gantt Gantt Timeline Schedule Calendar [ javascript gantt, js gantt, projects gantt, timeline, scheduler, gantt timeline, reservation timeline, react gantt, angular gantt, vue gantt, svelte gantt, booking manager ]
https://gantt-schedule-timeline-calendar.neuronet.io
Other
3.15k stars 367 forks source link

How to update item state while item is currently being moved #402

Closed dq-cg closed 3 months ago

dq-cg commented 3 months ago

Your question and description of the problem goes here I've been playing around with the calendar and so far it's fantastic. I want to set it up so that users can copy / clone items by holding down a certain key, then dragging an item to a different place on the calendar. So far I have been able to get this to work, but ONLY once the user has released the mouse button, as it seems that item state updates are paused while the mouse button is held? The result is a poor experience as nothing happens while the user is moving the item, then once it is dropped in place, the 'copied' item suddenly appears out of nowhere. Ideally the 'copied' item should appear as soon as the user begins to move the original item, to make it obvious that the item has been copied.

I have tried updating the item state within the movement plugin onStart, onMove, & onEnd functions, but it seems to only work when used within onEnd.

Code

let copiedItem = null;
let controlKeyIsDown = false;

const movementConfig = {
    enabled: true,
    events: {
        onStart({ items: { after, targetData } }) {
            if (controlKeyIsDown) {
                // Original item keeps moving, but we create a new 'copied' item in the initial cell to make it look as though the original item has been cloned / copied. Sleight of hand...
                copiedItem = JSON.parse(JSON.stringify(targetData));
                copiedItem.id = gstcApi.GSTCID(generateId());

                // Does not work within onStart or onMove
                // state.update('config.chart.items', (currentItems) => {
                //  currentItems[copiedItem.id] = copiedItem;

                //  return currentItems;
                // });
            }

            return after;
        },
        onMove({ items: { after } }) {
            // if (controlKeyIsDown) {
            //  state.update('config.chart.items', (currentItems) => {
            //      currentItems[copiedItem.id] = copiedItem;

            //      return currentItems;
            //  });
            // }

            return after;
        },
        onEnd({ items: { after } }) {
            if (controlKeyIsDown) {
                // Works within onEnd
                state.update('config.chart.items', (currentItems) => {
                    currentItems[copiedItem.id] = copiedItem;

                    return currentItems;
                });
            }

            copiedItem = null;

            return after;
        }
    }
};

Screenshots or movies / GIF's Ideally, the item state would be updated / original item would be copied within the movementConfig's onStart function, rather than the onEnd function.

copy

neuronetio commented 3 months ago

To create a new item it must have a different id. This id should come from the server - an asynchronous task that you have to wait for. You cannot stop moving items while waiting for a response from the server, so you will need to create some temporary id (e.g. Math.random()+currentTime) and after completing the item shifting operation, send a request to the server to create such an item - when the response comes (together with this random id) you just need to replace the id with the one from the server. To speed up the operation of the component, some "unnecessary" functionalities, including adding new items, are disabled when moving items. So I added the shouldMuteNotNeededMethods setting which prevents this when it is set to false but may result in a drop in performance with a large number of items. Unfortunately, it is currently not possible to clone an item and move the cloned item. When you click on an item, the item selected at the beginning will move - it cannot be replaced with the cloned one. So the effect will be that the cloned item will remain in place and the old item will be moved. If this behavior is enough, below is the code on how to do it in the latest version (you need to update the library).

https://github.com/neuronetio/gantt-schedule-timeline-calendar/assets/25027696/d92d0e13-7830-4894-9e8c-5b786080f9d4

https://github.com/neuronetio/gantt-schedule-timeline-calendar/assets/25027696/d81d113b-eec1-4bbf-ba55-78031d7fdb4d

function generateId() {
  return GSTC.api.GSTCID('temp-' + String(Math.round(Math.random() * 1_000_000) + '-' + Date.now()));
}
/**
 * @type {import('../../dist/plugins/item-movement').Options}
 */
const itemMovementOptions = {
  shouldMuteNotNeededMethods: false, // <- false
  events: {
    onStart({ items }) {
      const cloned = GSTC.api.merge({}, items.targetData, { id: generateId() });
      cloned.dependant = [];
      cloned.linkedWith = [];
      state.update('config.chart.items', (items) => {
        items[cloned.id] = cloned;
        return items;
      });
      return items.after;
    },
    onMove({ items }) {
      console.log('after', items.after);
      for (let i = 0, len = items.after.length; i < len; i++) {
        const item = items.after[i];
        if (!canMove(item)) return items.before;
      }
      return items.after;
    },
  },
};
dq-cg commented 3 months ago

That's exactly what I was after, thank you for the prompt response - much appreciated.

dq-cg commented 3 months ago

Hi again Rafał,

Thanks for the previous update, I've been experimenting with it and noticed a visual bug when moving items with shouldMuteNotNeededMethods disabled. If multiple items are stacked within the same cell, whatever calculation is done to set their height offset appears to not be working.

E.g. Moving an item normally: without control

Moving an item with shouldMuteNotNeededMethods disabled: with control

Is there an additional config option I'm maybe missing to prevent this from happening? Alternatively, is there a way of forcing items to always render with the correct offset?

Thanks for your help.

neuronetio commented 3 months ago

Fixed in 3.37.5. Only now, when an item is cloned, it will place one item under the other (just like other items) - to avoid this you can use the code below. For this purpose we will use the overlap items option. https://gantt-schedule-timeline-calendar.neuronet.io/documentation/configuration/chart/items

function generateId() {
  return GSTC.api.GSTCID('temp-' + String(Math.round(Math.random() * 1_000_000) + '-' + Date.now()));
}
let cloned; // <- save the cloned item here
const movementPluginConfig = {
  shouldMuteNotNeededMethods: false,
  events: {
    onStart({ items }) {
      cloned = GSTC.api.merge({}, items.targetData, { id: generateId() });
      cloned.dependant = [];
      cloned.linkedWith = [];
      cloned.overlap = true; // allow overlap
      items.targetData.overlap = true; // allow overlap
      state.update('config.chart.items', (items) => {
        items[cloned.id] = cloned;
        return items;
      });
      return items.after;
    },
    onEnd({ items }) {
      const target = GSTC.api.merge({}, items.targetData);
      state.update('config.chart.items', (items) => {
        items[cloned.id].overlap = false; // move items to the next line
        items[target.id].overlap = false; // move items to the next line
        return items;
      });
      return items.after;
    },
  },
};
dq-cg commented 3 months ago

Legend, thank you.