MDL-59382 calendar: add modal to create and update events

This commit is contained in:
Ryan Wyllie 2017-07-24 08:01:14 +00:00
parent 6103fd2efe
commit aa0912258d
23 changed files with 2392 additions and 157 deletions

View file

@ -1 +1 @@
define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_factory","core_calendar/summary_modal","core/modal_events","core_calendar/calendar_repository"],function(a,b,c,d,e,f,g,h,i,j){var k={ROOT:"[data-region='calendar']",EVENT_LINK:"[data-action='view-event']"},l=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},m=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?'<a href="'+a.url+'">'+b+"</a>":b})},n=function(b){j.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=l(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=m(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:h.TYPE,body:d.render("core_calendar/event_summary_body",a)};return a.caneditevent||(b.footer=""),g.create(b)}).done(function(a){a.getRoot().on(i.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},o=function(){a(k.EVENT_LINK).click(function(b){b.preventDefault();var c=a(this).attr("data-event-id");n(c)})};return{init:function(){o()}}});
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"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m={ROOT:"[data-region='calendar']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']"},n=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},o=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?'<a href="'+a.url+'">'+b+"</a>":b})},p=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=n(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=o(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)};return a.caneditevent||(b.footer=""),h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(a){var b=a.find(m.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}},b)},r=function(b,c){var d=a("body");d.on(l.created,function(){window.location.reload()}),d.on(l.deleted,function(){window.location.reload()}),d.on(l.updated,function(){window.location.reload()}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})})},s=function(){var b=a(m.ROOT);a(m.EVENT_LINK).click(function(b){b.preventDefault();var c=a(this).attr("data-event-id");p(c)});var c=q(b);r(b,c)};return{init:function(){s()}}});

1
calendar/amd/build/event_form.min.js vendored Normal file
View file

@ -0,0 +1 @@
define(["jquery","core/templates"],function(a,b){var c={EVENT_TYPE:'[name="eventtype"]',EVENT_COURSE_ID:'[name="courseid"]',EVENT_GROUP_COURSE_ID:'[name="groupcourseid"]',EVENT_GROUP_ID:'[name="groupid"]',FORM_GROUP:".form-group",SELECT_OPTION:"option",ADVANCED_ELEMENT:".fitem.advanced",FIELDSET_ADVANCED_ELEMENTS:"fieldset.containsadvancedelements",MORELESS_TOGGLE:".moreless-actions"},d={USER:"user",SITE:"site",COURSE:"course",GROUP:"group"},e={SHOW_ADVANCED:"event_form-show-advanced",HIDE_ADVANCED:"event_form-hide-advanced",ADVANCED_SHOWN:"event_form-advanced-shown",ADVANCED_HIDDEN:"event_form-advanced-hidden"},f=function(a){a.find(c.FIELDSET_ADVANCED_ELEMENTS).removeClass("containsadvancedelements");var d=a.find(c.MORELESS_TOGGLE);b.replaceNode(d,"","")},g=function(a){a.find(c.ADVANCED_ELEMENT).removeClass("hidden"),a.trigger(e.ADVANCED_SHOWN)},h=function(a){a.find(c.ADVANCED_ELEMENT).addClass("hidden"),a.trigger(e.ADVANCED_HIDDEN)},i=function(a){a.on(e.SHOW_ADVANCED,function(){g(a)}),a.on(e.HIDE_ADVANCED,function(){h(a)})},j=function(b){b.find(c.EVENT_GROUP_ID).find(c.SELECT_OPTION).each(function(b,c){var c=a(c),d=c.attr("value"),e=d.split("-"),f=e[0];c.attr("data-course-id",f)})},k=function(a){var b=a.find(c.EVENT_TYPE),e=b.val(),f=a.find(c.EVENT_COURSE_ID).closest(c.FORM_GROUP).removeClass("hidden"),g=a.find(c.EVENT_GROUP_COURSE_ID).closest(c.FORM_GROUP).removeClass("hidden"),h=a.find(c.EVENT_GROUP_ID).closest(c.FORM_GROUP).removeClass("hidden");switch(e){case d.COURSE:g.addClass("hidden"),h.addClass("hidden");break;case d.GROUP:f.addClass("hidden");break;default:f.addClass("hidden"),g.addClass("hidden"),h.addClass("hidden")}},l=function(a){var b=a.find(c.EVENT_TYPE);b.on("change",function(){k(a)})},m=function(b){var d=b.find(c.EVENT_GROUP_COURSE_ID),e=b.find(c.EVENT_GROUP_ID),f=e.find(c.SELECT_OPTION),g=function(){var b=d.val(),c=null;f.each(function(d,e){e=a(e),e.attr("data-course-id")==b?(e.removeClass("hidden"),e.prop("disabled",!1),null===c&&(c=d)):(e.addClass("hidden"),e.prop("disabled",!0))}),e.prop("selectedIndex",c)};d.on("change",g),g()},n=function(b,c){var d=a("#"+b);i(d),f(d),k(d),j(d),l(d),m(d),c?g(d):h(d)};return{init:n,events:e}});

