Proyectos de Subversion Moodle

Rev

| 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
 * JS for the recordings page on mod_bigbluebuttonbn plugin.
18
 *
19
 * @module      mod_bigbluebuttonbn/recordings
20
 * @copyright   2021 Blindside Networks Inc
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import * as repository from './repository';
25
import {exception as displayException, saveCancelPromise} from 'core/notification';
26
import {prefetchStrings} from 'core/prefetch';
27
import {getString, getStrings} from 'core/str';
28
import {addIconToContainerWithPromise} from 'core/loadingicon';
29
import Pending from 'core/pending';
30
 
31
const stringsWithKeys = {
32
    first: 'view_recording_yui_first',
33
    prev: 'view_recording_yui_prev',
34
    next: 'view_recording_yui_next',
35
    last: 'view_recording_yui_last',
36
    goToLabel: 'view_recording_yui_page',
37
    goToAction: 'view_recording_yui_go',
38
    perPage: 'view_recording_yui_rows',
39
    showAll: 'view_recording_yui_show_all',
40
};
41
// Load global strings.
42
prefetchStrings('bigbluebuttonbn', Object.entries(stringsWithKeys).map((entry) => entry[1]));
43
 
44
const getStringsForYui = () => {
45
    const stringMap = Object.keys(stringsWithKeys).map(key => {
46
        return {
47
            key: stringsWithKeys[key],
48
            component: 'mod_bigbluebuttonbn',
49
        };
50
    });
51
 
52
    // Return an object with the matching string keys (we want an object with {<stringkey>: <stringvalue>...}).
53
    return getStrings(stringMap)
54
        .then((stringArray) => Object.assign(
55
            {},
56
            ...Object.keys(stringsWithKeys).map(
57
                (key, index) => ({[key]: stringArray[index]})
58
            )
59
        ));
60
};
61
 
62
const getYuiInstance = lang => new Promise(resolve => {
63
    // eslint-disable-next-line
64
    YUI({
65
        lang,
66
    }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => {
67
        resolve(Y);
68
    });
69
});
70
 
71
/**
72
 * Format the supplied date per the specified locale.
73
 *
74
 * @param   {string} locale
75
 * @param   {number} date
76
 * @returns {array}
77
 */
78
const formatDate = (locale, date) => {
79
    const realDate = new Date(date);
80
    return realDate.toLocaleDateString(locale, {
81
        weekday: 'long',
82
        year: 'numeric',
83
        month: 'long',
84
        day: 'numeric',
85
    });
86
};
87
 
88
/**
89
 * Format response data for the table.
90
 *
91
 * @param   {string} response JSON-encoded table data
92
 * @returns {array}
93
 */
94
const getFormattedData = response => {
95
    const recordingData = response.tabledata;
96
    return JSON.parse(recordingData.data);
97
};
98
 
99
const getTableNode = tableSelector => document.querySelector(tableSelector);
100
 
101
const fetchRecordingData = tableSelector => {
102
    const tableNode = getTableNode(tableSelector);
103
    if (tableNode === null) {
104
        return Promise.resolve(false);
105
    }
106
 
107
    if (tableNode.dataset.importMode) {
108
        return repository.fetchRecordingsToImport(
109
            tableNode.dataset.bbbid,
110
            tableNode.dataset.bbbSourceInstanceId,
111
            tableNode.dataset.bbbSourceCourseId,
112
            tableNode.dataset.tools,
113
            tableNode.dataset.groupId
114
        );
115
    } else {
116
        return repository.fetchRecordings(
117
            tableNode.dataset.bbbid,
118
            tableNode.dataset.tools,
119
            tableNode.dataset.groupId
120
        );
121
    }
122
};
123
 
124
/**
125
 * Fetch the data table functinos for the specified table.
126
 *
127
 * @param {String} tableId in which we will display the table
128
 * @param {String} searchFormId The Id of the relate.
129
 * @param {Object} dataTable
130
 * @returns {Object}
131
 * @private
132
 */
133
const getDataTableFunctions = (tableId, searchFormId, dataTable) => {
134
    const tableNode = getTableNode(tableId);
135
    const bbbid = tableNode.dataset.bbbid;
136
 
137
    const updateTableFromResponse = response => {
138
        if (!response || !response.status) {
139
            // There was no output at all.
140
            return;
141
        }
142
 
143
        dataTable.get('data').reset(getFormattedData(response));
144
        dataTable.set(
145
            'currentData',
146
            dataTable.get('data')
147
        );
148
 
149
        const currentFilter = dataTable.get('currentFilter');
150
        if (currentFilter) {
151
            filterByText(currentFilter);
152
        }
153
    };
154
 
155
    const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse);
