Completed
Branch pre-recording-server (120b27)
by Jacob
02:07
created

tinymce/js/commonmodule.js   B

Complexity

Total Complexity 48
Complexity/F 2.18

Size

Lines of Code 279
Function Count 22

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 16
Bugs 1 Features 0
Metric Value
cc 0
c 16
b 1
f 0
nc 128
dl 0
loc 279
rs 8.4864
wmc 48
mnd 5
bc 50
fnc 22
bpm 2.2727
cpm 2.1818
noi 0

12 Functions

Rating   Name   Duplication   Size   Complexity  
B M.tinymce_recordrtc.make_xmlhttprequest 0 27 1
A M.tinymce_recordrtc.pad 0 9 2
B M.tinymce_recordrtc.handle_stop 0 44 1
A M.tinymce_recordrtc.insert_annotation 0 12 2
B M.tinymce_recordrtc.handle_data_available 0 20 5
B 0 21 5
B M.tinymce_recordrtc.start_recording 0 25 1
A M.tinymce_recordrtc.set_time 0 12 2
A M.tinymce_recordrtc.capture_user_media 0 3 1
A M.tinymce_recordrtc.create_annotation 0 12 2
A M.tinymce_recordrtc.upload_to_server 0 56 1
A commonmodule.js ➔ d 0 3 1

How to fix   Complexity   

Complexity

Complex classes like tinymce/js/commonmodule.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
// TinyMCE recordrtc library functions.
2
// @package    tinymce_recordrtc.
3
// @author     Jesus Federico  (jesus [at] blindsidenetworks [dt] com).
4
// @author     Jacob Prud'homme (jacob [dt] prudhomme [at] blindsidenetworks [dt] com)
5
// @copyright  2016 onwards, Blindside Networks Inc.
6
// @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
7
8
// ESLint directives.
9
/* global tinyMCE, tinyMCEPopup */
10
/* exported alertWarning, alertDanger, countdownTicker, playerDOM */
11
/* eslint-disable camelcase, no-alert */
12
13
// Scrutinizer CI directives.
14
/** global: navigator */
15
/** global: parent */
16
/** global: M */
17
/** global: Y */
18
/** global: tinyMCEPopup */
19
20
M.tinymce_recordrtc = M.tinymce_recordrtc || {};
21
22
// Extract plugin settings to params hash.
23
(function() {
24
    var params = {};
25
    var r = /([^&=]+)=?([^&]*)/g;
26
27
    var d = function(s) {
28
        return window.decodeURIComponent(s.replace(/\+/g, ' '));
29
    };
30
31
    var search = window.location.search;
32
    var match = r.exec(search.substring(1));
33
    while (match) {
34
        params[d(match[1])] = d(match[2]);
35
36
        if (d(match[2]) === 'true' || d(match[2]) === 'false') {
37
            params[d(match[1])] = d(match[2]) === 'true' ? true : false;
38
        }
39
        match = r.exec(search.substring(1));
40
    }
41
42
    window.params = params;
43
})();
44
45
// Initialize some variables.
46
var alertWarning = null;
47
var alertDanger = null;
48
var blobSize = null;
49
var chunks = null;
50
var countdownSeconds = null;
51
var countdownTicker = null;
52
var maxUploadSize = null;
53
var mediaRecorder = null;
54
var player = null;
55
var playerDOM = null;
56
var recType = null;
57
var startStopBtn = null;
58
var uploadBtn = null;
59
60
// Capture webcam/microphone stream.
61
M.tinymce_recordrtc.capture_user_media = function(mediaConstraints, successCallback, errorCallback) {
62
    window.navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
63
};
64
65
// Add chunks of audio/video to array when made available.
66
M.tinymce_recordrtc.handle_data_available = function(event) {
67
    // Size of all recorded data so far.
68
    blobSize += event.data.size;
69
70
    // Push recording slice to array.
71
    // If total size of recording so far exceeds max upload limit, stop recording.
72
    // An extra condition exists to avoid displaying alert twice.
73
    if ((blobSize >= maxUploadSize) && (!window.localStorage.getItem('alerted'))) {
74
        window.localStorage.setItem('alerted', 'true');
75
76
        Y.use('node-event-simulate', function() {
77
            startStopBtn.simulate('click');
78
        });
79
        M.tinymce_recordrtc.show_alert('nearingmaxsize');
80
    } else if ((blobSize >= maxUploadSize) && (window.localStorage.getItem('alerted') === 'true')) {
81
        window.localStorage.removeItem('alerted');
82
    } else {
83
        chunks.push(event.data);
84
    }
85
};
86
87
M.tinymce_recordrtc.handle_stop = function() {
88
    // Set source of audio player.
89
    var blob = new window.Blob(chunks, {type: mediaRecorder.mimeType});
90
    player.set('src', window.URL.createObjectURL(blob));
91
92
    // Show audio player with controls enabled, and unmute.
93
    player.set('muted', false);
94
    player.set('controls', true);
95
    player.ancestor().ancestor().removeClass('hide');
96
97
    // Show upload button.
98
    uploadBtn.ancestor().ancestor().removeClass('hide');
99
    uploadBtn.set('textContent', M.util.get_string('attachrecording', 'tinymce_recordrtc'));
100
    uploadBtn.set('disabled', false);
101
102
    // Handle when upload button is clicked.
103
    uploadBtn.on('click', function() {
104
        // Trigger error if no recording has been made.
105
        if (!player.get('src') || chunks === []) {
106
            M.tinymce_recordrtc.show_alert('norecordingfound');
107
        } else {
108
            uploadBtn.set('disabled', true);
109
110
            // Upload recording to server.
111
            M.tinymce_recordrtc.upload_to_server(recType, function(progress, fileURLOrError) {
112
                if (progress === 'ended') { // Insert annotation in text.
113
                    uploadBtn.set('disabled', false);
114
                    M.tinymce_recordrtc.insert_annotation(recType, fileURLOrError);
115
                } else if (progress === 'upload-failed') { // Show error message in upload button.
116
                    uploadBtn.set('disabled', false);
117
                    uploadBtn.set('textContent', M.util.get_string('uploadfailed', 'tinymce_recordrtc') + ' ' + fileURLOrError);
118
                } else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
119
                    uploadBtn.set('disabled', false);
120
                    uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'tinymce_recordrtc'));
121
                } else if (progress === 'upload-aborted') {
122
                    uploadBtn.set('disabled', false);
123
                    uploadBtn.set('textContent', M.util.get_string('uploadaborted', 'tinymce_recordrtc') + ' ' + fileURLOrError);
124
                } else {
125
                    uploadBtn.set('textContent', progress);
126
                }
127
            });
128
        }
