Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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.
11 efrain 148
 * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
1 efrain 149
 */
11 efrain 150
const registerEventListeners = (root, isCalendarBlock) => {
1 efrain 151
    root = $(root);
152
 
153
    // Bind click events to event links.
154
    root.on('click', CalendarSelectors.links.eventLink, (e) => {
155
        const target = e.target;
156
        let eventLink = null;
157
        let eventId = null;
158
        const pendingPromise = new Pending('core_calendar/view_manager:eventLink:click');
159
 
160
        if (target.matches(CalendarSelectors.actions.viewEvent)) {
161
            eventLink = target;
162
        } else {
163
            eventLink = target.closest(CalendarSelectors.actions.viewEvent);
164
        }
165
 
166
        if (eventLink) {
167
            eventId = eventLink.dataset.eventId;
168
        } else {
169
            eventId = target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId;
170
        }
171
 
172
        if (eventId) {
173
            // A link was found. Show the modal.
174
 
175
            e.preventDefault();
176
            // We've handled the event so stop it from bubbling
177
            // and causing the day click handler to fire.
178
            e.stopPropagation();
179
 
180
            renderEventSummaryModal(eventId)
181
            .then(pendingPromise.resolve)
182
            .catch();
183
        } else {
184
            pendingPromise.resolve();
185
        }
186
    });
187
 
188
    root.on('click', CalendarSelectors.links.navLink, (e) => {
189
        const wrapper = root.find(CalendarSelectors.wrapper);
190
        const view = wrapper.data('view');
191
        const courseId = wrapper.data('courseid');
192
        const categoryId = wrapper.data('categoryid');
193
        const link = e.currentTarget;
194
 
195
        if (view === 'month' || view === 'monthblock') {
11 efrain 196
            changeMonth(root, link.href, link.dataset.year, link.dataset.month,
197
                courseId, categoryId, link.dataset.day, isCalendarBlock);
1 efrain 198
            e.preventDefault();
199
        } else if (view === 'day') {
11 efrain 200
            changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day,
201
                courseId, categoryId, isCalendarBlock);
1 efrain 202
            e.preventDefault();
203
        }
204
    });
205
 
206
    const viewSelector = root.find(CalendarSelectors.viewSelector);
207
    CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
208
    viewSelector.on(
209
        CustomEvents.events.activate,
210
        (e) => {
211
            e.preventDefault();
212
 
213
            const option = e.target;
214
            if (option.classList.contains('active')) {
215
                return;
216
            }
217
 
218
            const view = option.dataset.view,
219
                year = option.dataset.year,
220
                month = option.dataset.month,
221
                day = option.dataset.day,
222
                courseId = option.dataset.courseid,
223
                categoryId = option.dataset.categoryid;
224
 
225
            if (view == 'month') {
226
                refreshMonthContent(root, year, month, courseId, categoryId, root, 'core_calendar/calendar_month', day)
227
                    .then(() => {
228
                        updateUrl('?view=month&course=' + courseId);
229
                        return;
230
                    }).fail(Notification.exception);
231
            } else if (view == 'day') {
232
                refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day')
233
                    .then(() => {
234
                        updateUrl('?view=day&course=' + courseId);
235
                        return;
236
                    }).fail(Notification.exception);
237
            } else if (view == 'upcoming') {
238
                reloadCurrentUpcoming(root, courseId, categoryId, root, 'core_calendar/calendar_upcoming')
239
                    .then(() => {
240
                        updateUrl('?view=upcoming&course=' + courseId);
241
                        return;
242
                    }).fail(Notification.exception);
243
            }
244
        }
245
    );
246
};
247
 
248
/**
249
 * Refresh the month content.
250
 *
251
 * @param {object} root The root element.
252
 * @param {number} year Year
253
 * @param {number} month Month
254
 * @param {number} courseId The id of the course whose events are shown
255
 * @param {number} categoryId The id of the category whose events are shown
256
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
257
 * @param {string} template The template to be rendered.
258
 * @param {number} day Day (optional)
259
 * @return {promise}
260
 */
