Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * A javascript module to handler calendar view changes.
18
 *
19
 * @module     core_calendar/view_manager
20
 * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import $ from 'jquery';
25
import Templates from 'core/templates';
26
import Notification from 'core/notification';
27
import * as CalendarRepository from 'core_calendar/repository';
28
import CalendarEvents from 'core_calendar/events';
29
import * as CalendarSelectors from 'core_calendar/selectors';
30
import ModalEvents from 'core/modal_events';
31
import SummaryModal from 'core_calendar/summary_modal';
32
import CustomEvents from 'core/custom_interaction_events';
33
import {getString} from 'core/str';
34
import Pending from 'core/pending';
35
import {prefetchStrings} from 'core/prefetch';
36
import Url from 'core/url';
37
import Config from 'core/config';
38
 
39
/**
40
 * Limit number of events per day
41
 *
42
 */
43
const LIMIT_DAY_EVENTS = 5;
44
 
45
/**
46
 * Hide day events if more than 5.
47
 *
48
 */
49
export const foldDayEvents = () => {
50
    const root = $(CalendarSelectors.elements.monthDetailed);
51
    const days = root.find(CalendarSelectors.day);
52
    if (days.length === 0) {
53
        return;
54
    }
55
    days.each(function() {
56
        const dayContainer = $(this);
57
        const eventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-eventtype]`;
58
        const filteredEventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-filtered="true"]`;
59
        const moreEventsSelector = `${CalendarSelectors.elements.dateContent} [data-action="view-more-events"]`;
60
        const events = dayContainer.find(eventsSelector);
61
        if (events.length === 0) {
62
            return;
63
        }
64
 
65
        const filteredEvents = dayContainer.find(filteredEventsSelector);
66
        const numberOfFiltered = filteredEvents.length;
67
        const numberOfEvents = events.length - numberOfFiltered;
68
 
69
        let count = 1;
70
        events.each(function() {
71
            const event = $(this);
72
            const isNotFiltered = event.attr('data-event-filtered') !== 'true';
73
            const offset = (numberOfEvents === LIMIT_DAY_EVENTS) ? 0 : 1;
74
            if (isNotFiltered) {
75
                if (count > LIMIT_DAY_EVENTS - offset) {
76
                    event.attr('data-event-folded', 'true');
77
                    event.hide();
78
                } else {
79
                    event.attr('data-event-folded', 'false');
80
                    event.show();
81
                    count++;
82
                }
83
            } else {
84
                // It's being filtered out.
85
                event.attr('data-event-folded', 'false');
86
            }
87
        });
88
 
89
        const moreEventsLink = dayContainer.find(moreEventsSelector);
90
        if (numberOfEvents > LIMIT_DAY_EVENTS) {
91
            const numberOfHiddenEvents = numberOfEvents - LIMIT_DAY_EVENTS + 1;
92
            moreEventsLink.show();
93
            getString('moreevents', 'calendar', numberOfHiddenEvents).then(str => {
94
                const link = moreEventsLink.find('strong a');
95
                moreEventsLink.attr('data-event-folded', 'false');
96
                link.text(str);
97
                return str;
98
            }).catch(Notification.exception);
99
        } else {
100
            moreEventsLink.hide();
101
        }
102
    });
103
};
104
 
105
/**
106
 * Register and handle month calendar events.
107
 *
108
 * @param {string} pendingId pending id.
109
 */