View file

@ -0,0 +1 @@
define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository","core_calendar/event_form"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=!1,n={MORELESS_BUTTON:'[data-action="more-less-toggle"]',SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},o=function(a){g.call(this,a),this.eventId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(n.SAVE_BUTTON),this.moreLessButton=this.getFooter().find(n.MORELESS_BUTTON)};return o.TYPE="core_calendar-modal_event_form",o.prototype=Object.create(g.prototype),o.prototype.constructor=o,o.prototype.setEventId=function(a){this.eventId=a},o.prototype.getEventId=function(){return this.eventId},o.prototype.hasEventId=function(){return null!==this.eventId},o.prototype.getForm=function(){return this.getBody().find("form")},o.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0),this.moreLessButton.prop("disabled",!0)},o.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1),this.moreLessButton.prop("disabled",!1)},o.prototype.setMoreButton=function(){this.moreLessButton.attr("data-collapsed","true"),c.get_string("more","calendar").then(function(a){this.moreLessButton.text(a)}.bind(this))},o.prototype.setLessButton=function(){this.moreLessButton.attr("data-collapsed","false"),c.get_string("less","calendar").then(function(a){this.moreLessButton.text(a)}.bind(this))},o.prototype.toggleMoreLessButton=function(){var a=this.getForm();"true"==this.moreLessButton.attr("data-collapsed")?(a.trigger(l.events.SHOW_ADVANCED),this.setLessButton()):(a.trigger(l.events.HIDE_ADVANCED),this.setMoreButton())},o.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)),this.titlePromise)},o.prototype.reloadBodyContent=function(a,b){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var c=this.saveButton.attr("data-context-id"),e={};return this.hasEventId()&&(e.eventid=this.getEventId()),"undefined"!=typeof a&&(e.formdata=a),e.haserror="undefined"!=typeof b&&b,this.bodyPromise=i.loadFragment("calendar","event_form",c,e),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this))["catch"](d.exception).always(function(){this.reloadingBody=!1}.bind(this)),this.bodyPromise},o.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},o.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},o.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null)},o.prototype.getFormData=function(){return this.getForm().serialize()},o.prototype.save=function(){var b=this.saveButton.find(n.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?this.reloadBodyContent(c,!0):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this))["catch"](d.exception)},o.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,n.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on(f.events.activate,n.MORELESS_BUTTON,function(a,b){this.toggleMoreLessButton(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on(l.events.ADVANCED_SHOWN,function(){this.setLessButton()}.bind(this)),this.getModal().on(l.events.ADVANCED_HIDDEN,function(){this.setMoreButton()}.bind(this))},m||(h.register(o.TYPE,o,"calendar/modal_event_form"),m=!0),o});

View file