261
export const refreshMonthContent = (root, year, month, courseId, categoryId, target = null, template = '', day = 1) => {
262
    startLoading(root);
263
 
264
    target = target || root.find(CalendarSelectors.wrapper);
265
    template = template || root.attr('data-template');
266
    M.util.js_pending([root.get('id'), year, month, courseId].join('-'));
267
    const includenavigation = root.data('includenavigation');
268
    const mini = root.data('mini');
269
    const viewMode = target.data('view');
270
    return CalendarRepository.getCalendarMonthData(year, month, courseId, categoryId, includenavigation, mini, day, viewMode)
271
        .then(context => {
272
            return Templates.render(template, context);
273
        })
274
        .then((html, js) => {
275
            return Templates.replaceNode(target, html, js);
276
        })
277
        .then(() => {
278
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
279
            return;
280
        })
281
        .always(() => {
282
            M.util.js_complete([root.get('id'), year, month, courseId].join('-'));
283
            return stopLoading(root);
284
        })
285
        .fail(Notification.exception);
286
};
287
 
288
/**
289
 * Handle changes to the current calendar view.
290
 *
291
 * @param {object} root The container element
292
 * @param {string} url The calendar url to be shown
293
 * @param {number} year Year
294
 * @param {number} month Month
295
 * @param {number} courseId The id of the course whose events are shown
296
 * @param {number} categoryId The id of the category whose events are shown
297
 * @param {number} day Day (optional)
11 efrain 298
 * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
1 efrain 299
 * @return {promise}
300
 */
11 efrain 301
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => {
1 efrain 302
    return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day)
303
        .then((...args) => {
11 efrain 304
            if (url.length && url !== '#' && !isCalendarBlock) {
1 efrain 305
                updateUrl(url);
306
            }
307
            return args;
308
        })
309
        .then((...args) => {
11 efrain 310
            $('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]);
1 efrain 311
            return args;
312
        });
313
};
314
 
315
/**
316
 * Reload the current month view data.
317
 *
318
 * @param {object} root The container element.
319
 * @param {number} courseId The course id.
320
 * @param {number} categoryId The id of the category whose events are shown
321
 * @return {promise}
322
 */
323
export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => {
324
    const year = root.find(CalendarSelectors.wrapper).data('year');
325
    const month = root.find(CalendarSelectors.wrapper).data('month');
326
    const day = root.find(CalendarSelectors.wrapper).data('day');
327
 
328
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
329
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
330
 
331
    return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day).
332
        then((...args) => {
333
            $('body').trigger(CalendarEvents.courseChanged, [year, month, courseId, categoryId]);
334
            return args;
335
        });
336
};
337
 
338
 
339
/**
340
 * Refresh the day content.
341
 *
342
 * @param {object} root The root element.
343
 * @param {number} year Year
344
 * @param {number} month Month
345
 * @param {number} day Day
346
 * @param {number} courseId The id of the course whose events are shown
347
 * @param {number} categoryId The id of the category whose events are shown
348
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
349
 * @param {string} template The template to be rendered.
11 efrain 350
 * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
1 efrain 351
 *
352
 * @return {promise}
353
 */
11 efrain 354
export const refreshDayContent = (root, year, month, day, courseId, categoryId,
355
    target = null, template = '', isCalendarBlock = false) => {
1 efrain 356
    startLoading(root);
357
 
358
    if (!target || target.length == 0){
359
        target = root.find(CalendarSelectors.wrapper);
360
    }
361
    template = template || root.attr('data-template');
362
    M.util.js_pending([root.get('id'), year, month, day, courseId, categoryId].join('-'));
363
    const includenavigation = root.data('includenavigation');
364
    return CalendarRepository.getCalendarDayData(year, month, day, courseId, categoryId, includenavigation)
365
        .then((context) => {
366
            context.viewingday = true;
367
            context.showviewselector = true;
11 efrain 368
            context.iscalendarblock = isCalendarBlock;
1 efrain 369
            return Templates.render(template, context);
370
        })
371
        .then((html, js) => {
372
            return Templates.replaceNode(target, html, js);
373
        })
374
        .then(() => {
375
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
376
            return;
377
        })
378
        .always(() => {
379
            M.util.js_complete([root.get('id'), year, month, day, courseId, categoryId].join('-'));
380
            return stopLoading(root);
381
        })
382
        .fail(Notification.exception);
383
};
384
 