156
 
157
    const filterByText = value => {
158
        const dataModel = dataTable.get('currentData');
159
        dataTable.set('currentFilter', value);
160
 
161
        const escapedRegex = value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
162
        const rsearch = new RegExp(`<span>.*?${escapedRegex}.*?</span>`, 'i');
163
 
164
        dataTable.set('data', dataModel.filter({asList: true}, item => {
165
            const name = item.get('recording');
166
            if (name && rsearch.test(name)) {
167
                return true;
168
            }
169
 
170
            const description = item.get('description');
171
            return description && rsearch.test(description);
172
        }));
173
    };
174
 
175
    const requestAction = async(element) => {
176
        const getDataFromAction = (element, dataType) => {
177
            const dataElement = element.closest(`[data-${dataType}]`);
178
            if (dataElement) {
179
                return dataElement.dataset[dataType];
180
            }
181
 
182
            return null;
183
        };
184
 
185
        const elementData = element.dataset;
186
        const payload = {
187
            bigbluebuttonbnid: bbbid,
188
            recordingid: getDataFromAction(element, 'recordingid'),
189
            additionaloptions: getDataFromAction(element, 'additionaloptions'),
190
            action: elementData.action,
191
        };
192
        // Slight change for import, for additional options.
193
        if (!payload.additionaloptions) {
194
            payload.additionaloptions = {};
195
        }
196
        if (elementData.action === 'import') {
197
            const bbbsourceid = getDataFromAction(element, 'source-instance-id');
198
            const bbbcourseid = getDataFromAction(element, 'source-course-id');
199
            if (!payload.additionaloptions) {
200
                payload.additionaloptions = {};
201
            }
202
            payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0;
203
            payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0;
204
        }
205
        // Now additional options should be a json string.
206
        payload.additionaloptions = JSON.stringify(payload.additionaloptions);
207
        if (element.dataset.requireConfirmation === "1") {
208
            // Create the confirmation dialogue.
209
            try {
210
                await saveCancelPromise(
211
                    getString('confirm'),
212
                    recordingConfirmationMessage(payload),
213
                    getString('ok', 'moodle'),
214
                );
215
            } catch {
216
                // User cancelled the dialogue.
217
                return;
218
            }
219
        }
220
 
221
        return repository.updateRecording(payload);
222
    };
223
 
