Commit aefb2950 authored by Ryan Wyllie's avatar Ryan Wyllie
Browse files

MDL-59394 calendar: add drag and drop between months

parent f6e8cc83
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -168,24 +168,30 @@ define([
* updated.
*
* @param {event} e The calendar move event
* @param {object} eventElement The jQuery element with the event id
* @param {object} originElement The jQuery element for where the event is moving from
* @param {int} eventId The event id being moved
* @param {object|null} originElement The jQuery element for where the event is moving from
* @param {object} destinationElement The jQuery element for where the event is moving to
*/
var handleMoveEvent = function(e, eventElement, originElement, destinationElement) {
var eventId = eventElement.attr('data-event-id');
var originTimestamp = originElement.attr('data-day-timestamp');
var handleMoveEvent = function(e, eventId, originElement, destinationElement) {
var originTimestamp = null;
var destinationTimestamp = destinationElement.attr('data-day-timestamp');
if (originElement) {
originTimestamp = originElement.attr('data-day-timestamp');
}
// If the event has actually changed day.
if (originTimestamp != destinationTimestamp) {
if (!originElement || originTimestamp != destinationTimestamp) {
Templates.render('core/loading', {})
.then(function(html, js) {
// First we show some loading icons in each of the days being affected.
originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
Templates.appendNodeContents(originElement, html, js);
Templates.appendNodeContents(destinationElement, html, js);
if (originElement) {
originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
Templates.appendNodeContents(originElement, html, js);
}
return;
})
.then(function() {
......@@ -195,19 +201,21 @@ define([
.then(function() {
// If the update was successful then broadcast an event letting the calendar
// know that an event has been moved.
$('body').trigger(CalendarEvents.eventMoved, [eventElement, originElement, destinationElement]);
$('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]);
return;
})
.always(function() {
// Always remove the loading icons regardless of whether the update
// request was successful or not.
var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);
var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON);
originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
Templates.replaceNode(originLoadingElement, '', '');
Templates.replaceNode(destinationLoadingElement, '', '');
if (originElement) {
var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);
originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
Templates.replaceNode(originLoadingElement, '', '');
}
return;
})
.fail(Notification.exception);
......@@ -265,7 +273,7 @@ define([
body.on(CalendarEvents.moveEvent, handleMoveEvent);
// When an event is successfully moved we should updated the UI.
body.on(CalendarEvents.eventMoved, function() {
window.location.reload();
CalendarViewManager.reloadCurrentMonth(root);
});
eventFormModalPromise.then(function(modal) {
......
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to store calendar drag and drop data.
*
* This module is unfortunately required because of the limitations
* of the HTML5 drag and drop API and it's ability to provide data
* between the different stages of the drag/drop lifecycle.
*
* @module core_calendar/drag_drop_data_store
* @package core_calendar
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
/* @var {int|null} eventId The id of the event being dragged */
var eventId = null;
/* @var {int|null} durationDays How many days the event spans */
var durationDays = null;
/**
* Store the id of the event being dragged.
*
* @param {int} id The event id
*/
var setEventId = function(id) {
eventId = id;
};
/**
* Get the stored event id.
*
* @return {int|null}
*/
var getEventId = function() {
return eventId;
};
/**
* Check if the store has an event id.
*
* @return {bool}
*/
var hasEventId = function() {
return eventId !== null;
};
/**
* Store the duration (in days) of the event being dragged.
*
* @param {int} days Number of days the event spans
*/
var setDurationDays = function(days) {
durationDays = days;
};
/**
* Get the stored number of days.
*
* @return {int|null}
*/
var getDurationDays = function() {
return durationDays;
};
/**
* Reset all of the stored values.
*/
var clearAll = function() {
setEventId(null);
setDurationDays(null);
};
return {
setEventId: setEventId,
getEventId: getEventId,
hasEventId: hasEventId,
setDurationDays: setDurationDays,
getDurationDays: getDurationDays,
clearAll: clearAll
};
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle calendar drag and drop in the calendar
* month view navigation.
*
* This code is run each time the calendar month view is re-rendered. We
* only register the event handlers once per page load so that the in place
* DOM updates that happen on month change don't continue to register handlers.
*
* @module core_calendar/month_navigation_drag_drop
* @class month_navigation_drag_drop
* @package core_calendar
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core_calendar/drag_drop_data_store',
],
function(
$,
DataStore
) {
var SELECTORS = {
DRAGGABLE: '[draggable="true"][data-region="event-item"]',
DROP_ZONE: '[data-drop-zone="nav-link"]',
};
var HOVER_CLASS = 'bg-primary text-white';
var TARGET_CLASS = 'drop-target';
var HOVER_TIME = 1000; // 1 second hover to change month.
// We store some static variables at the module level because this
// module is called each time the calendar month view is reloaded but
// we want some actions to only occur ones.
/* @var {bool} registered If the event listeners have been added */
var registered = false;
/* @var {int} hoverTimer The timeout id of any timeout waiting for hover */
var hoverTimer = null;
/* @var {object} root The root nav element we're operating on */
var root = null;
/**
* Add or remove the appropriate styling to indicate whether
* the drop target is being hovered over.
*
* @param {object} target The target drop zone element
* @param {bool} hovered If the element is hovered over ot not
*/
var updateHoverState = function(target, hovered) {
if (hovered) {
target.addClass(HOVER_CLASS);
} else {
target.removeClass(HOVER_CLASS);
}
};
/**
* Add some styling to the UI to indicate that the nav links
* are an acceptable drop target.
*/
var addDropZoneIndicator = function() {
root.find(SELECTORS.DROP_ZONE).addClass(TARGET_CLASS);
};
/**
* Remove the styling from the nav links.
*/
var removeDropZoneIndicator = function() {
root.find(SELECTORS.DROP_ZONE).removeClass(TARGET_CLASS);
};
/**
* Get the drop zone target from the event, if one is found.
*
* @param {event} e Javascript event
* @return {object|null}
*/
var getTargetFromEvent = function(e) {
var target = $(e.target).closest(SELECTORS.DROP_ZONE);
return (target.length) ? target : null;
};
/**
* This will add a visual indicator to the calendar UI to
* indicate which nav link is a valid drop zone.
*/
var dragstartHandler = function(e) {
// Make sure the drag event is for a calendar event.
var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);
if (eventElement.length) {
addDropZoneIndicator();
}
};
/**
* Update the hover state of the target nav element when
* the user is dragging an event over it.
*
* This will add a visual indicator to the calendar UI to
* indicate which nav link is being hovered.
*
* @param {event} e The dragover event
*/
var dragoverHandler = function(e) {
e.preventDefault();
var target = getTargetFromEvent(e);
if (!target) {
return;
}
// If we're not draggin a calendar event then
// ignore it.
if (!DataStore.hasEventId()) {
return;
}
if (!hoverTimer) {
hoverTimer = setTimeout(function() {
target.click();
hoverTimer = null;
}, HOVER_TIME);
}
updateHoverState(target, true);
removeDropZoneIndicator();
};
/**
* Update the hover state of the target nav element that was
* previously dragged over but has is no longer a drag target.
*
* This will remove the visual indicator from the calendar UI
* that was added by the dragoverHandler.
*
* @param {event} e The dragstart event
*/
var dragleaveHandler = function(e) {
var target = getTargetFromEvent(e);
if (!target) {
return;
}
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
updateHoverState(target, false);
addDropZoneIndicator();
e.preventDefault();
};
/**
* Remove the visual indicator from the calendar UI that was
* added by the dragoverHandler.
*
* @param {event} e The drop event
*/
var dropHandler = function(e) {
removeDropZoneIndicator();
var target = getTargetFromEvent(e);
if (!target) {
return;
}
updateHoverState(target, false);
e.preventDefault();
};
return {
/**
* Initialise the event handlers for the drag events.
*
* @param {object} rootElement The element containing calendar nav links
*/
init: function(rootElement) {
// Only register the handlers once on the first load.
if (!registered) {
// These handlers are only added the first time the module
// is loaded because we don't want to have a new listener
// added each time the "init" function is called otherwise we'll
// end up with lots of stale handlers.
document.addEventListener('dragstart', dragstartHandler, false);
document.addEventListener('dragover', dragoverHandler, false);
document.addEventListener('dragleave', dragleaveHandler, false);
document.addEventListener('drop', dropHandler, false);
document.addEventListener('dragend', removeDropZoneIndicator, false);
registered = true;
}
// Update the module variable to operate on the given
// root element.
root = $(rootElement);
// If we're currently dragging then add the indicators.
if (DataStore.hasEventId()) {
addDropZoneIndicator();
}
},
};
});
......@@ -14,44 +14,47 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle calendar drag and drop. This module
* unfortunately requires some state to be maintained because of the
* limitations of the HTML5 drag and drop API which means it can't
* be used multiple times with the current implementation.
* A javascript module to handle calendar drag and drop in the calendar
* month view.
*
* @module core_calendar/drag_drop
* @class drag_drop
* @module core_calendar/month_view_drag_drop
* @class month_view_drag_drop
* @package core_calendar
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core_calendar/events'
'core_calendar/events',
'core_calendar/drag_drop_data_store'
],
function(
$,
CalendarEvents
CalendarEvents,
DataStore
) {
var SELECTORS = {
ROOT: "[data-region='calendar']",
DRAGGABLE: '[draggable="true"]',
DROP_ZONE: '[data-drop-zone="true"]',
DRAGGABLE: '[draggable="true"][data-region="event-item"]',
DROP_ZONE: '[data-drop-zone="month-view-day"]',
WEEK: '[data-region="month-view-week"]',
};
var HOVER_CLASS = 'bg-primary';
var HOVER_CLASS = 'bg-primary text-white';
/* @var {bool} registered If the event listeners have been added */
var registered = false;
// Unfortunately we are required to maintain some module
// level state due to the limitations of the HTML5 drag
// and drop API. Specifically the inability to pass data
// between the dragstate and dragover events handlers
// using the DataTransfer object in the event.
/** @var int eventId The event id being moved. */
var eventId = null;
/** @var int duration The number of days the event spans */
var duration = null;
/**
* Get the correct drop zone element from the given javascript
* event.
*
* @param {event} e The javascript event
* @return {object|null}
*/
var getDropZoneFromEvent = function(e) {
var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE);
return (dropZone.length) ? dropZone : null;
};
/**
* Update the hover state for the event in the calendar to reflect
......@@ -70,11 +73,10 @@ define([
* @param {bool} hovered If the target is hovered or not
* @param {int} count How many days to highlight (default to duration)
*/
var updateHoverState = function(target, hovered, count) {
var dropZone = $(target).closest(SELECTORS.DROP_ZONE);
var updateHoverState = function(dropZone, hovered, count) {
if (typeof count === 'undefined') {
// This is how many days we need to highlight.
count = duration;
count = DataStore.getDurationDays();
}
if (hovered) {
......@@ -115,16 +117,20 @@ define([
* @param {event} e The dragstart event
*/
var dragstartHandler = function(e) {
var eventElement = $(e.target);
var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);
if (!eventElement.is('[data-event-id]')) {
eventElement = eventElement.find('[data-event-id]');
if (!eventElement.length) {
return;
}
eventId = eventElement.attr('data-event-id');
eventElement = eventElement.find('[data-event-id]');
var eventId = eventElement.attr('data-event-id');
var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
duration = $(eventsSelector).length;
var duration = $(eventsSelector).length;
DataStore.setEventId(eventId);
DataStore.setDurationDays(duration);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.dropEffect = "move";
......@@ -145,7 +151,14 @@ define([
*/
var dragoverHandler = function(e) {
e.preventDefault();
updateHoverState(e.target, true);
var dropZone = getDropZoneFromEvent(e);
if (!dropZone) {
return;
}
updateHoverState(dropZone, true);
};
/**
......@@ -158,8 +171,14 @@ define([
* @param {event} e The dragstart event
*/
var dragleaveHandler = function(e) {
var dropZone = getDropZoneFromEvent(e);
if (!dropZone) {
return;
}
updateHoverState(dropZone, false);
e.preventDefault();
updateHoverState(e.target, false);
};
/**
......@@ -174,35 +193,46 @@ define([
* @param {event} e The dragstart event
*/
var dropHandler = function(e) {
e.preventDefault();
var dropZone = getDropZoneFromEvent(e);
if (!dropZone) {
DataStore.clearAll();
return;
}
var eventId = DataStore.getEventId();
var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
var eventElement = $(eventElementSelector);
var origin = eventElement.closest(SELECTORS.DROP_ZONE);
var origin = null;
var destination = $(e.target).closest(SELECTORS.DROP_ZONE);
updateHoverState(e.target, false);
$('body').trigger(CalendarEvents.moveEvent, [eventElement, origin, destination]);
if (eventElement.length) {
origin = eventElement.closest(SELECTORS.DROP_ZONE);
}
updateHoverState(dropZone, false);
$('body').trigger(CalendarEvents.moveEvent, [eventId, origin, destination]);
DataStore.clearAll();
e.preventDefault();
};
return {
/**
* Initialise the event handlers for the drag events.
*
* @param {object} root The root calendar element that containers the drag drop elements
*/
init: function(root) {
root = $(root);
root.find(SELECTORS.DRAGGABLE).each(function(index, element) {
element.addEventListener('dragstart', dragstartHandler, true);
});
root.find(SELECTORS.DROP_ZONE).each(function(index, element) {
element.addEventListener('dragover', dragoverHandler, true);
element.addEventListener('dragleave', dragleaveHandler, true);
element.addEventListener('drop', dropHandler, true);
});
init: function() {
if (!registered) {
// These handlers are only added the first time the module
// is loaded because we don't want to have a new listener
// added each time the "init" function is called otherwise we'll
// end up with lots of stale handlers.
document.addEventListener('dragstart', dragstartHandler, false);
document.addEventListener('dragover', dragoverHandler, false);
document.addEventListener('dragleave', dragleaveHandler, false);
document.addEventListener('drop', dropHandler, false);
registered = true;
}
},
};
});