@ -1 +1 @@
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/calendar_repository","core_calendar/calendar_events"],function(a,b,c,d,e,f,g,h,i,j){var k=!1,l={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]',EVENT_LINK:'[data-action="event-link"]'},m=function(a){e.call(this,a),this.getFooter().find(l.EDIT_BUTTON).length||c.exception({message:"No edit button found"}),this.getFooter().find(l.DELETE_BUTTON).length||c.exception({message:"No delete button found"})};return m.TYPE="core_calendar-event_summary",m.prototype=Object.create(e.prototype),m.prototype.constructor=m,m.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this);var a=g.create({type:g.types.CONFIRM},this.getFooter().find(l.DELETE_BUTTON)).then(function(a){return b.get_string("confirm").then(function(b){a.setTitle(b)}.bind(this))["catch"](c.exception),a.getRoot().on(h.yes,function(){var b=this.getBody().find(l.ROOT).attr("data-event-id");i.deleteEvent(b).done(function(){a.getRoot().trigger(j.deleted,b),window.location.reload()}).fail(c.exception)}.bind(this)),a}.bind(this));this.getRoot().on(h.bodyRendered,function(){var c=this.getBody().find(l.ROOT).attr("data-event-title");a.then(function(a){a.setBody(b.get_string("confirmeventdelete","core_calendar",c))})}.bind(this))},k||(f.register(m.TYPE,m,"core_calendar/event_summary_modal"),k=!0),m});
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e,f,g,h,i,j){var k=!1,l={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]'},m=function(a){e.call(this,a),this.getEditButton().length||c.exception({message:"No edit button found"}),this.getDeleteButton().length||c.exception({message:"No delete button found"})};return m.TYPE="core_calendar-event_summary",m.prototype=Object.create(e.prototype),m.prototype.constructor=m,m.prototype.getEditButton=function(){return"undefined"==typeof this.editButton&&(this.editButton=this.getFooter().find(l.EDIT_BUTTON)),this.editButton},m.prototype.getDeleteButton=function(){return"undefined"==typeof this.deleteButton&&(this.deleteButton=this.getFooter().find(l.DELETE_BUTTON)),this.deleteButton},m.prototype.getEventId=function(){return this.getBody().find(l.ROOT).attr("data-event-id")},m.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this);var f=g.create({type:g.types.CONFIRM},this.getDeleteButton()).then(function(b){return b.getRoot().on(h.yes,function(){var b=this.getEventId();i.deleteEvent(b).then(function(){a("body").trigger(j.deleted,[b]),this.hide()}.bind(this))["catch"](c.exception)}.bind(this)),b}.bind(this));this.getRoot().on(h.bodyRendered,function(){var a=this.getBody().find(l.ROOT).attr("data-event-title");f.then(function(c){c.setBody(b.get_string("confirmeventdelete","core_calendar",a))})}.bind(this)),d.define(this.getEditButton(),[d.events.activate]),this.getEditButton().on(d.events.activate,function(b,c){a("body").trigger(j.editEvent,[this.getEventId()]),this.hide(),b.preventDefault(),b.stopPropagation(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()}.bind(this))},k||(f.register(m.TYPE,m,"core_calendar/event_summary_modal"),k=!0),m});

View file

@ -14,128 +14,215 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to calendar events.
* This module is the highest level module for the calendar. It is
* responsible for initialising all of the components required for
* the calendar to run. It also coordinates the interaction between
* components by listening for and responding to different events
* triggered within the calendar UI.
*
* @module core_calendar/calendar
* @package core_calendar
* @copyright 2017 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/str', 'core/templates', 'core/notification', 'core/custom_interaction_events',
'core/modal_factory', 'core_calendar/summary_modal', 'core/modal_events', 'core_calendar/calendar_repository'],
function($, Ajax, Str, Templates, Notification, CustomEvents, ModalFactory, SummaryModal, ModalEvents, CalendarRepository) {
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'
],
function(
$,
Ajax,
Str,
Templates,
Notification,
CustomEvents,
ModalEvents,
ModalFactory,
ModalEventForm,
SummaryModal,
CalendarRepository,
CalendarEvents
) {
var SELECTORS = {
ROOT: "[data-region='calendar']",
EVENT_LINK: "[data-action='view-event']",
};
var SELECTORS = {
ROOT: "[data-region='calendar']",
EVENT_LINK: "[data-action='view-event']",
NEW_EVENT_BUTTON: "[data-action='new-event-button']"
};
/**
* Get the event type lang string.
*
* @param {String} eventType The event type.
* @return {promise} The lang string promise.
*/
var getEventType = function(eventType) {
var lang = 'type' + eventType;
return Str.get_string(lang, 'core_calendar').then(function(langStr) {
return langStr;
});
};
/**
* Get the event type lang string.
*
* @param {String} eventType The event type.
* @return {promise} The lang string promise.
*/
var getEventType = function(eventType) {
var lang = 'type' + eventType;
return Str.get_string(lang, 'core_calendar').then(function(langStr) {
return langStr;
});
};
/**
* Get the event source.
*
* @param {Object} subscription The event subscription object.
* @return {promise} The lang string promise.
*/
var getEventSource = function(subscription) {
return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
if (subscription.url) {
return '<a href="' + subscription.url + '">' + langStr + '</a>';
}
return langStr;
});
};
/**
* Get the event source.
*
* @param {Object} subscription The event subscription object.
* @return {promise} The lang string promise.
*/
var getEventSource = function(subscription) {
return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
if (subscription.url) {
return '<a href="' + subscription.url + '">' + langStr + '</a>';
}
return langStr;
});
};
/**
* Render the event summary modal.
*
* @param {Number} eventId The calendar event id.
*/
var renderEventSummaryModal = function(eventId) {
// Calendar repository promise.
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
if (!getEventResponse.event) {
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
}
var eventData = getEventResponse.event;
var eventTypePromise = getEventType(eventData.eventtype);
/**
* Render the event summary modal.
*
* @param {Number} eventId The calendar event id.
*/
var renderEventSummaryModal = function(eventId) {
// Calendar repository promise.
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
if (!getEventResponse.event) {
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
}
var eventData = getEventResponse.event;
var eventTypePromise = getEventType(eventData.eventtype);
// If the calendar event has event source, get the source's language string/link.
if (eventData.displayeventsource) {
eventData.subscription = JSON.parse(eventData.subscription);
var eventSourceParams = {
url: eventData.subscription.url,
name: eventData.subscription.name
};
var eventSourcePromise = getEventSource(eventSourceParams);
// If the calendar event has event source, get the source's language string/link.
if (eventData.displayeventsource) {
eventData.subscription = JSON.parse(eventData.subscription);
var eventSourceParams = {
url: eventData.subscription.url,
name: eventData.subscription.name
};
var eventSourcePromise = getEventSource(eventSourceParams);
// Return event data with event type and event source info.
return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
eventData.eventtype = eventType;
eventData.source = eventSource;
return eventData;
});
}
// Return event data with event type info.
return eventTypePromise.then(function(eventType) {
// Return event data with event type and event source info.
return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
eventData.eventtype = eventType;
eventData.source = eventSource;
return eventData;
});
}).then(function(eventData) {
// Build the modal parameters from the event data.
var modalParams = {
title: eventData.name,
type: SummaryModal.TYPE,
body: Templates.render('core_calendar/event_summary_body', eventData)
};
if (!eventData.caneditevent) {
modalParams.footer = '';
}
// Create the modal.
return ModalFactory.create(modalParams);
}).done(function(modal) {
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
// Finally, render the modal!
modal.show();
}).fail(Notification.exception);
};
/**
* Register event listeners for the module.
*/
var registerEventListeners = function() {
// Bind click events to event links.
$(SELECTORS.EVENT_LINK).click(function(e) {
e.preventDefault();
var eventId = $(this).attr('data-event-id');
renderEventSummaryModal(eventId);
});
};
return {
init: function() {
registerEventListeners();
}
};
});
// Return event data with event type info.
return eventTypePromise.then(function(eventType) {
eventData.eventtype = eventType;
return eventData;
});
}).then(function(eventData) {
// Build the modal parameters from the event data.
var modalParams = {
title: eventData.name,
type: SummaryModal.TYPE,
body: Templates.render('core_calendar/event_summary_body', eventData)
};
if (!eventData.caneditevent) {
modalParams.footer = '';
}
// Create the modal.
return ModalFactory.create(modalParams);
}).done(function(modal) {
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
// Finally, render the modal!
modal.show();
}).fail(Notification.exception);
};
/**
* Create the event form modal for creating new events and
* editing existing events.
*
* @method registerEventFormModal
* @param {object} root The calendar root element
* @return {object} The create modal promise
*/
var registerEventFormModal = function(root) {
var newEventButton = root.find(SELECTORS.NEW_EVENT_BUTTON);
var contextId = newEventButton.attr('data-context-id');
return ModalFactory.create(
{
type: ModalEventForm.TYPE,
large: true,
templateContext: {
contextid: contextId
}
},
newEventButton
);
};
/**
* Listen to and handle any calendar events fired by the calendar UI.
*
* @method registerCalendarEventListeners
* @param {object} root The calendar root element
* @param {object} eventFormModalPromise A promise reolved with the event form modal
*/
var registerCalendarEventListeners = function(root, eventFormModalPromise) {
var body = $('body');
// TODO: Replace these with actual logic to update
// the UI without having to force a page reload.
body.on(CalendarEvents.created, function() { window.location.reload(); });
body.on(CalendarEvents.deleted, function() { window.location.reload(); });
body.on(CalendarEvents.updated, function() { window.location.reload(); });
eventFormModalPromise.then(function(modal) {
// When something within the calendar tells us the user wants
// to edit an event then show the event form modal.
body.on(CalendarEvents.editEvent, function(e, eventId) {
modal.setEventId(eventId);
modal.show();
});
return;
});
};
/**
* Register event listeners for the module.
*/
var registerEventListeners = function() {
var root = $(SELECTORS.ROOT);
// Bind click events to event links.
$(SELECTORS.EVENT_LINK).click(function(e) {
e.preventDefault();
var eventId = $(this).attr('data-event-id');
renderEventSummaryModal(eventId);
});
var eventFormPromise = registerEventFormModal(root);
registerCalendarEventListeners(root, eventFormPromise);
};
return {
init: function() {
registerEventListeners();
}
};
});