224
    const recordingConfirmationMessage = async(data) => {
225
 
226
        const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`);
227
        const recordingType = await getString(
228
            playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording',
229
            'bigbluebuttonbn'
230
        );
231
 
232
        const confirmation = await getString(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType);
233
 
234
        if (data.action === 'import') {
235
            return confirmation;
236
        }
237
 
238
        // If it has associated links imported in a different course/activity, show that in confirmation dialog.
239
        const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links;
240
        if (!associatedLinkCount || associatedLinkCount === 0) {
241
            return confirmation;
242
        }
243
 
244
        const confirmationWarning = await getString(
245
            associatedLinkCount === 1
246
                ? `view_recording_${data.action}_confirmation_warning_p`
247
                : `view_recording_${data.action}_confirmation_warning_s`,
248
            'bigbluebuttonbn',
249
            associatedLinkCount
250
        );
251
 
252
        return confirmationWarning + '\n\n' + confirmation;
253
    };
254
 
255
    /**
256
     * Process an action event.
257
     *
258
     * @param   {Event} e
259
     */
260
    const processAction = e => {
261
        const popoutLink = e.target.closest('[data-action="play"]');
262
        if (popoutLink) {
263
            e.preventDefault();
264
 
265
            const videoPlayer = window.open('', '_blank');
266
            videoPlayer.opener = null;
267
            videoPlayer.location.href = popoutLink.href;
268
            // TODO send a recording viewed event when this event will be implemented.
269
            return;
270
        }
271
 
272
        // Fetch any clicked anchor.
273
        const clickedLink = e.target.closest('a[data-action]');
274
        if (clickedLink && !clickedLink.classList.contains('disabled')) {
275
            e.preventDefault();
276
 
277
            // Create a spinning icon on the table.
278
            const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode());
279
 
280
            requestAction(clickedLink)
281
                .then(refreshTableData)
282
                .then(iconPromise.resolve)
283
                .catch(displayException);
284
        }
285
    };
286
 
287
    const processSearchSubmission = e => {
288
        // Prevent the default action.
289
        e.preventDefault();
290
        const parentNode = e.target.closest('div[role=search]');
291
        const searchInput = parentNode.querySelector('input[name=search]');
292
        filterByText(searchInput.value);
293
    };
294
 
295
    const registerEventListeners = () => {
296
        // Add event listeners to the table boundingBox.
297
        const boundingBox = dataTable.get('boundingBox').getDOMNode();
298
        boundingBox.addEventListener('click', processAction);
299
 
300
        // Setup the search from handlers.
301
        const searchForm = document.querySelector(searchFormId);
302
        if (searchForm) {
303
            const searchButton = document.querySelector(searchFormId + ' button');
304
            searchButton.addEventListener('click', processSearchSubmission);
305
        }
306
    };
307
 
308
    return {
309
        filterByText,
310
        refreshTableData,
311
        registerEventListeners,
312
    };
313
};
314
 
315
/**
316
 * Setup the data table for the specified BBB instance.
317
 *
318
 * @param {String} tableId in which we will display the table
319
 * @param {String} searchFormId The Id of the relate.
320
 * @param   {object} response The response from the data request
321
 * @returns {Promise}
322
 */
323
const setupDatatable = (tableId, searchFormId, response) => {
324
    if (!response) {
325
        return Promise.resolve();
326
    }
327
 
328
    if (!response.status) {
329
        // Something failed. Continue to show the plain output.
330
        return Promise.resolve();
331
    }
332
 
333
    const recordingData = response.tabledata;
334
 
335
    const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable');
336
    return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()])
337
        .then(([yuiInstance, strings]) => {
338
            // Here we use a custom formatter for date.
339
            // See https://clarle.github.io/yui3/yui/docs/api/classes/DataTable.BodyView.Formatters.html
340
            // Inspired from examples here: https://clarle.github.io/yui3/yui/docs/datatable/
341
            // Normally formatter have the prototype: (col) => (cell) => <computed value>, see:
342
            // https://clarle.github.io/yui3/yui/docs/api/files/datatable_js_formatters.js.html#l100 .
343
            const dateCustomFormatter = () => (cell) => formatDate(recordingData.locale, cell.value);
344
            // Add the fetched strings to the YUI Instance.
345
            yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings});
346
            yuiInstance.DataTable.BodyView.Formatters.customDate = dateCustomFormatter;
347
            return yuiInstance;
348
        })
349
        .then(yuiInstance => {
350
 
351
            const tableData = getFormattedData(response);
352
            yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view', yuiInstance.DataTable.Paginator.View, [], {
353
                _modelChange: function(e) {
354
                    var changed = e.changed,
355
                        totalItems = (changed && changed.totalItems);
356
                    if (totalItems) {
357
                        this._updateControlsUI(e.target.get('page'));
358
                    }
359
                }
360
            });
361
            return new yuiInstance.DataTable({
362
                paginatorView: "RecordsPaginatorView",
363
                width: "1195px",
364
                columns: recordingData.columns,
365
                data: tableData,
366
                rowsPerPage: 10,
367
                paginatorLocation: ['header', 'footer'],
368
                autoSync: true
369
            });
370
        })
371
        .then(dataTable => {
372
            dataTable.render(tableId);
373
            const {registerEventListeners} = getDataTableFunctions(
374
                tableId,
375
                searchFormId,
376
                dataTable);
377
            registerEventListeners();
378
            return dataTable;
379
        })
380
        .then(dataTable => {
381
            pendingPromise.resolve();
382
            return dataTable;
383
        });
384
};
385
 
386
/**
387
 * Initialise recordings code.
388
 *
389
 * @method init
390
 * @param {String} tableId in which we will display the table
391
 * @param {String} searchFormId The Id of the relate.
392
 */
393
export const init = (tableId, searchFormId) => {
394
    const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings:init');
395
 
396
    fetchRecordingData(tableId)
397
        .then(response => setupDatatable(tableId, searchFormId, response))
398
        .then(() => pendingPromise.resolve())
399
        .catch(displayException);
400
};