129
    });
130
};
131
132
// Get everything set up to start recording.
133
M.tinymce_recordrtc.start_recording = function(type, stream) {
134
    // The options for the recording codecs and bitrates.
135
    var options = M.tinymce_recordrtc.select_rec_options(type);
136
    mediaRecorder = new window.MediaRecorder(stream, options);
137
138
    // Initialize MediaRecorder events and start recording.
139
    mediaRecorder.ondataavailable = M.tinymce_recordrtc.handle_data_available;
140
    mediaRecorder.onstop = M.tinymce_recordrtc.handle_stop;
141
    mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
142
143
    // Mute audio, distracting while recording.
144
    player.set('muted', true);
145
146
    // Set recording timer to the time specified in the settings.
147
    countdownSeconds = window.params.timelimit;
148
    countdownSeconds++;
149
    var timerText = M.util.get_string('stoprecording', 'tinymce_recordrtc');
150
    timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
151
    startStopBtn.setHTML(timerText);
152
    M.tinymce_recordrtc.set_time();
153
    countdownTicker = window.setInterval(M.tinymce_recordrtc.set_time, 1000);
154
155
    // Make button clickable again, to allow stopping recording.
156
    startStopBtn.set('disabled', false);
157
};
158
159
// Upload recorded audio/video to server.
160
M.tinymce_recordrtc.upload_to_server = function(type, callback) {
161
    var xhr = new window.XMLHttpRequest();
162
163
    // Get src media of audio/video tag.
164
    xhr.open('GET', player.get('src'), true);
165
    xhr.responseType = 'blob';
166
167
    xhr.onload = function() {
168
        if (xhr.status === 200) { // If src media was successfully retrieved.
169
            // blob is now the media that the audio/video tag's src pointed to.
170
            var blob = this.response;
171
172
            // Generate filename with random ID and file extension.
173
            var fileName = (Math.random() * 1000).toString().replace('.', '');
174
            if (type === 'audio') {
175
                fileName += '-audio.ogg';
176
            } else {
177
                fileName += '-video.webm';
178
            }
179
180
            // Create FormData to send to PHP filepicker-upload script.
181
            var formData = new window.FormData(),
182
                editorId = tinyMCE.activeEditor.id,
183
                filepickerOptions = parent.M.editor_tinymce.filepicker_options[editorId].link,
184
                repositoryKeys = window.Object.keys(filepickerOptions.repositories);
185
186
            formData.append('repo_upload_file', blob, fileName);
187
            formData.append('itemid', filepickerOptions.itemid);
188
189
            for (var i = 0; i < repositoryKeys.length; i++) {
190
                if (filepickerOptions.repositories[repositoryKeys[i]].type === 'upload') {
191
                    formData.append('repo_id', filepickerOptions.repositories[repositoryKeys[i]].id);
192
                    break;
193
                }
194
            }
195
196
            formData.append('env', filepickerOptions.env);
197
            formData.append('sesskey', M.cfg.sesskey);
198
            formData.append('client_id', filepickerOptions.client_id);
199
            formData.append('savepath', '/');
200
            formData.append('ctx_id', filepickerOptions.context.id);
201
202
            // Pass FormData to PHP script using XHR.
203
            var uploadEndpoint = M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload';
204
            M.tinymce_recordrtc.make_xmlhttprequest(uploadEndpoint, formData, function(progress, responseText) {
205
                if (progress === 'upload-ended') {
206
                    callback('ended', window.JSON.parse(responseText).url);
207
                } else {
208
                    callback(progress);
209
                }
210
            });
211
        }
212
    };
213
214
    xhr.send();
215
};
216
217
// Handle XHR sending/receiving/status.
218
M.tinymce_recordrtc.make_xmlhttprequest = function(url, data, callback) {
219
    var xhr = new window.XMLHttpRequest();
220
221
    xhr.onreadystatechange = function() {
222
        if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
223
            callback('upload-ended', xhr.responseText);
224
        } else if (xhr.status === 404) { // When request returns 404 Not Found.
225
            callback('upload-failed-404');
226
        }
227
    };
