MDL-48756 editor_atto: Bulk atto autosave queries

This commit is contained in:
Frederic Massart
2016-06-07 11:49:49 +01:00
committed by Dan Poltawski
parent 906c52ee96
commit adaf294a75
8 changed files with 1053 additions and 417 deletions
+161 -122
View File
@@ -27,156 +27,195 @@ define('AJAX_SCRIPT', true);
require_once(dirname(__FILE__) . '/../../../config.php');
require_once($CFG->libdir . '/filestorage/file_storage.php');
$contextid = required_param('contextid', PARAM_INT);
$elementid = required_param('elementid', PARAM_ALPHANUMEXT);
$pagehash = required_param('pagehash', PARAM_ALPHANUMEXT);
$pageinstance = required_param('pageinstance', PARAM_ALPHANUMEXT);
// Clean up actions.
$actions = array_map(function($actionparams) {
$action = isset($actionparams['action']) ? $actionparams['action'] : null;
$params = [];
$keys = [
'action' => PARAM_ALPHA,
'contextid' => PARAM_INT,
'elementid' => PARAM_ALPHANUMEXT,
'pagehash' => PARAM_ALPHANUMEXT,
'pageinstance' => PARAM_ALPHANUMEXT
];
if ($action == 'save') {
$keys['drafttext'] = PARAM_RAW;
} else if ($action == 'resume') {
$keys['draftid'] = PARAM_INT;
}
foreach ($keys as $key => $type) {
// Replicate required_param().
if (!isset($actionparams[$key])) {
print_error('missingparam', '', '', $key);
}
$params[$key] = clean_param($actionparams[$key], $type);
}
return $params;
}, isset($_REQUEST['actions']) ? $_REQUEST['actions'] : []);
$now = time();
// This is the oldest time any autosave text will be recovered from.
// This is so that there is a good chance the draft files will still exist (there are many variables so
// this is impossible to guarantee).
$before = $now - 60*60*24*4;
list($context, $course, $cm) = get_context_info_array($contextid);
$context = context_system::instance();
$PAGE->set_url('/lib/editor/atto/autosave-ajax.php');
$PAGE->set_context($context);
require_login($course, false, $cm);
require_sesskey();
require_login();
if (isguestuser()) {
print_error('accessdenied', 'admin');
}
require_sesskey();
if (!in_array('atto', explode(',', get_config('core', 'texteditors')))) {
print_error('accessdenied', 'admin');
}
$action = required_param('action', PARAM_ALPHA);
$responses = array();
foreach ($actions as $actionparams) {
$response = array();
$action = $actionparams['action'];
$contextid = $actionparams['contextid'];
$elementid = $actionparams['elementid'];
$pagehash = $actionparams['pagehash'];
$pageinstance = $actionparams['pageinstance'];
if ($action === 'save') {
$drafttext = required_param('drafttext', PARAM_RAW);
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
if ($action === 'save') {
$drafttext = $actionparams['drafttext'];
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
$record = $DB->get_record('editor_atto_autosave', $params);
if ($record && $record->pageinstance != $pageinstance) {
print_error('concurrent access from the same user is not supported');
die();
}
if (!$record) {
$record = new stdClass();
$record->elementid = $elementid;
$record->userid = $USER->id;
$record->pagehash = $pagehash;
$record->contextid = $contextid;
$record->drafttext = $drafttext;
$record->pageinstance = $pageinstance;
$record->timemodified = $now;
$DB->insert_record('editor_atto_autosave', $record);
// No response means no error.
die();
} else {
$record->drafttext = $drafttext;
$record->timemodified = time();
$DB->update_record('editor_atto_autosave', $record);
// No response means no error.
die();
}
} else if ($action == 'resume') {
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
$newdraftid = required_param('draftid', PARAM_INT);
$record = $DB->get_record('editor_atto_autosave', $params);
if (!$record) {
$record = new stdClass();
$record->elementid = $elementid;
$record->userid = $USER->id;
$record->pagehash = $pagehash;
$record->contextid = $contextid;
$record->pageinstance = $pageinstance;
$record->pagehash = $pagehash;
$record->draftid = $newdraftid;
$record->timemodified = time();
$record->drafttext = '';
$DB->insert_record('editor_atto_autosave', $record);
// No response means no error.
die();
} else {
// Copy all draft files from the old draft area.
$usercontext = context_user::instance($USER->id);
$stale = $record->timemodified < $before;
require_once($CFG->libdir . '/filelib.php');
$fs = get_file_storage();
$files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
$lastfilemodified = 0;
foreach ($files as $file) {
$lastfilemodified = max($lastfilemodified, $file->get_timemodified());
}
if ($record->timemodified < $lastfilemodified) {
$stale = true;
$record = $DB->get_record('editor_atto_autosave', $params);
if ($record && $record->pageinstance != $pageinstance) {
print_error('concurrent access from the same user is not supported');
die();
}
if (!$stale) {
// This function copies all the files in one draft area, to another area (in this case it's
// another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
$newdrafttext = file_save_draft_area_files($record->draftid,
$usercontext->id,
'user',
'draft',
$newdraftid,
array(),
$record->drafttext);
// Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
$newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
'draftfile.php',
$usercontext->id,
'user',
'draft',
$newdraftid);
$record->drafttext = $newdrafttext;
if (!$record) {
$record = new stdClass();
$record->elementid = $elementid;
$record->userid = $USER->id;
$record->pagehash = $pagehash;
$record->contextid = $contextid;
$record->drafttext = $drafttext;
$record->pageinstance = $pageinstance;
$record->draftid = $newdraftid;
$record->timemodified = $now;
$DB->insert_record('editor_atto_autosave', $record);
// No response means no error.
$responses[] = null;
continue;
} else {
$record->drafttext = $drafttext;
$record->timemodified = time();
$DB->update_record('editor_atto_autosave', $record);
// A response means the draft has been restored and here is the auto-saved text.
$response['result'] = $record->drafttext;
echo json_encode($response);
} else {
$DB->delete_records('editor_atto_autosave', array('id' => $record->id));
// No response means no error.
$responses[] = null;
continue;
}
} else if ($action == 'resume') {
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
$newdraftid = $actionparams['draftid'];
$record = $DB->get_record('editor_atto_autosave', $params);
if (!$record) {
$record = new stdClass();
$record->elementid = $elementid;
$record->userid = $USER->id;
$record->pagehash = $pagehash;
$record->contextid = $contextid;
$record->pageinstance = $pageinstance;
$record->pagehash = $pagehash;
$record->draftid = $newdraftid;
$record->timemodified = time();
$record->drafttext = '';
$DB->insert_record('editor_atto_autosave', $record);
// No response means no error.
}
die();
}
} else if ($action == 'reset') {
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
$responses[] = null;
continue;
$DB->delete_records('editor_atto_autosave', $params);
die();
} else {
// Copy all draft files from the old draft area.
$usercontext = context_user::instance($USER->id);
$stale = $record->timemodified < $before;
require_once($CFG->libdir . '/filelib.php');
$fs = get_file_storage();
$files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
$lastfilemodified = 0;
foreach ($files as $file) {
$lastfilemodified = max($lastfilemodified, $file->get_timemodified());
}
if ($record->timemodified < $lastfilemodified) {
$stale = true;
}
if (!$stale) {
// This function copies all the files in one draft area, to another area (in this case it's
// another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
$newdrafttext = file_save_draft_area_files($record->draftid,
$usercontext->id,
'user',
'draft',
$newdraftid,
array(),
$record->drafttext);
// Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
$newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
'draftfile.php',
$usercontext->id,
'user',
'draft',
$newdraftid);
$record->drafttext = $newdrafttext;
$record->pageinstance = $pageinstance;
$record->draftid = $newdraftid;
$record->timemodified = time();
$DB->update_record('editor_atto_autosave', $record);
// A response means the draft has been restored and here is the auto-saved text.
$response = ['result' => $record->drafttext];
$responses[] = $response;
} else {
$DB->delete_records('editor_atto_autosave', array('id' => $record->id));
// No response means no error.
$responses[] = null;
}
continue;
}
} else if ($action == 'reset') {
$params = array('elementid' => $elementid,
'userid' => $USER->id,
'pagehash' => $pagehash,
'contextid' => $contextid);
$DB->delete_records('editor_atto_autosave', $params);
$responses[] = null;
continue;
}
}
print_error('invalidarguments');
echo json_encode($responses);
@@ -786,19 +786,6 @@ EditorAutosave.ATTRS= {
pageHash: {
value: '',
writeOnce: true
},
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
}
};
@@ -839,8 +826,7 @@ EditorAutosave.prototype = {
form,
optiontype = null,
options = this.get('filepickeroptions'),
params,
url;
params;
if (!this.get('autosaveEnabled')) {
// Autosave disabled for this instance.
@@ -856,99 +842,73 @@ EditorAutosave.prototype = {
// First see if there are any saved drafts.
// Make an ajax request.
url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'resume',
drafttext: '',
draftid: draftid,
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
success: function(id,o) {
var response_json;
if (typeof o.responseText !== "undefined" && o.responseText !== "") {
response_json = JSON.parse(o.responseText);
this.autosaveIo(params, this, {
success: function(response) {
if (response === null) {
// This can happen when there is nothing to resume from.
return;
} else if (!response) {
Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE);
return;
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
response_json.result === '<br>') {
response_json.result = '';
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
response.result === '<br>') {
response.result = '';
}
// Check for IE 9 and 10.
if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
response_json.result = '';
}
// Check for IE 9 and 10.
if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
response.result = '';
}
if (response_json.error || typeof response_json.result === 'undefined') {
Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response_json.result !== this.textarea.get('value') &&
response_json.result !== '') {
Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
this.recoverText(response_json.result);
}
this._fireSelectionChanged();
}
},
failure: function() {
if (response.error || typeof response.result === 'undefined') {
Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response.result !== this.textarea.get('value') &&
response.result !== '') {
Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
this.recoverText(response.result);
}
this._fireSelectionChanged();
},
failure: function() {
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
}
});
// Now setup the timer for periodic saves.
var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
// Now setup the listener for form submission.
form = this.textarea.ancestor('form');
if (form) {
form.on('submit', this.resetAutosave, this);
this.autosaveIoOnSubmit(form, {
action: 'reset',
contextid: this.get('contextid'),
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
});
}
return this;
},
/**
* Clear the autosave text because the form was submitted normally.
*
* @method resetAutosave
* @chainable
*/
resetAutosave: function() {
// Make an ajax request to reset the autosaved text.
var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
var params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'reset',
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
sync: true
});
return this;
},
/**
* Recover a previous version of this text and show a message.
*
@@ -1004,29 +964,23 @@ EditorAutosave.prototype = {
};
// Reusable error handler - must be passed the correct context.
var ajaxErrorFunction = function(code, response) {
var ajaxErrorFunction = function(response) {
var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE);
Y.log(response, 'warn', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
error: ajaxErrorFunction,
failure: ajaxErrorFunction,
success: function(code, response) {
if (response.responseText !== "") {
Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
this.autosaveIo(params, this, {
failure: ajaxErrorFunction,
success: function(response) {
if (response && response.error) {
Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
}
});
@@ -1051,6 +1005,250 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A autosave function for the Atto editor.
*
* @module moodle-editor_atto-autosave-io
* @submodule autosave-io
* @package editor_atto
* @copyright 2016 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var EditorAutosaveIoDispatcherInstance = null;
function EditorAutosaveIoDispatcher() {
EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
this._submitEvents = {};
this._queue = [];
this._throttle = null;
}
EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
EditorAutosaveIoDispatcher.ATTRS = {
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
},
/**
* The time buffer for the throttled requested.
*
* @attribute delay
* @type Number
* @default 50
* @readOnly
*/
delay: {
value: 50,
readOnly: true
}
};
Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
dispatch: function(params, context, callbacks) {
if (this._throttle) {
this._throttle.cancel();
}
this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
this._queue.push([params, context, callbacks]);
},
/**
* Dispatches the requests in the queue.
*
* @return {Void}
*/
_processDispatchQueue: function() {
var queue = this._queue,
data = {};
this._queue = [];
if (queue.length < 1) {
return;
}
Y.Array.each(queue, function(item, index) {
data[index] = item[0];
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
on: {
start: this._makeIoEventCallback('start', queue),
complete: this._makeIoEventCallback('complete', queue),
failure: this._makeIoEventCallback('failure', queue),
end: this._makeIoEventCallback('end', queue),
success: this._makeIoEventCallback('success', queue)
}
});
},
/**
* Creates a function that dispatches an IO response to callbacks.
*
* @param {String} event The type of event.
* @param {Array} queue The queue.
* @return {Function}
*/
_makeIoEventCallback: function(event, queue) {
var noop = function() {};
return function() {
var response = arguments[1],
parsed = {};
if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
&& typeof response.responseText !== 'undefined' && response.responseText !== '')) {
// Success and complete events need to parse the response.
parsed = JSON.parse(response.responseText) || {};
}
Y.Array.each(queue, function(item, index) {
var context = item[1],
cb = (item[2] && item[2][event]) || noop,
arg;
if (parsed && parsed.error) {
// The response is an error, we send it to everyone.
arg = parsed;
} else if (parsed) {
// The response was parsed, we only communicate the relevant portion of the response.
arg = parsed[index];
}
cb.apply(context, [arg]);
});
};
},
/**
* Form submit handler.
*
* @param {EventFacade} e The event.
* @return {Void}
*/
_onSubmit: function(e) {
var data = {},
id = e.currentTarget.generateID(),
params = this._submitEvents[id];
if (!params || params.ios.length < 1) {
return;
}
Y.Array.each(params.ios, function(param, index) {
data[index] = param;
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
sync: true
});
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} node The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
whenSubmit: function(node, params) {
if (typeof this._submitEvents[node.generateID()] === 'undefined') {
this._submitEvents[node.generateID()] = {
event: node.on('submit', this._onSubmit, this),
ios: []
};
}
this._submitEvents[node.get('id')].ios.push([params]);
}
});
EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
function EditorAutosaveIo() {}
EditorAutosaveIo.prototype = {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
autosaveIo: function(params, context, callbacks) {
EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} form The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
autosaveIoOnSubmit: function(form, params) {
EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
}
};
Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @module moodle-editor_atto-editor
* @submodule clean
@@ -2606,6 +2804,7 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);
"moodle-core-notification-confirm",
"moodle-editor_atto-rangy",
"handlebars",
"timers"
"timers",
"querystring-stringify"
]
});
File diff suppressed because one or more lines are too long
@@ -781,19 +781,6 @@ EditorAutosave.ATTRS= {
pageHash: {
value: '',
writeOnce: true
},
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
}
};
@@ -834,8 +821,7 @@ EditorAutosave.prototype = {
form,
optiontype = null,
options = this.get('filepickeroptions'),
params,
url;
params;
if (!this.get('autosaveEnabled')) {
// Autosave disabled for this instance.
@@ -851,97 +837,70 @@ EditorAutosave.prototype = {
// First see if there are any saved drafts.
// Make an ajax request.
url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'resume',
drafttext: '',
draftid: draftid,
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
success: function(id,o) {
var response_json;
if (typeof o.responseText !== "undefined" && o.responseText !== "") {
response_json = JSON.parse(o.responseText);
this.autosaveIo(params, this, {
success: function(response) {
if (response === null) {
// This can happen when there is nothing to resume from.
return;
} else if (!response) {
return;
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
response_json.result === '<br>') {
response_json.result = '';
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
response.result === '<br>') {
response.result = '';
}
// Check for IE 9 and 10.
if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
response_json.result = '';
}
// Check for IE 9 and 10.
if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
response.result = '';
}
if (response_json.error || typeof response_json.result === 'undefined') {
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response_json.result !== this.textarea.get('value') &&
response_json.result !== '') {
this.recoverText(response_json.result);
}
this._fireSelectionChanged();
}
},
failure: function() {
if (response.error || typeof response.result === 'undefined') {
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response.result !== this.textarea.get('value') &&
response.result !== '') {
this.recoverText(response.result);
}
this._fireSelectionChanged();
},
failure: function() {
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
}
});
// Now setup the timer for periodic saves.
var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
// Now setup the listener for form submission.
form = this.textarea.ancestor('form');
if (form) {
form.on('submit', this.resetAutosave, this);
this.autosaveIoOnSubmit(form, {
action: 'reset',
contextid: this.get('contextid'),
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
});
}
return this;
},
/**
* Clear the autosave text because the form was submitted normally.
*
* @method resetAutosave
* @chainable
*/
resetAutosave: function() {
// Make an ajax request to reset the autosaved text.
var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
var params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'reset',
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
sync: true
});
return this;
},
/**
* Recover a previous version of this text and show a message.
*
@@ -996,27 +955,21 @@ EditorAutosave.prototype = {
};
// Reusable error handler - must be passed the correct context.
var ajaxErrorFunction = function(code, response) {
var ajaxErrorFunction = function(response) {
var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
error: ajaxErrorFunction,
failure: ajaxErrorFunction,
success: function(code, response) {
if (response.responseText !== "") {
Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
this.autosaveIo(params, this, {
failure: ajaxErrorFunction,
success: function(response) {
if (response && response.error) {
Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
}
});
@@ -1041,6 +994,250 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A autosave function for the Atto editor.
*
* @module moodle-editor_atto-autosave-io
* @submodule autosave-io
* @package editor_atto
* @copyright 2016 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var EditorAutosaveIoDispatcherInstance = null;
function EditorAutosaveIoDispatcher() {
EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
this._submitEvents = {};
this._queue = [];
this._throttle = null;
}
EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
EditorAutosaveIoDispatcher.ATTRS = {
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
},
/**
* The time buffer for the throttled requested.
*
* @attribute delay
* @type Number
* @default 50
* @readOnly
*/
delay: {
value: 50,
readOnly: true
}
};
Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
dispatch: function(params, context, callbacks) {
if (this._throttle) {
this._throttle.cancel();
}
this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
this._queue.push([params, context, callbacks]);
},
/**
* Dispatches the requests in the queue.
*
* @return {Void}
*/
_processDispatchQueue: function() {
var queue = this._queue,
data = {};
this._queue = [];
if (queue.length < 1) {
return;
}
Y.Array.each(queue, function(item, index) {
data[index] = item[0];
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
on: {
start: this._makeIoEventCallback('start', queue),
complete: this._makeIoEventCallback('complete', queue),
failure: this._makeIoEventCallback('failure', queue),
end: this._makeIoEventCallback('end', queue),
success: this._makeIoEventCallback('success', queue)
}
});
},
/**
* Creates a function that dispatches an IO response to callbacks.
*
* @param {String} event The type of event.
* @param {Array} queue The queue.
* @return {Function}
*/
_makeIoEventCallback: function(event, queue) {
var noop = function() {};
return function() {
var response = arguments[1],
parsed = {};
if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
&& typeof response.responseText !== 'undefined' && response.responseText !== '')) {
// Success and complete events need to parse the response.
parsed = JSON.parse(response.responseText) || {};
}
Y.Array.each(queue, function(item, index) {
var context = item[1],
cb = (item[2] && item[2][event]) || noop,
arg;
if (parsed && parsed.error) {
// The response is an error, we send it to everyone.
arg = parsed;
} else if (parsed) {
// The response was parsed, we only communicate the relevant portion of the response.
arg = parsed[index];
}
cb.apply(context, [arg]);
});
};
},
/**
* Form submit handler.
*
* @param {EventFacade} e The event.
* @return {Void}
*/
_onSubmit: function(e) {
var data = {},
id = e.currentTarget.generateID(),
params = this._submitEvents[id];
if (!params || params.ios.length < 1) {
return;
}
Y.Array.each(params.ios, function(param, index) {
data[index] = param;
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
sync: true
});
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} node The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
whenSubmit: function(node, params) {
if (typeof this._submitEvents[node.generateID()] === 'undefined') {
this._submitEvents[node.generateID()] = {
event: node.on('submit', this._onSubmit, this),
ios: []
};
}
this._submitEvents[node.get('id')].ios.push([params]);
}
});
EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
function EditorAutosaveIo() {}
EditorAutosaveIo.prototype = {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
autosaveIo: function(params, context, callbacks) {
EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} form The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
autosaveIoOnSubmit: function(form, params) {
EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
}
};
Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @module moodle-editor_atto-editor
* @submodule clean
@@ -2593,6 +2790,7 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);
"moodle-core-notification-confirm",
"moodle-editor_atto-rangy",
"handlebars",
"timers"
"timers",
"querystring-stringify"
]
});
@@ -7,6 +7,7 @@
"notify.js",
"textarea.js",
"autosave.js",
"autosave-io.js",
"clean.js",
"commands.js",
"toolbar.js",
+244
View File
@@ -0,0 +1,244 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A autosave function for the Atto editor.
*
* @module moodle-editor_atto-autosave-io
* @submodule autosave-io
* @package editor_atto
* @copyright 2016 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var EditorAutosaveIoDispatcherInstance = null;
function EditorAutosaveIoDispatcher() {
EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
this._submitEvents = {};
this._queue = [];
this._throttle = null;
}
EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
EditorAutosaveIoDispatcher.ATTRS = {
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
},
/**
* The time buffer for the throttled requested.
*
* @attribute delay
* @type Number
* @default 50
* @readOnly
*/
delay: {
value: 50,
readOnly: true
}
};
Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
dispatch: function(params, context, callbacks) {
if (this._throttle) {
this._throttle.cancel();
}
this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
this._queue.push([params, context, callbacks]);
},
/**
* Dispatches the requests in the queue.
*
* @return {Void}
*/
_processDispatchQueue: function() {
var queue = this._queue,
data = {};
this._queue = [];
if (queue.length < 1) {
return;
}
Y.Array.each(queue, function(item, index) {
data[index] = item[0];
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
on: {
start: this._makeIoEventCallback('start', queue),
complete: this._makeIoEventCallback('complete', queue),
failure: this._makeIoEventCallback('failure', queue),
end: this._makeIoEventCallback('end', queue),
success: this._makeIoEventCallback('success', queue)
}
});
},
/**
* Creates a function that dispatches an IO response to callbacks.
*
* @param {String} event The type of event.
* @param {Array} queue The queue.
* @return {Function}
*/
_makeIoEventCallback: function(event, queue) {
var noop = function() {};
return function() {
var response = arguments[1],
parsed = {};
if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
&& typeof response.responseText !== 'undefined' && response.responseText !== '')) {
// Success and complete events need to parse the response.
parsed = JSON.parse(response.responseText) || {};
}
Y.Array.each(queue, function(item, index) {
var context = item[1],
cb = (item[2] && item[2][event]) || noop,
arg;
if (parsed && parsed.error) {
// The response is an error, we send it to everyone.
arg = parsed;
} else if (parsed) {
// The response was parsed, we only communicate the relevant portion of the response.
arg = parsed[index];
}
cb.apply(context, [arg]);
});
};
},
/**
* Form submit handler.
*
* @param {EventFacade} e The event.
* @return {Void}
*/
_onSubmit: function(e) {
var data = {},
id = e.currentTarget.generateID(),
params = this._submitEvents[id];
if (!params || params.ios.length < 1) {
return;
}
Y.Array.each(params.ios, function(param, index) {
data[index] = param;
});
Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
method: 'POST',
data: Y.QueryString.stringify({
actions: data,
sesskey: M.cfg.sesskey
}),
sync: true
});
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} node The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
whenSubmit: function(node, params) {
if (typeof this._submitEvents[node.generateID()] === 'undefined') {
this._submitEvents[node.generateID()] = {
event: node.on('submit', this._onSubmit, this),
ios: []
};
}
this._submitEvents[node.get('id')].ios.push([params]);
}
});
EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
function EditorAutosaveIo() {}
EditorAutosaveIo.prototype = {
/**
* Dispatch an IO request.
*
* This method will put the requests in a queue in order to attempt to bulk them.
*
* @param {Object} params The parameters of the request.
* @param {Object} context The context in which the callbacks are called.
* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
* optional keys defining the callbacks to call. Success and Complete
* functions will receive the response as parameter. Success and Complete
* may receive an object containing the error key, use this to confirm
* that no errors occured.
* @return {Void}
*/
autosaveIo: function(params, context, callbacks) {
EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
},
/**
* Registers a request to be made on form submission.
*
* @param {Node} form The forum node we will listen to.
* @param {Object} params Parameters for the IO request.
* @return {Void}
*/
autosaveIoOnSubmit: function(form, params) {
EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
}
};
Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
+51 -97
View File
@@ -65,19 +65,6 @@ EditorAutosave.ATTRS= {
pageHash: {
value: '',
writeOnce: true
},
/**
* The relative path to the ajax script.
*
* @attribute autosaveAjaxScript
* @type String
* @default '/lib/editor/atto/autosave-ajax.php'
* @readOnly
*/
autosaveAjaxScript: {
value: '/lib/editor/atto/autosave-ajax.php',
readOnly: true
}
};
@@ -118,8 +105,7 @@ EditorAutosave.prototype = {
form,
optiontype = null,
options = this.get('filepickeroptions'),
params,
url;
params;
if (!this.get('autosaveEnabled')) {
// Autosave disabled for this instance.
@@ -135,99 +121,73 @@ EditorAutosave.prototype = {
// First see if there are any saved drafts.
// Make an ajax request.
url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'resume',
drafttext: '',
draftid: draftid,
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
success: function(id,o) {
var response_json;
if (typeof o.responseText !== "undefined" && o.responseText !== "") {
response_json = JSON.parse(o.responseText);
this.autosaveIo(params, this, {
success: function(response) {
if (response === null) {
// This can happen when there is nothing to resume from.
return;
} else if (!response) {
Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE);
return;
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
response_json.result === '<br>') {
response_json.result = '';
}
// Revert untouched editor contents to an empty string.
// Check for FF and Chrome.
if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
response.result === '<br>') {
response.result = '';
}
// Check for IE 9 and 10.
if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
response_json.result = '';
}
// Check for IE 9 and 10.
if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
response.result = '';
}
if (response_json.error || typeof response_json.result === 'undefined') {
Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response_json.result !== this.textarea.get('value') &&
response_json.result !== '') {
Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
this.recoverText(response_json.result);
}
this._fireSelectionChanged();
}
},
failure: function() {
if (response.error || typeof response.result === 'undefined') {
Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
} else if (response.result !== this.textarea.get('value') &&
response.result !== '') {
Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
this.recoverText(response.result);
}
this._fireSelectionChanged();
},
failure: function() {
this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
}
});
// Now setup the timer for periodic saves.
var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
// Now setup the listener for form submission.
form = this.textarea.ancestor('form');
if (form) {
form.on('submit', this.resetAutosave, this);
this.autosaveIoOnSubmit(form, {
action: 'reset',
contextid: this.get('contextid'),
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
});
}
return this;
},
/**
* Clear the autosave text because the form was submitted normally.
*
* @method resetAutosave
* @chainable
*/
resetAutosave: function() {
// Make an ajax request to reset the autosaved text.
var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
var params = {
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'reset',
elementid: this.get('elementid'),
pageinstance: this.autosaveInstance,
pagehash: this.get('pageHash')
};
Y.io(url, {
method: 'POST',
data: params,
sync: true
});
return this;
},
/**
* Recover a previous version of this text and show a message.
*
@@ -283,29 +243,23 @@ EditorAutosave.prototype = {
};
// Reusable error handler - must be passed the correct context.
var ajaxErrorFunction = function(code, response) {
var ajaxErrorFunction = function(response) {
var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE);
Y.log(response, 'warn', LOGNAME_AUTOSAVE);
this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
};
Y.io(url, {
method: 'POST',
data: params,
context: this,
on: {
error: ajaxErrorFunction,
failure: ajaxErrorFunction,
success: function(code, response) {
if (response.responseText !== "") {
Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
this.autosaveIo(params, this, {
failure: ajaxErrorFunction,
success: function(response) {
if (response && response.error) {
Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
} else {
// All working.
this.lastText = newText;
this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
}
}
});
@@ -15,7 +15,8 @@
"moodle-core-notification-confirm",
"moodle-editor_atto-rangy",
"handlebars",
"timers"
"timers",
"querystring-stringify"
]
},
"moodle-editor_atto-plugin": {