From aefb2950d3a0b99c164c2ee35974f6a7f8b975cb Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 24 Aug 2017 08:20:35 +0000 Subject: [PATCH] MDL-59394 calendar: add drag and drop between months --- calendar/amd/build/calendar.min.js | 2 +- calendar/amd/build/drag_drop.min.js | 1 - .../amd/build/drag_drop_data_store.min.js | 1 + .../build/month_navigation_drag_drop.min.js | 1 + .../amd/build/month_view_drag_drop.min.js | 1 + calendar/amd/build/view_manager.min.js | 2 +- calendar/amd/src/calendar.js | 36 +-- calendar/amd/src/drag_drop_data_store.js | 95 ++++++++ .../amd/src/month_navigation_drag_drop.js | 221 ++++++++++++++++++ .../{drag_drop.js => month_view_drag_drop.js} | 128 ++++++---- calendar/amd/src/view_manager.js | 2 +- calendar/lib.php | 2 + calendar/templates/month_detailed.mustache | 8 +- calendar/templates/month_navigation.mustache | 8 +- theme/boost/scss/moodle/calendar.scss | 7 + .../bootstrapbase/less/moodle/bs4-compat.less | 43 ++++ theme/bootstrapbase/less/moodle/calendar.less | 8 + theme/bootstrapbase/less/moodle/core.less | 4 + theme/bootstrapbase/style/moodle.css | 44 ++++ 19 files changed, 542 insertions(+), 72 deletions(-) delete mode 100644 calendar/amd/build/drag_drop.min.js create mode 100644 calendar/amd/build/drag_drop_data_store.min.js create mode 100644 calendar/amd/build/month_navigation_drag_drop.min.js create mode 100644 calendar/amd/build/month_view_drag_drop.min.js create mode 100644 calendar/amd/src/drag_drop_data_store.js create mode 100644 calendar/amd/src/month_navigation_drag_drop.js rename calendar/amd/src/{drag_drop.js => month_view_drag_drop.js} (64%) diff --git a/calendar/amd/build/calendar.min.js b/calendar/amd/build/calendar.min.js index c7974037cad..5167149de1d 100644 --- a/calendar/amd/build/calendar.min.js +++ b/calendar/amd/build/calendar.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?''+b+"":b})},q=function(b){k.getEventById(b).then(function(c){if(!c.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+b);var d=c.event,e=o(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=p(f);return a.when(e,g).then(function(a,b){return d.eventtype=a,d.source=b,d})}return e.then(function(a){return d.eventtype=a,d})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},r=function(b,c,f,g){var h=c.attr("data-event-id"),i=f.attr("data-day-timestamp"),j=g.attr("data-day-timestamp");i!=j&&d.render("core/loading",{}).then(function(a,b){f.find(n.DAY_CONTENT).addClass("hidden"),g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b),d.appendNodeContents(g,a,b)}).then(function(){return k.updateEventStartDay(h,j)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=f.find(n.LOADING_ICON),b=g.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),d.replaceNode(b,"","")}).fail(e.exception)},s=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}},[a,n.NEW_EVENT_BUTTON])},t=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth()}),d.on(l.deleted,function(){m.reloadCurrentMonth()}),d.on(l.updated,function(){m.reloadCurrentMonth()}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,r),d.on(l.eventMoved,function(){window.location.reload()}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})})},u=function(){var b=a(n.ROOT);b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),q(d)});var c=s(b);t(b,c),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var e=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(e),a.show()}),b.preventDefault()}})};return{init:function(){m.init(),u()}}}); \ No newline at end of file +define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?''+b+"":b})},q=function(b){k.getEventById(b).then(function(c){if(!c.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+b);var d=c.event,e=o(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=p(f);return a.when(e,g).then(function(a,b){return d.eventtype=a,d.source=b,d})}return e.then(function(a){return d.eventtype=a,d})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},r=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},s=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}},[a,n.NEW_EVENT_BUTTON])},t=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth()}),d.on(l.deleted,function(){m.reloadCurrentMonth()}),d.on(l.updated,function(){m.reloadCurrentMonth()}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,r),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})})},u=function(){var b=a(n.ROOT);b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),q(d)});var c=s(b);t(b,c),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var e=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(e),a.show()}),b.preventDefault()}})};return{init:function(){m.init(),u()}}}); \ No newline at end of file diff --git a/calendar/amd/build/drag_drop.min.js b/calendar/amd/build/drag_drop.min.js deleted file mode 100644 index 249c6d6c060..00000000000 --- a/calendar/amd/build/drag_drop.min.js +++ /dev/null @@ -1 +0,0 @@ -define(["jquery","core_calendar/events"],function(a,b){var c={ROOT:"[data-region='calendar']",DRAGGABLE:'[draggable="true"]',DROP_ZONE:'[data-drop-zone="true"]',WEEK:'[data-region="month-view-week"]'},d="bg-primary",e=null,f=null,g=function(b,e,h){var i=a(b).closest(c.DROP_ZONE);if("undefined"==typeof h&&(h=f),e?i.addClass(d):i.removeClass(d),h--,h>0){var j=i.next();if(!j.length){var k=i.closest(c.WEEK).next();k.length&&(j=k.children(c.DROP_ZONE).first())}j.length&&g(j,e,h)}},h=function(b){var d=a(b.target);d.is("[data-event-id]")||(d=d.find("[data-event-id]")),e=d.attr("data-event-id");var g=c.ROOT+' [data-event-id="'+e+'"]';f=a(g).length,b.dataTransfer.effectAllowed="move",b.dataTransfer.dropEffect="move",b.dataTransfer.setData("text/plain",e),b.dropEffect="move"},i=function(a){a.preventDefault(),g(a.target,!0)},j=function(a){a.preventDefault(),g(a.target,!1)},k=function(d){d.preventDefault();var f=c.ROOT+' [data-event-id="'+e+'"]',h=a(f),i=h.closest(c.DROP_ZONE),j=a(d.target).closest(c.DROP_ZONE);g(d.target,!1),a("body").trigger(b.moveEvent,[h,i,j])};return{init:function(b){b=a(b),b.find(c.DRAGGABLE).each(function(a,b){b.addEventListener("dragstart",h,!0)}),b.find(c.DROP_ZONE).each(function(a,b){b.addEventListener("dragover",i,!0),b.addEventListener("dragleave",j,!0),b.addEventListener("drop",k,!0)})}}}); \ No newline at end of file diff --git a/calendar/amd/build/drag_drop_data_store.min.js b/calendar/amd/build/drag_drop_data_store.min.js new file mode 100644 index 00000000000..e2cc7decf47 --- /dev/null +++ b/calendar/amd/build/drag_drop_data_store.min.js @@ -0,0 +1 @@ +define([],function(){var a=null,b=null,c=function(b){a=b},d=function(){return a},e=function(){return null!==a},f=function(a){b=a},g=function(){return b},h=function(){c(null),f(null)};return{setEventId:c,getEventId:d,hasEventId:e,setDurationDays:f,getDurationDays:g,clearAll:h}}); \ No newline at end of file diff --git a/calendar/amd/build/month_navigation_drag_drop.min.js b/calendar/amd/build/month_navigation_drag_drop.min.js new file mode 100644 index 00000000000..a8888bf3f49 --- /dev/null +++ b/calendar/amd/build/month_navigation_drag_drop.min.js @@ -0,0 +1 @@ +define(["jquery","core_calendar/drag_drop_data_store"],function(a,b){var c={DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="nav-link"]'},d="bg-primary text-white",e="drop-target",f=1e3,g=!1,h=null,i=null,j=function(a,b){b?a.addClass(d):a.removeClass(d)},k=function(){i.find(c.DROP_ZONE).addClass(e)},l=function(){i.find(c.DROP_ZONE).removeClass(e)},m=function(b){var d=a(b.target).closest(c.DROP_ZONE);return d.length?d:null},n=function(b){var d=a(b.target).closest(c.DRAGGABLE);d.length&&k()},o=function(a){a.preventDefault();var c=m(a);c&&b.hasEventId()&&(h||(h=setTimeout(function(){c.click(),h=null},f)),j(c,!0),l())},p=function(a){var b=m(a);b&&(h&&(clearTimeout(h),h=null),j(b,!1),k(),a.preventDefault())},q=function(a){l();var b=m(a);b&&(j(b,!1),a.preventDefault())};return{init:function(c){g||(document.addEventListener("dragstart",n,!1),document.addEventListener("dragover",o,!1),document.addEventListener("dragleave",p,!1),document.addEventListener("drop",q,!1),document.addEventListener("dragend",l,!1),g=!0),i=a(c),b.hasEventId()&&k()}}}); \ No newline at end of file diff --git a/calendar/amd/build/month_view_drag_drop.min.js b/calendar/amd/build/month_view_drag_drop.min.js new file mode 100644 index 00000000000..b2ee13dec67 --- /dev/null +++ b/calendar/amd/build/month_view_drag_drop.min.js @@ -0,0 +1 @@ +define(["jquery","core_calendar/events","core_calendar/drag_drop_data_store"],function(a,b,c){var d={ROOT:"[data-region='calendar']",DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="month-view-day"]',WEEK:'[data-region="month-view-week"]'},e="bg-primary text-white",f=!1,g=function(b){var c=a(b.target).closest(d.DROP_ZONE);return c.length?c:null},h=function(a,b,f){if("undefined"==typeof f&&(f=c.getDurationDays()),b?a.addClass(e):a.removeClass(e),f--,f>0){var g=a.next();if(!g.length){var i=a.closest(d.WEEK).next();i.length&&(g=i.children(d.DROP_ZONE).first())}g.length&&h(g,b,f)}},i=function(b){var e=a(b.target).closest(d.DRAGGABLE);if(e.length){e=e.find("[data-event-id]");var f=e.attr("data-event-id"),g=d.ROOT+' [data-event-id="'+f+'"]',h=a(g).length;c.setEventId(f),c.setDurationDays(h),b.dataTransfer.effectAllowed="move",b.dataTransfer.dropEffect="move",b.dataTransfer.setData("text/plain",f),b.dropEffect="move"}},j=function(a){a.preventDefault();var b=g(a);b&&h(b,!0)},k=function(a){var b=g(a);b&&(h(b,!1),a.preventDefault())},l=function(e){var f=g(e);if(!f)return void c.clearAll();var i=c.getEventId(),j=d.ROOT+' [data-event-id="'+i+'"]',k=a(j),l=null,m=a(e.target).closest(d.DROP_ZONE);k.length&&(l=k.closest(d.DROP_ZONE)),h(f,!1),a("body").trigger(b.moveEvent,[i,l,m]),c.clearAll(),e.preventDefault()};return{init:function(){f||(document.addEventListener("dragstart",i,!1),document.addEventListener("dragover",j,!1),document.addEventListener("dragleave",k,!1),document.addEventListener("drop",l,!1),f=!0)}}}); \ No newline at end of file diff --git a/calendar/amd/build/view_manager.min.js b/calendar/amd/build/view_manager.min.js index dc730d31728..bb8c146c02c 100644 --- a/calendar/amd/build/view_manager.min.js +++ b/calendar/amd/build/view_manager.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:"span.calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper"},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=a(b).find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),e=a(c.currentTarget);i(e.attr("href"),e.data("time"),d),c.preventDefault()})},h=function(a,e){return d.getCalendarMonthData(a,e).then(function(a){return b.render("core_calendar/month_detailed",a)}).then(function(a,c){return b.replaceNode(f.CALENDAR_MONTH_WRAPPER,a,c)}).fail(c.exception)},i=function(b,c,d){return h(c,d).then(function(){return window.history.pushState({},"",b),arguments}).then(function(){return a("body").trigger(e.monthChanged,[c,d]),arguments})},j=function(){var b=a(f.ROOT),c=b.find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),d=b.find(f.CALENDAR_MONTH_WRAPPER).data("current-time");return h(d,c)};return{init:function(){g(f.ROOT)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper"},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=a(b).find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),e=a(c.currentTarget);i(e.attr("href"),e.data("time"),d),c.preventDefault()})},h=function(a,e){return d.getCalendarMonthData(a,e).then(function(a){return b.render("core_calendar/month_detailed",a)}).then(function(a,c){return b.replaceNode(f.CALENDAR_MONTH_WRAPPER,a,c)}).fail(c.exception)},i=function(b,c,d){return h(c,d).then(function(){return window.history.pushState({},"",b),arguments}).then(function(){return a("body").trigger(e.monthChanged,[c,d]),arguments})},j=function(){var b=a(f.ROOT),c=b.find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),d=b.find(f.CALENDAR_MONTH_WRAPPER).data("current-time");return h(d,c)};return{init:function(){g(f.ROOT)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}}); \ No newline at end of file diff --git a/calendar/amd/src/calendar.js b/calendar/amd/src/calendar.js index e75671c59fd..5231a6cef4e 100644 --- a/calendar/amd/src/calendar.js +++ b/calendar/amd/src/calendar.js @@ -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) { diff --git a/calendar/amd/src/drag_drop_data_store.js b/calendar/amd/src/drag_drop_data_store.js new file mode 100644 index 00000000000..6414f8ef88e --- /dev/null +++ b/calendar/amd/src/drag_drop_data_store.js @@ -0,0 +1,95 @@ +// 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 . + +/** + * 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 + * @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 + }; +}); diff --git a/calendar/amd/src/month_navigation_drag_drop.js b/calendar/amd/src/month_navigation_drag_drop.js new file mode 100644 index 00000000000..0e192df437c --- /dev/null +++ b/calendar/amd/src/month_navigation_drag_drop.js @@ -0,0 +1,221 @@ +// 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 . + +/** + * 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 + * @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(); + } + }, + }; +}); diff --git a/calendar/amd/src/drag_drop.js b/calendar/amd/src/month_view_drag_drop.js similarity index 64% rename from calendar/amd/src/drag_drop.js rename to calendar/amd/src/month_view_drag_drop.js index de085f0f4f7..05b37331215 100644 --- a/calendar/amd/src/drag_drop.js +++ b/calendar/amd/src/month_view_drag_drop.js @@ -14,44 +14,47 @@ // along with Moodle. If not, see . /** - * 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 * @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; + } }, }; }); diff --git a/calendar/amd/src/view_manager.js b/calendar/amd/src/view_manager.js index eccb0a3540d..38e7971a649 100644 --- a/calendar/amd/src/view_manager.js +++ b/calendar/amd/src/view_manager.js @@ -26,7 +26,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito var SELECTORS = { ROOT: "[data-region='calendar']", - CALENDAR_NAV_LINK: "span.calendarwrapper .arrow_link", + CALENDAR_NAV_LINK: ".calendarwrapper .arrow_link", CALENDAR_MONTH_WRAPPER: ".calendarwrapper", }; diff --git a/calendar/lib.php b/calendar/lib.php index b088e7140ee..3ad809b6b59 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -2175,6 +2175,7 @@ function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = $attrs = [ 'data-time' => calendar_get_timestamp($d, $m, $y, $time), + 'data-drop-zone' => 'nav-link', ]; return link_arrow_left($text, $href->out(false), $accesshide, 'previous', $attrs); @@ -2202,6 +2203,7 @@ function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = fals $attrs = [ 'data-time' => calendar_get_timestamp($d, $m, $y, $time), + 'data-drop-zone' => 'nav-link', ]; return link_arrow_right($text, $href->out(false), $accesshide, 'next', $attrs); diff --git a/calendar/templates/month_detailed.mustache b/calendar/templates/month_detailed.mustache index 9d170862943..ec8cf9d6046 100644 --- a/calendar/templates/month_detailed.mustache +++ b/calendar/templates/month_detailed.mustache @@ -31,10 +31,10 @@ { } }} -
+
{{> core_calendar/month_header }} {{> core_calendar/month_navigation }} - +
{{# daynames }} @@ -58,7 +58,7 @@ }}{{#durationevents}} duration_{{.}}{{/durationevents}}{{! }}" data-day-timestamp="{{timestamp}}" - data-drop-zone="true" + data-drop-zone="month-view-day" data-region="day" data-new-event-timestamp="{{neweventtimestamp}}">
@@ -112,7 +112,7 @@
{{#js}} -require(['jquery', 'core_calendar/drag_drop'], function($, DragDrop) { +require(['jquery', 'core_calendar/month_view_drag_drop'], function($, DragDrop) { var root = $('#month-detailed-{{uniqid}}'); DragDrop.init(root); }); diff --git a/calendar/templates/month_navigation.mustache b/calendar/templates/month_navigation.mustache index d7d350955a4..f0f67ddcc68 100644 --- a/calendar/templates/month_navigation.mustache +++ b/calendar/templates/month_navigation.mustache @@ -32,7 +32,13 @@ } }} {{#navigation}} -
+
{{{navigation}}}
{{/navigation}} +{{#js}} +require(['jquery', 'core_calendar/month_navigation_drag_drop'], function($, DragDrop) { + var root = $('#month-navigation-{{uniqid}}'); + DragDrop.init(root); +}); +{{/js}} diff --git a/theme/boost/scss/moodle/calendar.scss b/theme/boost/scss/moodle/calendar.scss index df6829ee144..2c047052fc3 100644 --- a/theme/boost/scss/moodle/calendar.scss +++ b/theme/boost/scss/moodle/calendar.scss @@ -48,6 +48,7 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue. .previous { text-align: left; + border: 1px solid transparent; } .current { @@ -57,6 +58,12 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue. .next { text-align: right; + border: 1px solid transparent; + } + + .drop-target { + box-sizing: border-box; + border: 1px dashed $brand-primary; } } diff --git a/theme/bootstrapbase/less/moodle/bs4-compat.less b/theme/bootstrapbase/less/moodle/bs4-compat.less index 15d05d2ad51..ccc168ee750 100644 --- a/theme/bootstrapbase/less/moodle/bs4-compat.less +++ b/theme/bootstrapbase/less/moodle/bs4-compat.less @@ -254,3 +254,46 @@ text-overflow: ellipsis; white-space: nowrap; } + +.text-white { + color: @white; +} + +// Bg utilities. +.bg { + // Important (red) + &-danger { + background-color: @errorText; + } + &-danger[href] { + background-color: darken(@errorText, 10%); + } + // Warnings (orange) + &-warning { + background-color: @orange; + } + &-warning[href] { + background-color: darken(@orange, 10%); + } + // Success (green) + &-success { + background-color: @successText; + } + &-success[href] { + background-color: darken(@successText, 10%); + } + // Info (turquoise) + &-info { + background-color: @infoText; + } + &-info[href] { + background-color: darken(@infoText, 10%); + } + // Primary (blue) + &-primary { + background-color: @blue; + } + &-primary[href] { + background-color: darken(@blue, 10%); + } +} diff --git a/theme/bootstrapbase/less/moodle/calendar.less b/theme/bootstrapbase/less/moodle/calendar.less index d89dd37fd03..f74dcc8236f 100644 --- a/theme/bootstrapbase/less/moodle/calendar.less +++ b/theme/bootstrapbase/less/moodle/calendar.less @@ -45,6 +45,8 @@ } .previous { text-align: left; + border: 1px solid transparent; + box-sizing: border-box; } .current { text-align: center; @@ -52,6 +54,12 @@ } .next { text-align: right; + border: 1px solid transparent; + box-sizing: border-box; + } + .drop-target { + box-sizing: border-box; + border: 1px dashed @blue; } } .filters { diff --git a/theme/bootstrapbase/less/moodle/core.less b/theme/bootstrapbase/less/moodle/core.less index 85e2608c04a..7a4d53ba159 100644 --- a/theme/bootstrapbase/less/moodle/core.less +++ b/theme/bootstrapbase/less/moodle/core.less @@ -2385,3 +2385,7 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions { .clickable { cursor: pointer; } + +[data-drag-type="move"] { + cursor: move; +} diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 70102d31a8f..59154a6bdec 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -4777,6 +4777,9 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions { .clickable { cursor: pointer; } +[data-drag-type="move"] { + cursor: move; +} /* admin.less */ .formtable tbody th { font-weight: normal; @@ -5576,6 +5579,8 @@ img.iconsmall { } .path-calendar .calendar-controls .previous { text-align: left; + border: 1px solid transparent; + box-sizing: border-box; } .path-calendar .calendar-controls .current { text-align: center; @@ -5583,6 +5588,12 @@ img.iconsmall { } .path-calendar .calendar-controls .next { text-align: right; + border: 1px solid transparent; + box-sizing: border-box; +} +.path-calendar .calendar-controls .drop-target { + box-sizing: border-box; + border: 1px dashed #049cdb; } .path-calendar .filters table { border-collapse: separate; @@ -21827,3 +21838,36 @@ ul.indented-list { text-overflow: ellipsis; white-space: nowrap; } +.text-white { + color: #fff; +} +.bg-danger { + background-color: #b94a48; +} +.bg-danger[href] { + background-color: #953b39; +} +.bg-warning { + background-color: #f89406; +} +.bg-warning[href] { + background-color: #c67605; +} +.bg-success { + background-color: #468847; +} +.bg-success[href] { + background-color: #356635; +} +.bg-info { + background-color: #3a87ad; +} +.bg-info[href] { + background-color: #2d6987; +} +.bg-primary { + background-color: #049cdb; +} +.bg-primary[href] { + background-color: #0378a9; +}