110
export const registerEventListenersForMonthDetailed = (pendingId) => {
111
    const events = `${CalendarEvents.viewUpdated}`;
112
    $('body').on(events, function(e) {
113
        foldDayEvents(e);
114
    });
115
    foldDayEvents();
116
    $('body').on(CalendarEvents.filterChanged, function(e, data) {
117
        const root = $(CalendarSelectors.elements.monthDetailed);
118
        const pending = new Pending(pendingId);
119
        const target = root.find(CalendarSelectors.eventType[data.type]);
120
        const transitionPromise = $.Deferred();
121
        if (data.hidden) {
122
            transitionPromise.then(function() {
123
                target.attr('data-event-filtered', 'true');
124
                return target.hide().promise();
125
            }).fail();
126
        } else {
127
            transitionPromise.then(function() {
128
                target.attr('data-event-filtered', 'false');
129
                return target.show().promise();
130
            }).fail();
131
        }
132
 
133
        transitionPromise.then(function() {
134
            foldDayEvents();
135
            return;
136
        })
137
        .always(pending.resolve)
138
        .fail();
139
 
140
        transitionPromise.resolve();
141
    });
142
};
143
 
144
/**
145
 * Register event listeners for the module.
146
 *
147
 * @param {object} root The root element.
148
 */
149
const registerEventListeners = (root) => {
150
    root = $(root);
151
 
152
    // Bind click events to event links.
153
    root.on('click', CalendarSelectors.links.eventLink, (e) => {
154
        const target = e.target;
155
        let eventLink = null;
156
        let eventId = null;
157
        const pendingPromise = new Pending('core_calendar/view_manager:eventLink:click');
158
 
159
        if (target.matches(CalendarSelectors.actions.viewEvent)) {
160
            eventLink = target;
161
        } else {
162
            eventLink = target.closest(CalendarSelectors.actions.viewEvent);
163
        }
164
 
165
        if (eventLink) {
166
            eventId = eventLink.dataset.eventId;
167
        } else {
168
            eventId = target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId;
169
        }
170
 
171
        if (eventId) {
172
            // A link was found. Show the modal.
173
 
174
            e.preventDefault();
175
            // We've handled the event so stop it from bubbling
176
            // and causing the day click handler to fire.
177
            e.stopPropagation();
178
 
179
            renderEventSummaryModal(eventId)
180
            .then(pendingPromise.resolve)
181
            .catch();
182
        } else {
183
            pendingPromise.resolve();
184
        }
185
    });
186
 
187
    root.on('click', CalendarSelectors.links.navLink, (e) => {
188
        const wrapper = root.find(CalendarSelectors.wrapper);
189
        const view = wrapper.data('view');
190
        const courseId = wrapper.data('courseid');
191
        const categoryId = wrapper.data('categoryid');
192
        const link = e.currentTarget;
193
 
194
        if (view === 'month' || view === 'monthblock') {
195
            changeMonth(root, link.href, link.dataset.year, link.dataset.month, courseId, categoryId, link.dataset.day);
196
            e.preventDefault();
197
        } else if (view === 'day') {
198
            changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day, courseId, categoryId);
199
            e.preventDefault();
200
        }
201
    });
202
 
203
    const viewSelector = root.find(CalendarSelectors.viewSelector);
204
    CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
205
    viewSelector.on(
206
        CustomEvents.events.activate,
207
        (e) => {
208
            e.preventDefault();
209
 
210
            const option = e.target;
211
            if (option.classList.contains('active')) {
212
                return;
213
            }
214
 
215
            const view = option.dataset.view,
216
                year = option.dataset.year,
217
                month = option.dataset.month,
218
                day = option.dataset.day,
219
                courseId = option.dataset.courseid,
220
                categoryId = option.dataset.categoryid;
221
 
222
            if (view == 'month') {
223
                refreshMonthContent(root, year, month, courseId, categoryId, root, 'core_calendar/calendar_month', day)
224
                    .then(() => {
225
                        updateUrl('?view=month&course=' + courseId);
226
                        return;
227
                    }).fail(Notification.exception);
228
            } else if (view == 'day') {
229
                refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day')
230
                    .then(() => {
231
                        updateUrl('?view=day&course=' + courseId);
232
                        return;
233
                    }).fail(Notification.exception);
234
            } else if (view == 'upcoming') {
235
                reloadCurrentUpcoming(root, courseId, categoryId, root, 'core_calendar/calendar_upcoming')
236
                    .then(() => {
237
                        updateUrl('?view=upcoming&course=' + courseId);
238
                        return;
239
                    }).fail(Notification.exception);
240
            }
241
        }
242
    );