View file

@ -0,0 +1,282 @@
// 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 enhance the event form.
*
* @module core_calendar/event_form
* @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/templates'], function($, Templates) {
var SELECTORS = {
EVENT_TYPE: '[name="eventtype"]',
EVENT_COURSE_ID: '[name="courseid"]',
EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
EVENT_GROUP_ID: '[name="groupid"]',
FORM_GROUP: '.form-group',
SELECT_OPTION: 'option',
ADVANCED_ELEMENT: '.fitem.advanced',
FIELDSET_ADVANCED_ELEMENTS: 'fieldset.containsadvancedelements',
MORELESS_TOGGLE: '.moreless-actions'
};
var EVENT_TYPES = {
USER: 'user',
SITE: 'site',
COURSE: 'course',
GROUP: 'group'
};
var EVENTS = {
SHOW_ADVANCED: 'event_form-show-advanced',
HIDE_ADVANCED: 'event_form-hide-advanced',
ADVANCED_SHOWN: 'event_form-advanced-shown',
ADVANCED_HIDDEN: 'event_form-advanced-hidden',
};
/**
* Find the old show more / show less toggle added by the mform and destroy it.
* We are handling the visibility of the advanced fields with the more/less button
* in the footer of the modal that this form is rendered within.
*
* @method destroyOldMoreLessToggle
* @param {object} formElement The root form element
*/
var destroyOldMoreLessToggle = function(formElement) {
formElement.find(SELECTORS.FIELDSET_ADVANCED_ELEMENTS).removeClass('containsadvancedelements');
var element = formElement.find(SELECTORS.MORELESS_TOGGLE);
Templates.replaceNode(element, '', '');
};
/**
* Find each of the advanced form elements and make them visible.
*
* This function triggers the ADVANCED_SHOWN event for any other
* component to handle (e.g. the event form modal).
*
* @method destroyOldMoreLessToggle
* @param {object} formElement The root form element
*/
var showAdvancedElements = function(formElement) {
formElement.find(SELECTORS.ADVANCED_ELEMENT).removeClass('hidden');
formElement.trigger(EVENTS.ADVANCED_SHOWN);
};
/**
* Find each of the advanced form elements and hide them.
*
* This function triggers the ADVANCED_HIDDEN event for any other
* component to handle (e.g. the event form modal).
*
* @method hideAdvancedElements
* @param {object} formElement The root form element
*/
var hideAdvancedElements = function(formElement) {
formElement.find(SELECTORS.ADVANCED_ELEMENT).addClass('hidden');
formElement.trigger(EVENTS.ADVANCED_HIDDEN);
};
/**
* Listen for any events telling this module to show or hide it's
* advanced elements.
*
* This function listens for SHOW_ADVANCED and HIDE_ADVANCED.
*
* @method listenForShowHideEvents
* @param {object} formElement The root form element
*/
var listenForShowHideEvents = function(formElement) {
formElement.on(EVENTS.SHOW_ADVANCED, function() {
showAdvancedElements(formElement);
});
formElement.on(EVENTS.HIDE_ADVANCED, function() {
hideAdvancedElements(formElement);
});
};
/**
* Parse the group id select element in the event form and pull out
* the course id from the value to allow us to toggle other select
* elements based on the course id for the group a user selects.
*
* This is a little hacky but I couldn't find a better way to pass
* the course id for each group id with the limitations of mforms.
*
* The group id options are rendered with a value like:
* "<courseid>-<groupid>"
* E.g.
* For a group with id 10 in a course with id 3 the value of the
* option will be 3-10.
*
* @method parseGroupSelect
* @param {object} formElement The root form element
*/
var parseGroupSelect = function(formElement) {
formElement.find(SELECTORS.EVENT_GROUP_ID)
.find(SELECTORS.SELECT_OPTION)
.each(function(index, element) {
var element = $(element);
var value = element.attr('value');
var splits = value.split('-');
var courseId = splits[0];
element.attr('data-course-id', courseId);
});
};
/**
* Toggle the visibility of the secondary select elements based on
* the event type the user has selected.
*
* There are 3 secondary select elements within the form:
* - course: a list of all courses a user can add course events to
* - group course: a list of all courses a user can add group events to.
* this list can be different from the course list above.
* - group: a list of all groups a user can add an event to. This list will
* be filtered further based on the group course selected.
*
* There are 4 event types:
* - user: none of the secondary selects should be visible.
* - site: none of the secondary selects should be visible.
* - course: "course" select should be visible and both "group course"
* and "group" should be hidden.
* - group: "group course" and "group" should be visible and "course"
* should be hidden.
*
* @method hideTypeSubSelects
* @param {object} formElement The root form element
*/
var hideTypeSubSelects = function(formElement) {
var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
var eventType = typeSelect.val();
var courseIdSelect = formElement.find(SELECTORS.EVENT_COURSE_ID)
.closest(SELECTORS.FORM_GROUP)
.removeClass('hidden');
var groupCourseIdSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID)
.closest(SELECTORS.FORM_GROUP)
.removeClass('hidden');
var groupIdSelect = formElement.find(SELECTORS.EVENT_GROUP_ID)
.closest(SELECTORS.FORM_GROUP)
.removeClass('hidden');
// Hide the unreleated selectors for the given event type.
switch (eventType) {
case EVENT_TYPES.COURSE:
groupCourseIdSelect.addClass('hidden');
groupIdSelect.addClass('hidden');
break;
case EVENT_TYPES.GROUP:
courseIdSelect.addClass('hidden');
break;
default:
courseIdSelect.addClass('hidden');
groupCourseIdSelect.addClass('hidden');
groupIdSelect.addClass('hidden');
}
};
/**
* Listen for when the user changes the event type select in the
* form and then toggle the visibility of the appropriate secondary
* select elements.
*
* See: hideTypeSubSelects.
*
* @method addTypeSelectListeners
* @param {object} formElement The root form element
*/
var addTypeSelectListeners = function(formElement) {
var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
typeSelect.on('change', function() {
hideTypeSubSelects(formElement);
});
};
/**
* Listen for when the user changes the group course when configuring
* a group event and filter the options in the group select to only
* show the groups available within the course the user has selected.
*
* @method addCourseGroupSelectListeners
* @param {object} formElement The root form element
*/
var addCourseGroupSelectListeners = function(formElement) {
var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);
var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID);
var groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION);
var filterGroupSelectOptions = function() {
var selectedCourseId = courseGroupSelect.val();
var selectedIndex = null;
groupSelectOptions.each(function(index, element) {
element = $(element);
if (element.attr('data-course-id') == selectedCourseId) {
element.removeClass('hidden');
element.prop('disabled', false);
if (selectedIndex === null) {
selectedIndex = index;
}
} else {
element.addClass('hidden');
element.prop('disabled', true);
}
});
groupSelect.prop('selectedIndex', selectedIndex);
};
courseGroupSelect.on('change', filterGroupSelectOptions);
filterGroupSelectOptions();
};
/**
* Initialise all of the form enhancementds.
*
* @method init
* @param {string} formId The value of the form's id attribute
* @param {bool} hasError If the form has errors rendered form the server.
*/
var init = function(formId, hasError) {
var formElement = $('#' + formId);
listenForShowHideEvents(formElement);
destroyOldMoreLessToggle(formElement);
hideTypeSubSelects(formElement);
parseGroupSelect(formElement);
addTypeSelectListeners(formElement);
addCourseGroupSelectListeners(formElement);
// If we know that the form has been rendered with server side
// errors then we need to display all of the elements in the form
// in case one of those elements has the error.
if (hasError) {
showAdvancedElements(formElement);
} else {
hideAdvancedElements(formElement);
}
};
return {
init: init,
events: EVENTS,
};
});