385
/**
386
 * Reload the current day view data.
387
 *
388
 * @param {object} root The container element.
389
 * @param {number} courseId The course id.
390
 * @param {number} categoryId The id of the category whose events are shown
391
 * @return {promise}
392
 */
393
export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => {
394
    const wrapper = root.find(CalendarSelectors.wrapper);
395
    const year = wrapper.data('year');
396
    const month = wrapper.data('month');
397
    const day = wrapper.data('day');
398
 
399
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
400
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
401
 
402
    return refreshDayContent(root, year, month, day, courseId, categoryId);
403
};
404
 
405
/**
406
 * Handle changes to the current calendar view.
407
 *
408
 * @param {object} root The root element.
409
 * @param {String} url The calendar url to be shown
410
 * @param {Number} year Year
411
 * @param {Number} month Month
412
 * @param {Number} day Day
413
 * @param {Number} courseId The id of the course whose events are shown
414
 * @param {Number} categoryId The id of the category whose events are shown
11 efrain 415
 * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
1 efrain 416
 * @return {promise}
417
 */
11 efrain 418
export const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => {
419
    return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock)
1 efrain 420
        .then((...args) => {
11 efrain 421
            if (url.length && url !== '#' && !isCalendarBlock) {
1 efrain 422
                updateUrl(url);
423
            }
424
            return args;
425
        })
426
        .then((...args) => {
11 efrain 427
            $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]);
1 efrain 428
            return args;
429
        });
430
};
431
 
432
/**
433
 * Update calendar URL.
434
 *
435
 * @param {String} url The calendar url to be updated.
436
 */
437
export const updateUrl = (url) => {
438
    const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);
439
 
440
    // We want to update the url only if the user is viewing the full calendar.
441
    if (viewingFullCalendar) {
442
        window.history.pushState({}, '', url);
443
    }
444
};
445
 
446
/**
447
 * Set the element state to loading.
448
 *
449
 * @param {object} root The container element
450
 * @method startLoading
451
 */
452
const startLoading = (root) => {
453
    const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
454
 
455
    loadingIconContainer.removeClass('hidden');
456
};
457
 
458
/**
459
 * Remove the loading state from the element.
460
 *
461
 * @param {object} root The container element
462
 * @method stopLoading
463
 */
464
const stopLoading = (root) => {
465
    const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
466
 
467
    loadingIconContainer.addClass('hidden');
468
};
469
 
470
/**
471
 * Reload the current month view data.
472
 *
473
 * @param {object} root The container element.
474
 * @param {number} courseId The course id.
475
 * @param {number} categoryId The id of the category whose events are shown
476
 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
477
 * @param {string} template The template to be rendered.
478
 * @return {promise}
479
 */
480
export const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target = null, template = '') => {
481
    startLoading(root);
482
 
483
    target = target || root.find(CalendarSelectors.wrapper);
484
    template = template || root.attr('data-template');
485
    courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');
486
    categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');
487
 
488
    return CalendarRepository.getCalendarUpcomingData(courseId, categoryId)
489
        .then((context) => {
490
            context.viewingupcoming = true;
491
            context.showviewselector = true;
492
            return Templates.render(template, context);
493
        })
494
        .then((html, js) => {
495
            return Templates.replaceNode(target, html, js);
496
        })
497
        .then(() => {
498
            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
499
            return;
500
        })
501
        .always(function() {
502
            return stopLoading(root);
503
        })
504
        .fail(Notification.exception);
505
};
506
 
507
/**
508
 * Get the CSS class to apply for the given event type.
509
 *
510
 * @param {string} eventType The calendar event type
511
 * @return {string}
512
 */