243
};
244
 
245
/**
246
 * Refresh the month content.
247
 *
248
 * @param {object} root The root element.
249
 * @param {number} year Year
250
 * @param {number} month Month
251
 * @param {number} courseId The id of the course whose events are shown
252
 * @param {number} categoryId The id of the category whose events are shown
253
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
254
 * @param {string} template The template to be rendered.
255
 * @param {number} day Day (optional)
256
 * @return {promise}
257
 */
258
export const refreshMonthContent = (root, year, month, courseId, categoryId, target = null, template = '', day = 1) => {
259
    startLoading(root);
260
 
261
    target = target || root.find(CalendarSelectors.wrapper);
262
    template = template || root.attr('data-template');
263
    M.util.js_pending([root.get('id'), year, month, courseId].join('-'));
264
    const includenavigation = root.data('includenavigation');
265
    const mini = root.data('mini');
266
    const viewMode = target.data('view');
267
    return CalendarRepository.getCalendarMonthData(year, month, courseId, categoryId, includenavigation, mini, day, viewMode)
268
        .then(context => {
269
            return Templates.render(template, context);
270
        })
271
        .then((html, js) => {
272
            return Templates.replaceNode(target, html, js);
273
        })
274
        .then(() => {
275
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
276
            return;
277
        })
278
        .always(() => {
279
            M.util.js_complete([root.get('id'), year, month, courseId].join('-'));
280
            return stopLoading(root);
281
        })
282
        .fail(Notification.exception);
283
};
284
 
285
/**
286
 * Handle changes to the current calendar view.
287
 *
288
 * @param {object} root The container element
289
 * @param {string} url The calendar url to be shown
290
 * @param {number} year Year
291
 * @param {number} month Month
292
 * @param {number} courseId The id of the course whose events are shown
293
 * @param {number} categoryId The id of the category whose events are shown
294
 * @param {number} day Day (optional)
295
 * @return {promise}
296
 */
297
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1) => {
298
    return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day)
299
        .then((...args) => {
300
            if (url.length && url !== '#') {
301
                updateUrl(url);
302
            }
303
            return args;
304
        })
305
        .then((...args) => {
306
            $('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId]);
307
            return args;
308
        });
309
};
310
 
311
/**
312
 * Reload the current month view data.
313
 *
314
 * @param {object} root The container element.
315
 * @param {number} courseId The course id.
316
 * @param {number} categoryId The id of the category whose events are shown
317
 * @return {promise}
318
 */
319
export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => {
320
    const year = root.find(CalendarSelectors.wrapper).data('year');
321
    const month = root.find(CalendarSelectors.wrapper).data('month');
322
    const day = root.find(CalendarSelectors.wrapper).data('day');
323
 
324
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
325
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
326
 
327
    return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day).
328
        then((...args) => {
329
            $('body').trigger(CalendarEvents.courseChanged, [year, month, courseId, categoryId]);
330
            return args;
331
        });
332
};
333
 
334
 
335
/**
336
 * Refresh the day content.
337
 *
338
 * @param {object} root The root element.
339
 * @param {number} year Year
340
 * @param {number} month Month
341
 * @param {number} day Day
342
 * @param {number} courseId The id of the course whose events are shown
343
 * @param {number} categoryId The id of the category whose events are shown
344
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
345
 * @param {string} template The template to be rendered.
346
 *
347
 * @return {promise}
348
 */