View file

@ -0,0 +1,429 @@
// 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/>.
/**
* Contain the logic for the quick add or update event modal.
*
* @module calendar/modal_quick_add_event
* @class modal_quick_add_event
* @package core
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/event',
'core/str',
'core/notification',
'core/templates',
'core/custom_interaction_events',
'core/modal',
'core/modal_registry',
'core/fragment',
'core_calendar/events',
'core_calendar/repository',
'core_calendar/event_form'
],
function(
$,
Event,
Str,
Notification,
Templates,
CustomEvents,
Modal,
ModalRegistry,
Fragment,
CalendarEvents,
Repository,
EventForm
) {
var registered = false;
var SELECTORS = {
MORELESS_BUTTON: '[data-action="more-less-toggle"]',
SAVE_BUTTON: '[data-action="save"]',
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
};
/**
* Constructor for the Modal.
*
* @param {object} root The root jQuery element for the modal
*/
var ModalEventForm = function(root) {
Modal.call(this, root);
this.eventId = null;
this.reloadingBody = false;
this.reloadingTitle = false;
this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_BUTTON);
};
ModalEventForm.TYPE = 'core_calendar-modal_event_form';
ModalEventForm.prototype = Object.create(Modal.prototype);
ModalEventForm.prototype.constructor = ModalEventForm;
/**
* Set the event id to the given value.
*
* @method setEventId
* @param {int} id The event id
*/
ModalEventForm.prototype.setEventId = function(id) {
this.eventId = id;
};
/**
* Retrieve the current event id, if any.
*
* @method getEventId
* @return {int|null} The event id
*/
ModalEventForm.prototype.getEventId = function() {
return this.eventId;
};
/**
* Check if the modal has an event id.
*
* @method hasEventId
* @return {bool}
*/
ModalEventForm.prototype.hasEventId = function() {
return this.eventId !== null;
};
/**
* Get the form element from the modal.
*
* @method getForm
* @return {object}
*/
ModalEventForm.prototype.getForm = function() {
return this.getBody().find('form');
};
/**
* Disable the buttons in the footer.
*
* @method disableButtons
*/
ModalEventForm.prototype.disableButtons = function() {
this.saveButton.prop('disabled', true);
this.moreLessButton.prop('disabled', true);
};
/**
* Enable the buttons in the footer.
*
* @method enableButtons
*/
ModalEventForm.prototype.enableButtons = function() {
this.saveButton.prop('disabled', false);
this.moreLessButton.prop('disabled', false);
};
/**
* Set the more/less button in the footer to the "more"
* state.
*
* @method setMoreButton
*/
ModalEventForm.prototype.setMoreButton = function() {
this.moreLessButton.attr('data-collapsed', 'true');
Str.get_string('more', 'calendar').then(function(string) {
this.moreLessButton.text(string);
}.bind(this));
};
/**
* Set the more/less button in the footer to the "less"
* state.
*
* @method setLessButton
*/
ModalEventForm.prototype.setLessButton = function() {
this.moreLessButton.attr('data-collapsed', 'false');
Str.get_string('less', 'calendar').then(function(string) {
this.moreLessButton.text(string);
}.bind(this));
};
/**
* Toggle the more/less button in the footer from the current
* state to it's opposite state.
*
* @method toggleMoreLessButton
*/
ModalEventForm.prototype.toggleMoreLessButton = function() {
var form = this.getForm();
if (this.moreLessButton.attr('data-collapsed') == 'true') {
form.trigger(EventForm.events.SHOW_ADVANCED);
this.setLessButton();
} else {
form.trigger(EventForm.events.HIDE_ADVANCED);
this.setMoreButton();
}
};
/**
* Reload the title for the modal to the appropriate value
* depending on whether we are creating a new event or
* editing an existing event.
*
* @method reloadTitleContent
* @return {object} A promise resolved with the new title text
*/
ModalEventForm.prototype.reloadTitleContent = function() {
if (this.reloadingTitle) {
return this.titlePromise;
}
this.reloadingTitle = true;
if (this.hasEventId()) {
this.titlePromise = Str.get_string('editevent', 'calendar');
} else {
this.titlePromise = Str.get_string('newevent', 'calendar');
}
this.titlePromise.then(function(string) {
this.setTitle(string);
return string;
}.bind(this))
.always(function() {
this.reloadingTitle = false;
return;
}.bind(this));
return this.titlePromise;
};
/**
* Send a request to the server to get the event_form in a fragment
* and render the result in the body of the modal.
*
* If serialised form data is provided then it will be sent in the
* request to the server to have the form rendered with the data. This
* is used when the form had a server side error and we need the server
* to re-render it for us to display the error to the user.
*
* @method reloadBodyContent
* @param {string} formData The serialised form data
* @param {bool} hasError True if we know the form data is erroneous
* @return {object} A promise resolved with the fragment html and js from
*/
ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
if (this.reloadingBody) {
return this.bodyPromise;
}
this.reloadingBody = true;
this.disableButtons();
var contextId = this.saveButton.attr('data-context-id');
var args = {};
if (this.hasEventId()) {
args.eventid = this.getEventId();
}
if (typeof formData !== 'undefined') {
args.formdata = formData;
}
args.haserror = (typeof hasError == 'undefined') ? false : hasError;
this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
this.setBody(this.bodyPromise);
this.bodyPromise.then(function() {
this.enableButtons();
return;
}.bind(this))
.catch(Notification.exception)
.always(function() {
this.reloadingBody = false;
return;
}.bind(this));
return this.bodyPromise;
};
/**
* Reload both the title and body content.
*
* @method reloadAllContent
* @return {object} promise
*/
ModalEventForm.prototype.reloadAllContent = function() {
return $.when(this.reloadTitleContent(), this.reloadBodyContent());
};
/**
* Kick off a reload the modal content before showing it. This
* is to allow us to re-use the same modal for creating and
* editing different events within the page.
*
* We do the reload when showing the modal rather than hiding it
* to save a request to the server if the user closes the modal
* and never re-opens it.
*
* @method show
*/
ModalEventForm.prototype.show = function() {
this.reloadAllContent();
Modal.prototype.show.call(this);
};
/**
* Clear the event id from the modal when it's closed so
* that it is loaded fresh next time it's displayed.
*
* The event id will be set by the calling code if it wants
* to edit a specific event.
*
* @method hide
*/
ModalEventForm.prototype.hide = function() {
Modal.prototype.hide.call(this);
this.setEventId(null);
};
/**
* Get the serialised form data.
*
* @method getFormData
* @return {string} serialised form data
*/
ModalEventForm.prototype.getFormData = function() {
return this.getForm().serialize();
};
/**
* Send the form data to the server to create or update
* an event.
*
* If there is a server side validation error then we re-request the
* rendered form (with the data) from the server in order to get the
* server side errors to display.
*
* On success the modal is hidden and the page is reloaded so that the
* new event will display.
*
* @method save
* @return {object} A promise
*/
ModalEventForm.prototype.save = function() {
var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
loadingContainer.removeClass('hidden');
this.disableButtons();
var formData = this.getFormData();
// Send the form data to the server for processing.
return Repository.submitCreateUpdateForm(formData)
.then(function(response) {
if (response.validationerror) {
// If there was a server side validation error then
// we need to re-request the rendered form from the server
// in order to display the error for the user.
return this.reloadBodyContent(formData, true);
} else {
// No problemo! Our work here is done.
this.hide();
// Trigger the appropriate calendar event so that the view can
// be updated.
if (this.hasEventId()) {
$('body').trigger(CalendarEvents.updated, [response.event]);
} else {
$('body').trigger(CalendarEvents.created, [response.event]);
}
}
}.bind(this))
.always(function() {
// Regardless of success or error we should always stop
// the loading icon and re-enable the buttons.
loadingContainer.addClass('hidden');
this.enableButtons();
}.bind(this))
.catch(Notification.exception);
};
/**
* Set up all of the event handling for the modal.
*
* @method registerEventListeners
*/
ModalEventForm.prototype.registerEventListeners = function() {
// Apply parent event listeners.
Modal.prototype.registerEventListeners.call(this);
// When the user clicks the save button we trigger the form submission. We need to
// trigger an actual submission because there is some JS code in the form that is
// listening for this event and doing some stuff (e.g. saving draft areas etc).
this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
this.getForm().submit();
data.originalEvent.preventDefault();
e.stopPropagation();
}.bind(this));
// Catch the submit event before it is actually processed by the browser and
// prevent the submission. We'll take it from here.
this.getModal().on('submit', function(e) {
this.save();
// Stop the form from actually submitting and prevent it's
// propagation because we have already handled the event.
e.preventDefault();
e.stopPropagation();
}.bind(this));
// Toggle the state of the more/less button in the footer.
this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
this.toggleMoreLessButton();
data.originalEvent.preventDefault();
e.stopPropagation();
}.bind(this));
// When the event form tells us that the advanced fields are shown
// then the more/less button should be set to less to allow the user
// to hide the advanced fields.
this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
this.setLessButton();
}.bind(this));
// When the event form tells us that the advanced fields are hidden
// then the more/less button should be set to more to allow the user
// to show the advanced fields.
this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
this.setMoreButton();
}.bind(this));
};
// Automatically register with the modal registry the first time this module is imported so that you can create modals
// of this type using the modal factory.
if (!registered) {
ModalRegistry.register(ModalEventForm.TYPE, ModalEventForm, 'calendar/modal_event_form');
registered = true;
}
return ModalEventForm;
});

