MDL-48756 editor_atto: Bulk atto autosave queries
This commit is contained in:
committed by
Dan Poltawski
parent
906c52ee96
commit
adaf294a75
+161
-122
@@ -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);
|
||||
|
||||
+297
-98
@@ -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> </p>' || response_json.result === '<p><br> </p>') {
|
||||
response_json.result = '';
|
||||
}
|
||||
// Check for IE 9 and 10.
|
||||
if (response.result === '<p> </p>' || response.result === '<p><br> </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"
|
||||
]
|
||||
});
|
||||
|
||||
+4
-4
File diff suppressed because one or more lines are too long
+293
-95
@@ -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> </p>' || response_json.result === '<p><br> </p>') {
|
||||
response_json.result = '';
|
||||
}
|
||||
// Check for IE 9 and 10.
|
||||
if (response.result === '<p> </p>' || response.result === '<p><br> </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
@@ -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
@@ -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> </p>' || response_json.result === '<p><br> </p>') {
|
||||
response_json.result = '';
|
||||
}
|
||||
// Check for IE 9 and 10.
|
||||
if (response.result === '<p> </p>' || response.result === '<p><br> </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": {
|
||||
|
||||
Reference in New Issue
Block a user