228
229
    xhr.upload.onprogress = function(event) {
230
        callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'tinymce_recordrtc'));
231
    };
232
233
    xhr.upload.onerror = function(error) {
234
        callback('upload-failed', error);
235
    };
236
237
    xhr.upload.onabort = function(error) {
238
        callback('upload-aborted', error);
239
    };
240
241
    // POST FormData to PHP script that handles uploading/saving.
242
    xhr.open('POST', url);
243
    xhr.send(data);
244
};
245
246
// Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
247
M.tinymce_recordrtc.pad = function(val) {
248
    var valString = val + "";
249
250
    if (valString.length < 2) {
251
        return "0" + valString;
252
    } else {
253
        return valString;
254
    }
255
};
256
257
// Functionality to make recording timer count down.
258
// Also makes recording stop when time limit is hit.
259
M.tinymce_recordrtc.set_time = function() {
260
    countdownSeconds--;
261
262
    startStopBtn.one('span#seconds').set('textContent', M.tinymce_recordrtc.pad(countdownSeconds % 60));
263
    startStopBtn.one('span#minutes').set('textContent', M.tinymce_recordrtc.pad(window.parseInt(countdownSeconds / 60, 10)));
264
265
    if (countdownSeconds === 0) {
266
        Y.use('node-event-simulate', function() {
267
            startStopBtn.simulate('click');
268
        });
269
    }
270
};
271
272
// Generates link to recorded annotation to be inserted.
273
M.tinymce_recordrtc.create_annotation = function(type, recording_url) {
274
    var linkText = window.prompt(M.util.get_string('annotationprompt', 'tinymce_recordrtc'),
275
                                 M.util.get_string('annotation:' + type, 'tinymce_recordrtc'));
276
277
    // Return HTML for annotation link, if user did not press "Cancel".
278
    if (!linkText) {
279
        return undefined;
280
    } else {
281
        var annotation = '<div><a target="_blank" href="' + recording_url + '">' + linkText + '</a></div>';
282
        return annotation;
283
    }
284
};
285
286
// Inserts link to annotation in editor text area.
287
M.tinymce_recordrtc.insert_annotation = function(type, recording_url) {
288
    var annotation = M.tinymce_recordrtc.create_annotation(type, recording_url);
289
290
    // Insert annotation link.
291
    // If user pressed "Cancel", just go back to main recording screen.
292
    if (!annotation) {
293
        uploadBtn.set('textContent', M.util.get_string('attachrecording', 'tinymce_recordrtc'));
294
    } else {
295
        tinyMCEPopup.editor.execCommand('mceInsertContent', false, annotation);
296
        tinyMCEPopup.close();
297
    }
298
};
299