View file

@ -22,8 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_events', 'core/modal',
'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/calendar_repository',
'core_calendar/calendar_events'],
'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/repository',
'core_calendar/events'],
function($, Str, Notification, CustomEvents, Modal, ModalRegistry, ModalFactory, ModalEvents, CalendarRepository,
CalendarEvents) {
@ -32,7 +32,6 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
ROOT: "[data-region='summary-modal-container']",
EDIT_BUTTON: '[data-action="edit"]',
DELETE_BUTTON: '[data-action="delete"]',
EVENT_LINK: '[data-action="event-link"]'
};
/**
@ -43,11 +42,11 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
var ModalEventSummary = function(root) {
Modal.call(this, root);
if (!this.getFooter().find(SELECTORS.EDIT_BUTTON).length) {
if (!this.getEditButton().length) {
Notification.exception({message: 'No edit button found'});
}
if (!this.getFooter().find(SELECTORS.DELETE_BUTTON).length) {
if (!this.getDeleteButton().length) {
Notification.exception({message: 'No delete button found'});
}
};
@ -56,6 +55,48 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
ModalEventSummary.prototype = Object.create(Modal.prototype);
ModalEventSummary.prototype.constructor = ModalEventSummary;
/**
* Get the edit button element from the footer. The button is cached
* as it's not expected to change.
*
* @method getEditButton
* @return {object} button element
*/
ModalEventSummary.prototype.getEditButton = function() {
if (typeof this.editButton == 'undefined') {
this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON);
}
return this.editButton;
};
/**
* Get the delete button element from the footer. The button is cached
* as it's not expected to change.
*
* @method getDeleteButton
* @return {object} button element
*/
ModalEventSummary.prototype.getDeleteButton = function() {
if (typeof this.deleteButton == 'undefined') {
this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON);
}
return this.deleteButton;
};
/**
* Get the id for the event being shown in this modal. This value is
* not cached because it will change depending on which event is
* being displayed.
*
* @method getEventId
* @return {int}
*/
ModalEventSummary.prototype.getEventId = function() {
return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
};
/**
* Set up all of the event handling for the modal.
*
@ -64,28 +105,51 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
ModalEventSummary.prototype.registerEventListeners = function() {
// Apply parent event listeners.
Modal.prototype.registerEventListeners.call(this);
var confirmPromise = ModalFactory.create({
type: ModalFactory.types.CONFIRM,
}, this.getFooter().find(SELECTORS.DELETE_BUTTON)).then(function(modal) {
Str.get_string('confirm').then(function(languagestring) {
modal.setTitle(languagestring);
}.bind(this)).catch(Notification.exception);
var confirmPromise = ModalFactory.create(
{ type: ModalFactory.types.CONFIRM },
this.getDeleteButton()
).then(function(modal) {
modal.getRoot().on(ModalEvents.yes, function() {
var eventId = this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
CalendarRepository.deleteEvent(eventId).done(function() {
modal.getRoot().trigger(CalendarEvents.deleted, eventId);
window.location.reload();
}).fail(Notification.exception);
var eventId = this.getEventId();
CalendarRepository.deleteEvent(eventId)
.then(function() {
$('body').trigger(CalendarEvents.deleted, [eventId]);
this.hide();
}.bind(this))
.catch(Notification.exception);
}.bind(this));
return modal;
}.bind(this));
// We have to wait for the modal to finish rendering in order to ensure that
// the data-event-title property is available to use as the modal title.
this.getRoot().on(ModalEvents.bodyRendered, function() {
var eventTitle = this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
confirmPromise.then(function(modal) {
modal.setBody(Str.get_string('confirmeventdelete', 'core_calendar', eventTitle));
});
}.bind(this));
CustomEvents.define(this.getEditButton(), [
CustomEvents.events.activate
]);
this.getEditButton().on(CustomEvents.events.activate, function(e, data) {
// When the edit button is clicked we fire an event for the calendar UI to handle.
// We don't care how the UI chooses to handle it.
$('body').trigger(CalendarEvents.editEvent, [this.getEventId()]);
// There is nothing else for us to do so let's hide.
this.hide();
// We've handled this event so no need to propagate it.
e.preventDefault();
e.stopPropagation();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
}.bind(this));
};
// Automatically register with the modal registry the first time this module is imported so that you can create modals