349
export const refreshDayContent = (root, year, month, day, courseId, categoryId, target = null, template = '') => {
350
    startLoading(root);
351
 
352
    if (!target || target.length == 0){
353
        target = root.find(CalendarSelectors.wrapper);
354
    }
355
    template = template || root.attr('data-template');
356
    M.util.js_pending([root.get('id'), year, month, day, courseId, categoryId].join('-'));
357
    const includenavigation = root.data('includenavigation');
358
    return CalendarRepository.getCalendarDayData(year, month, day, courseId, categoryId, includenavigation)
359
        .then((context) => {
360
            context.viewingday = true;
361
            context.showviewselector = true;
362
            return Templates.render(template, context);
363
        })
364
        .then((html, js) => {
365
            return Templates.replaceNode(target, html, js);
366
        })
367
        .then(() => {
368
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
369
            return;
370
        })
371
        .always(() => {
372
            M.util.js_complete([root.get('id'), year, month, day, courseId, categoryId].join('-'));
373
            return stopLoading(root);
374
        })
375
        .fail(Notification.exception);
376
};
377
 
378
/**
379
 * Reload the current day view data.
380
 *
381
 * @param {object} root The container element.
382
 * @param {number} courseId The course id.
383
 * @param {number} categoryId The id of the category whose events are shown
384
 * @return {promise}
385
 */
386
export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => {
387
    const wrapper = root.find(CalendarSelectors.wrapper);
388
    const year = wrapper.data('year');
389
    const month = wrapper.data('month');
390
    const day = wrapper.data('day');
391
 
392
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
393
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
394
 
395
    return refreshDayContent(root, year, month, day, courseId, categoryId);
396
};
397
 
398
/**
399
 * Handle changes to the current calendar view.
400
 *
401
 * @param {object} root The root element.
402
 * @param {String} url The calendar url to be shown
403
 * @param {Number} year Year
404
 * @param {Number} month Month
405
 * @param {Number} day Day
406
 * @param {Number} courseId The id of the course whose events are shown
407
 * @param {Number} categoryId The id of the category whose events are shown
408
 * @return {promise}
409
 */
410
export const changeDay = (root, url, year, month, day, courseId, categoryId) => {
411
    return refreshDayContent(root, year, month, day, courseId, categoryId)
412
        .then((...args) => {
413
            if (url.length && url !== '#') {
414
                updateUrl(url);
415
            }
416
            return args;
417
        })
418
        .then((...args) => {
419
            $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId]);
420
            return args;
421
        });
422
};
423
 
424
/**
425
 * Update calendar URL.
426
 *
427
 * @param {String} url The calendar url to be updated.
428
 */
429
export const updateUrl = (url) => {
430
    const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);
431
 
432
    // We want to update the url only if the user is viewing the full calendar.
433
    if (viewingFullCalendar) {
434
        window.history.pushState({}, '', url);
435
    }
436
};
437
 
438
/**
439
 * Set the element state to loading.
440
 *
441
 * @param {object} root The container element
442
 * @method startLoading
443
 */
444
const startLoading = (root) => {
445
    const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
446
 
447
    loadingIconContainer.removeClass('hidden');
448
};
449
 
450
/**
451
 * Remove the loading state from the element.
452
 *
453
 * @param {object} root The container element
454
 * @method stopLoading
455
 */
456
const stopLoading = (root) => {
457
    const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
458
 
459
    loadingIconContainer.addClass('hidden');
460
};
461
 
462
/**
463
 * Reload the current month view data.
464
 *
465
 * @param {object} root The container element.
466
 * @param {number} courseId The course id.
467
 * @param {number} categoryId The id of the category whose events are shown
468
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
469
 * @param {string} template The template to be rendered.
470
 * @return {promise}
471
 */
472
export const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target = null, template = '') => {
473
    startLoading(root);
474
 
475
    target = target || root.find(CalendarSelectors.wrapper);
476
    template = template || root.attr('data-template');
477
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
478
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
479
 
480
    return CalendarRepository.getCalendarUpcomingData(courseId, categoryId)
481
        .then((context) => {
482
            context.viewingupcoming = true;
483
            context.showviewselector = true;
484
            return Templates.render(template, context);
485
        })
486
        .then((html, js) => {
487
            return Templates.replaceNode(target, html, js);
488
        })
489
        .then(() => {
490
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
491
            return;
492
        })
493
        .always(function() {
494
            return stopLoading(root);
495
        })
496
        .fail(Notification.exception);
497
};
498
 