513
const getEventTypeClassFromType = (eventType) => {
514
    return 'calendar_event_' + eventType;
515
};
516
 
517
/**
518
 * Render the event summary modal.
519
 *
520
 * @param {Number} eventId The calendar event id.
521
 * @returns {Promise}
522
 */
523
const renderEventSummaryModal = (eventId) => {
524
    const pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal');
525
 
526
    // Calendar repository promise.
527
    return CalendarRepository.getEventById(eventId)
528
    .then((getEventResponse) => {
529
        if (!getEventResponse.event) {
530
            throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
531
        }
532
 
533
        return getEventResponse.event;
534
    })
535
    .then(eventData => {
536
        // Build the modal parameters from the event data.
537
        const modalParams = {
538
            title: eventData.name,
539
            body: Templates.render('core_calendar/event_summary_body', eventData),
540
            templateContext: {
541
                canedit: eventData.canedit,
542
                candelete: eventData.candelete,
543
                headerclasses: getEventTypeClassFromType(eventData.normalisedeventtype),
544
                isactionevent: eventData.isactionevent,
545
                url: eventData.url,
546
                action: eventData.action
547
            }
548
        };
549
 
550
        // Create the modal.
551
        return SummaryModal.create(modalParams);
552
    })
553
    .then(modal => {
554
        // Handle hidden event.
555
        modal.getRoot().on(ModalEvents.hidden, function() {
556
            // Destroy when hidden.
557
            modal.destroy();
558
        });
559
 
560
        // Finally, render the modal!
561
        modal.show();
562
 
563
        return modal;
564
    })
565
    .then(modal => {
566
        pendingPromise.resolve();
567
 
568
        return modal;
569
    })
570
    .catch(Notification.exception);
571
};
572
 
11 efrain 573
/**
574
 * Initializes the calendar component by prefetching strings, folding day events,
575
 * and registering event listeners.
576
 *
577
 * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
578
 * @param {string} view - A flag indicating whether this is a calendar block.
579
 * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
580
 */
581
export const init = (root, view, isCalendarBlock) => {
1 efrain 582
    prefetchStrings('calendar', ['moreevents']);
583
    foldDayEvents();
11 efrain 584
    registerEventListeners(root, isCalendarBlock);
1 efrain 585
    const calendarTable = root.find(CalendarSelectors.elements.monthDetailed);
586
    if (calendarTable.length) {
587
        const pendingId = `month-detailed-${calendarTable.id}-filterChanged`;
588
        registerEventListenersForMonthDetailed(calendarTable, pendingId);
589
    }
590
};
591
 
592
/**
593
 * Handles the change of course and updates the relevant elements on the page.
594
 *
595
 * @param {integer} courseId - The ID of the new course.
596
 * @param {string} courseName - The name of the new course.
597
 * @returns {Promise<void>} - A promise that resolves after the updates are applied.
598
 */
599
export const handleCourseChange = async(courseId, courseName) => {
600
    // Select the <ul> element inside the data-region="view-selector".
601
    const ulElement = document.querySelector(CalendarSelectors.viewSelector + ' ul');
602
    // Select all <li><a> elements within the <ul>.
603
    const liElements = ulElement.querySelectorAll('li a');
604
    // Loop through the selected elements and update the courseid.
605
    liElements.forEach(element => {
606
        element.setAttribute('data-courseid', courseId);
607
    });
608
 
609
    const calendar = await getString('calendar', 'calendar');
610
    const pageHeaderHeadingsElement = document.querySelector(CalendarSelectors.pageHeaderHeadings);
611
    const courseUrl = Url.relativeUrl('/course/view.php', {id: courseId});
612
    // Apply the page header text.
613
    if (courseId !== Config.siteId) {
614
        pageHeaderHeadingsElement.innerHTML = calendar + ': <a href="' + courseUrl + '">' + courseName + '</a>';
615
    } else {
616
        pageHeaderHeadingsElement.innerHTML = calendar;
617
    }
618
};