Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"recordings.min.js","sources":["../src/recordings.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * JS for the recordings page on mod_bigbluebuttonbn plugin.\n *\n * @module      mod_bigbluebuttonbn/recordings\n * @copyright   2021 Blindside Networks Inc\n * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as repository from './repository';\nimport {exception as displayException, saveCancelPromise} from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString, getStrings} from 'core/str';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport Pending from 'core/pending';\n\nconst stringsWithKeys = {\n    first: 'view_recording_yui_first',\n    prev: 'view_recording_yui_prev',\n    next: 'view_recording_yui_next',\n    last: 'view_recording_yui_last',\n    goToLabel: 'view_recording_yui_page',\n    goToAction: 'view_recording_yui_go',\n    perPage: 'view_recording_yui_rows',\n    showAll: 'view_recording_yui_show_all',\n};\n// Load global strings.\nprefetchStrings('bigbluebuttonbn', Object.entries(stringsWithKeys).map((entry) => entry[1]));\n\nconst getStringsForYui = () => {\n    const stringMap = Object.keys(stringsWithKeys).map(key => {\n        return {\n            key: stringsWithKeys[key],\n            component: 'mod_bigbluebuttonbn',\n        };\n    });\n\n    // Return an object with the matching string keys (we want an object with {<stringkey>: <stringvalue>...}).\n    return getStrings(stringMap)\n        .then((stringArray) => Object.assign(\n            {},\n            ...Object.keys(stringsWithKeys).map(\n                (key, index) => ({[key]: stringArray[index]})\n            )\n        ));\n};\n\nconst getYuiInstance = lang => new Promise(resolve => {\n    // eslint-disable-next-line\n    YUI({\n        lang,\n    }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => {\n        resolve(Y);\n    });\n});\n\n/**\n * Format the supplied date per the specified locale.\n *\n * @param   {string} locale\n * @param   {number} date\n * @returns {array}\n */\nconst formatDate = (locale, date) => {\n    const realDate = new Date(date);\n    return realDate.toLocaleDateString(locale, {\n        weekday: 'long',\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n    });\n};\n\n/**\n * Format response data for the table.\n *\n * @param   {string} response JSON-encoded table data\n * @returns {array}\n */\nconst getFormattedData = response => {\n    const recordingData = response.tabledata;\n    return JSON.parse(recordingData.data);\n};\n\nconst getTableNode = tableSelector => document.querySelector(tableSelector);\n\nconst fetchRecordingData = tableSelector => {\n    const tableNode = getTableNode(tableSelector);\n    if (tableNode === null) {\n        return Promise.resolve(false);\n    }\n\n    if (tableNode.dataset.importMode) {\n        return repository.fetchRecordingsToImport(\n            tableNode.dataset.bbbid,\n            tableNode.dataset.bbbSourceInstanceId,\n            tableNode.dataset.bbbSourceCourseId,\n            tableNode.dataset.tools,\n            tableNode.dataset.groupId\n        );\n    } else {\n        return repository.fetchRecordings(\n            tableNode.dataset.bbbid,\n            tableNode.dataset.tools,\n            tableNode.dataset.groupId\n        );\n    }\n};\n\n/**\n * Fetch the data table functinos for the specified table.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param {Object} dataTable\n * @returns {Object}\n * @private\n */\nconst getDataTableFunctions = (tableId, searchFormId, dataTable) => {\n    const tableNode = getTableNode(tableId);\n    const bbbid = tableNode.dataset.bbbid;\n\n    const updateTableFromResponse = response => {\n        if (!response || !response.status) {\n            // There was no output at all.\n            return;\n        }\n\n        dataTable.get('data').reset(getFormattedData(response));\n        dataTable.set(\n            'currentData',\n            dataTable.get('data')\n        );\n\n        const currentFilter = dataTable.get('currentFilter');\n        if (currentFilter) {\n            filterByText(currentFilter);\n        }\n    };\n\n    const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse);\n\n    const filterByText = value => {\n        const dataModel = dataTable.get('currentData');\n        dataTable.set('currentFilter', value);\n\n        const escapedRegex = value.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, \"\\\\$&\");\n        const rsearch = new RegExp(`<span>.*?${escapedRegex}.*?</span>`, 'i');\n\n        dataTable.set('data', dataModel.filter({asList: true}, item => {\n            const name = item.get('recording');\n            if (name && rsearch.test(name)) {\n                return true;\n            }\n\n            const description = item.get('description');\n            return description && rsearch.test(description);\n        }));\n    };\n\n    const requestAction = async(element) => {\n        const getDataFromAction = (element, dataType) => {\n            const dataElement = element.closest(`[data-${dataType}]`);\n            if (dataElement) {\n                return dataElement.dataset[dataType];\n            }\n\n            return null;\n        };\n\n        const elementData = element.dataset;\n        const payload = {\n            bigbluebuttonbnid: bbbid,\n            recordingid: getDataFromAction(element, 'recordingid'),\n            additionaloptions: getDataFromAction(element, 'additionaloptions'),\n            action: elementData.action,\n        };\n        // Slight change for import, for additional options.\n        if (!payload.additionaloptions) {\n            payload.additionaloptions = {};\n        }\n        if (elementData.action === 'import') {\n            const bbbsourceid = getDataFromAction(element, 'source-instance-id');\n            const bbbcourseid = getDataFromAction(element, 'source-course-id');\n            if (!payload.additionaloptions) {\n                payload.additionaloptions = {};\n            }\n            payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0;\n            payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0;\n        }\n        // Now additional options should be a json string.\n        payload.additionaloptions = JSON.stringify(payload.additionaloptions);\n        if (element.dataset.requireConfirmation === \"1\") {\n            // Create the confirmation dialogue.\n            try {\n                await saveCancelPromise(\n                    getString('confirm'),\n                    recordingConfirmationMessage(payload),\n                    getString('ok', 'moodle'),\n                );\n            } catch {\n                // User cancelled the dialogue.\n                return;\n            }\n        }\n\n        return repository.updateRecording(payload);\n    };\n\n    const recordingConfirmationMessage = async(data) => {\n\n        const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`);\n        const recordingType = await getString(\n            playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording',\n            'bigbluebuttonbn'\n        );\n\n        const confirmation = await getString(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType);\n\n        if (data.action === 'import') {\n            return confirmation;\n        }\n\n        // If it has associated links imported in a different course/activity, show that in confirmation dialog.\n        const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links;\n        if (!associatedLinkCount || associatedLinkCount === 0) {\n            return confirmation;\n        }\n\n        const confirmationWarning = await getString(\n            associatedLinkCount === 1\n                ? `view_recording_${data.action}_confirmation_warning_p`\n                : `view_recording_${data.action}_confirmation_warning_s`,\n            'bigbluebuttonbn',\n            associatedLinkCount\n        );\n\n        return confirmationWarning + '\\n\\n' + confirmation;\n    };\n\n    /**\n     * Process an action event.\n     *\n     * @param   {Event} e\n     */\n    const processAction = e => {\n        const popoutLink = e.target.closest('[data-action=\"play\"]');\n        if (popoutLink) {\n            e.preventDefault();\n\n            const videoPlayer = window.open('', '_blank');\n            videoPlayer.opener = null;\n            videoPlayer.location.href = popoutLink.href;\n            // TODO send a recording viewed event when this event will be implemented.\n            return;\n        }\n\n        // Fetch any clicked anchor.\n        const clickedLink = e.target.closest('a[data-action]');\n        if (clickedLink && !clickedLink.classList.contains('disabled')) {\n            e.preventDefault();\n\n            // Create a spinning icon on the table.\n            const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode());\n\n            requestAction(clickedLink)\n                .then(refreshTableData)\n                .then(iconPromise.resolve)\n                .catch(displayException);\n        }\n    };\n\n    const processSearchSubmission = e => {\n        // Prevent the default action.\n        e.preventDefault();\n        const parentNode = e.target.closest('div[role=search]');\n        const searchInput = parentNode.querySelector('input[name=search]');\n        filterByText(searchInput.value);\n    };\n\n    const registerEventListeners = () => {\n        // Add event listeners to the table boundingBox.\n        const boundingBox = dataTable.get('boundingBox').getDOMNode();\n        boundingBox.addEventListener('click', processAction);\n\n        // Setup the search from handlers.\n        const searchForm = document.querySelector(searchFormId);\n        if (searchForm) {\n            const searchButton = document.querySelector(searchFormId + ' button');\n            searchButton.addEventListener('click', processSearchSubmission);\n        }\n    };\n\n    return {\n        filterByText,\n        refreshTableData,\n        registerEventListeners,\n    };\n};\n\n/**\n * Setup the data table for the specified BBB instance.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param   {object} response The response from the data request\n * @returns {Promise}\n */\nconst setupDatatable = (tableId, searchFormId, response) => {\n    if (!response) {\n        return Promise.resolve();\n    }\n\n    if (!response.status) {\n        // Something failed. Continue to show the plain output.\n        return Promise.resolve();\n    }\n\n    const recordingData = response.tabledata;\n\n    const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable');\n    return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()])\n        .then(([yuiInstance, strings]) => {\n            // Here we use a custom formatter for date.\n            // See https://clarle.github.io/yui3/yui/docs/api/classes/DataTable.BodyView.Formatters.html\n            // Inspired from examples here: https://clarle.github.io/yui3/yui/docs/datatable/\n            // Normally formatter have the prototype: (col) => (cell) => <computed value>, see:\n            // https://clarle.github.io/yui3/yui/docs/api/files/datatable_js_formatters.js.html#l100 .\n            const dateCustomFormatter = () => (cell) => formatDate(recordingData.locale, cell.value);\n            // Add the fetched strings to the YUI Instance.\n            yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings});\n            yuiInstance.DataTable.BodyView.Formatters.customDate = dateCustomFormatter;\n            return yuiInstance;\n        })\n        .then(yuiInstance => {\n\n            const tableData = getFormattedData(response);\n            yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view', yuiInstance.DataTable.Paginator.View, [], {\n                _modelChange: function(e) {\n                    var changed = e.changed,\n                        totalItems = (changed && changed.totalItems);\n                    if (totalItems) {\n                        this._updateControlsUI(e.target.get('page'));\n                    }\n                }\n            });\n            return new yuiInstance.DataTable({\n                paginatorView: \"RecordsPaginatorView\",\n                width: \"1195px\",\n                columns: recordingData.columns,\n                data: tableData,\n                rowsPerPage: 10,\n                paginatorLocation: ['header', 'footer'],\n                autoSync: true\n            });\n        })\n        .then(dataTable => {\n            dataTable.render(tableId);\n            const {registerEventListeners} = getDataTableFunctions(\n                tableId,\n                searchFormId,\n                dataTable);\n            registerEventListeners();\n            return dataTable;\n        })\n        .then(dataTable => {\n            pendingPromise.resolve();\n            return dataTable;\n        });\n};\n\n/**\n * Initialise recordings code.\n *\n * @method init\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n */\nexport const init = (tableId, searchFormId) => {\n    const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings:init');\n\n    fetchRecordingData(tableId)\n        .then(response => setupDatatable(tableId, searchFormId, response))\n        .then(() => pendingPromise.resolve())\n        .catch(displayException);\n};\n"],"names":["stringsWithKeys","first","prev","next","last","goToLabel","goToAction","perPage","showAll","Object","entries","map","entry","getStringsForYui","stringMap","keys","key","component","then","stringArray","assign","index","getFormattedData","response","recordingData","tabledata","JSON","parse","data","getTableNode","tableSelector","document","querySelector","fetchRecordingData","tableNode","Promise","resolve","dataset","importMode","repository","fetchRecordingsToImport","bbbid","bbbSourceInstanceId","bbbSourceCourseId","tools","groupId","fetchRecordings","getDataTableFunctions","tableId","searchFormId","dataTable","updateTableFromResponse","status","get","reset","set","currentFilter","filterByText","refreshTableData","value","dataModel","escapedRegex","replace","rsearch","RegExp","filter","asList","item","name","test","description","recordingConfirmationMessage","async","playbackElement","recordingid","recordingType","imported","confirmation","action","associatedLinkCount","_document$querySelect","_document$querySelect2","links","processAction","e","popoutLink","target","closest","preventDefault","videoPlayer","window","open","opener","location","href","clickedLink","classList","contains","iconPromise","getDOMNode","getDataFromAction","element","dataType","dataElement","elementData","payload","bigbluebuttonbnid","additionaloptions","bbbsourceid","bbbcourseid","sourceid","stringify","requireConfirmation","updateRecording","requestAction","catch","displayException","processSearchSubmission","searchInput","registerEventListeners","addEventListener","setupDatatable","pendingPromise","Pending","all","lang","locale","YUI","use","Y","_ref","yuiInstance","strings","Intl","add","config","DataTable","BodyView","Formatters","customDate","cell","formatDate","date","Date","toLocaleDateString","weekday","year","month","day","tableData","RecordsPaginatorView","Base","create","Paginator","View","_modelChange","changed","totalItems","_updateControlsUI","paginatorView","width","columns","rowsPerPage","paginatorLocation","autoSync","render"],"mappings":"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"}