499
/**
500
 * Get the CSS class to apply for the given event type.
501
 *
502
 * @param {string} eventType The calendar event type
503
 * @return {string}
504
 */
505
const getEventTypeClassFromType = (eventType) => {
506
    return 'calendar_event_' + eventType;
507
};
508
 
509
/**
510
 * Render the event summary modal.
511
 *
512
 * @param {Number} eventId The calendar event id.
513
 * @returns {Promise}
514
 */
515
const renderEventSummaryModal = (eventId) => {
516
    const pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal');
517
 
518
    // Calendar repository promise.
519
    return CalendarRepository.getEventById(eventId)
520
    .then((getEventResponse) => {
521
        if (!getEventResponse.event) {
522
            throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
523
        }
524
 
525
        return getEventResponse.event;
526
    })
527
    .then(eventData => {
528
        // Build the modal parameters from the event data.
529
        const modalParams = {
530
            title: eventData.name,
531
            body: Templates.render('core_calendar/event_summary_body', eventData),
532
            templateContext: {
533
                canedit: eventData.canedit,
534
                candelete: eventData.candelete,
535
                headerclasses: getEventTypeClassFromType(eventData.normalisedeventtype),
536
                isactionevent: eventData.isactionevent,
537
                url: eventData.url,
538
                action: eventData.action
539
            }
540
        };
541
 
542
        // Create the modal.
543
        return SummaryModal.create(modalParams);
544
    })
545
    .then(modal => {
546
        // Handle hidden event.
547
        modal.getRoot().on(ModalEvents.hidden, function() {
548
            // Destroy when hidden.
549
            modal.destroy();
550
        });
551
 
552
        // Finally, render the modal!
553
        modal.show();
554
 
555
        return modal;
556
    })
557
    .then(modal => {
558
        pendingPromise.resolve();
559
 
560
        return modal;
561
    })
562
    .catch(Notification.exception);
563
};
564
 
565
export const init = (root, view) => {
566
    prefetchStrings('calendar', ['moreevents']);
567
    foldDayEvents();
568
    registerEventListeners(root, view);
569
    const calendarTable = root.find(CalendarSelectors.elements.monthDetailed);
570
    if (calendarTable.length) {
571
        const pendingId = `month-detailed-${calendarTable.id}-filterChanged`;
572
        registerEventListenersForMonthDetailed(calendarTable, pendingId);
573
    }
574
};
575
 
576
/**
577
 * Handles the change of course and updates the relevant elements on the page.
578
 *
579
 * @param {integer} courseId - The ID of the new course.
580
 * @param {string} courseName - The name of the new course.
581
 * @returns {Promise<void>} - A promise that resolves after the updates are applied.
582
 */
583
export const handleCourseChange = async(courseId, courseName) => {
584
    // Select the <ul> element inside the data-region="view-selector".
585
    const ulElement = document.querySelector(CalendarSelectors.viewSelector + ' ul');
586
    // Select all <li><a> elements within the <ul>.
587
    const liElements = ulElement.querySelectorAll('li a');
588
    // Loop through the selected elements and update the courseid.
589
    liElements.forEach(element => {
590
        element.setAttribute('data-courseid', courseId);
591
    });
592
 
593
    const calendar = await getString('calendar', 'calendar');
594
    const pageHeaderHeadingsElement = document.querySelector(CalendarSelectors.pageHeaderHeadings);
595
    const courseUrl = Url.relativeUrl('/course/view.php', {id: courseId});
596
    // Apply the page header text.
597
    if (courseId !== Config.siteId) {
598
        pageHeaderHeadingsElement.innerHTML = calendar + ': <a href="' + courseUrl + '">' + courseName + '</a>';
599
    } else {
600
        pageHeaderHeadingsElement.innerHTML = calendar;
601
    }
602
};