MDL-43996 editor_atto: add image drag and drop capability

Adds the ability to drag and drop images directly into Atto, uploading the image and embedding it
correctly. Contains fixes from 1.0.2, as well as a policy change - images dragged and dropped into
Atto now have role=presentation by default.
This commit is contained in:
Paul Nicholls 2014-11-28 15:37:38 +08:00 committed by Jetha Chan
parent d87bcfb325
commit 1461aee88a
7 changed files with 358 additions and 4 deletions

View File

@ -42,4 +42,5 @@ $string['presentationoraltrequired'] = 'Images must have a description, except i
$string['preview'] = 'Preview';
$string['saveimage'] = 'Save image';
$string['size'] = 'Size';
$string['width'] = 'Width';
$string['uploading'] = 'Uploading, please wait...';
$string['width'] = 'Width';

View File

@ -49,6 +49,7 @@ function atto_image_strings_for_js() {
'presentationoraltrequired',
'size',
'width',
'uploading',
);
$PAGE->requires->strings_for_js($strings, 'atto_image');

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014111000; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2014112800; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2014110400; // Requires this Moodle version.
$plugin->component = 'atto_image'; // Full name of the plugin (used for diagnostics).

View File

@ -165,6 +165,7 @@ var CSS = {
'{{#if presentation}}role="presentation" {{/if}}' +
'style="{{alignment}}{{margin}}{{customstyle}}"' +
'{{#if classlist}}class="{{classlist}}" {{/if}}' +
'{{#if id}}id="{{id}}" {{/if}}' +
'/>';
Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
@ -206,6 +207,7 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
_rawImageDimensions: null,
initializer: function() {
this.addButton({
icon: 'e/insert_edit_image',
callback: this._displayDialogue,
@ -213,6 +215,121 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDoubleClick, 'img', this);
this.editor.on('drop', this._handleDragDrop, this);
},
/**
* Handle a drag and drop event with an image.
*
* @method _handleDragDrop
* @param {EventFacade} e
* @private
*/
_handleDragDrop: function(e) {
var self = this,
host = this.get('host'),
template = Y.Handlebars.compile(IMAGETEMPLATE);
host.saveSelection();
e = e._event;
// Only handle the event if an image file was dropped in.
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length && /^image\//.test(e.dataTransfer.files[0].type)) {
var options = host.get('filepickeroptions').image,
savepath = (options.savepath === undefined) ? '/' : options.savepath,
formData = new FormData(),
timestamp = 0,
uploadid = "",
xhr = new XMLHttpRequest(),
imagehtml = "",
keys = Object.keys(options.repositories);
e.preventDefault();
e.stopPropagation();
formData.append('repo_upload_file', e.dataTransfer.files[0]);
formData.append('itemid', options.itemid);
// List of repositories is an object rather than an array. This makes iteration more awkward.
for (var i = 0; i < keys.length; i++) {
if (options.repositories[keys[i]].type === 'upload') {
formData.append('repo_id', options.repositories[keys[i]].id);
break;
}
}
formData.append('env', options.env);
formData.append('sesskey', M.cfg.sesskey);
formData.append('client_id', options.client_id);
formData.append('savepath', savepath);
formData.append('ctx_id', options.context.id);
// Insert spinner as a placeholder.
timestamp = new Date().getTime();
uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
host.focus();
host.restoreSelection();
imagehtml = template({
url: M.util.image_url("i/loading_small", 'moodle'),
alt: M.util.get_string('uploading', COMPONENTNAME),
id: uploadid
});
host.insertContentAtFocusPoint(imagehtml);
self.markUpdated();
// Kick off a XMLHttpRequest.
xhr.onreadystatechange = function() {
var placeholder = self.editor.one('#' + uploadid),
result,
file,
newhtml,
newimage;
if (xhr.readyState === 4) {
if (xhr.status === 200) {
result = JSON.parse(xhr.responseText);
if (result) {
if (result.error) {
if (placeholder) {
placeholder.remove(true);
}
return new M.core.ajaxException(result);
}
file = result;
if (result.event && result.event === 'fileexists') {
// A file with this name is already in use here - rename to avoid conflict.
// Chances are, it's a different image (stored in a different folder on the user's computer).
// If the user wants to reuse an existing image, they can copy/paste it within the editor.
file = result.newfile;
}
// Replace placeholder with actual image.
newhtml = template({
url: file.url,
presentation: true
});
newimage = Y.Node.create(newhtml);
if (placeholder) {
placeholder.replace(newimage);
} else {
self.editor.appendChild(newimage);
}
self.markUpdated();
}
} else {
alert(M.util.get_string('servererror', 'moodle'));
if (placeholder) {
placeholder.remove(true);
}
}
}
};
xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
xhr.send(formData);
}
return false;
},
/**

File diff suppressed because one or more lines are too long

View File

@ -165,6 +165,7 @@ var CSS = {
'{{#if presentation}}role="presentation" {{/if}}' +
'style="{{alignment}}{{margin}}{{customstyle}}"' +
'{{#if classlist}}class="{{classlist}}" {{/if}}' +
'{{#if id}}id="{{id}}" {{/if}}' +
'/>';
Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
@ -206,6 +207,7 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
_rawImageDimensions: null,
initializer: function() {
this.addButton({
icon: 'e/insert_edit_image',
callback: this._displayDialogue,
@ -213,6 +215,121 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDoubleClick, 'img', this);
this.editor.on('drop', this._handleDragDrop, this);
},
/**
* Handle a drag and drop event with an image.
*
* @method _handleDragDrop
* @param {EventFacade} e
* @private
*/
_handleDragDrop: function(e) {
var self = this,
host = this.get('host'),
template = Y.Handlebars.compile(IMAGETEMPLATE);
host.saveSelection();
e = e._event;
// Only handle the event if an image file was dropped in.
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length && /^image\//.test(e.dataTransfer.files[0].type)) {
var options = host.get('filepickeroptions').image,
savepath = (options.savepath === undefined) ? '/' : options.savepath,
formData = new FormData(),
timestamp = 0,
uploadid = "",
xhr = new XMLHttpRequest(),
imagehtml = "",
keys = Object.keys(options.repositories);
e.preventDefault();
e.stopPropagation();
formData.append('repo_upload_file', e.dataTransfer.files[0]);
formData.append('itemid', options.itemid);
// List of repositories is an object rather than an array. This makes iteration more awkward.
for (var i = 0; i < keys.length; i++) {
if (options.repositories[keys[i]].type === 'upload') {
formData.append('repo_id', options.repositories[keys[i]].id);
break;
}
}
formData.append('env', options.env);
formData.append('sesskey', M.cfg.sesskey);
formData.append('client_id', options.client_id);
formData.append('savepath', savepath);
formData.append('ctx_id', options.context.id);
// Insert spinner as a placeholder.
timestamp = new Date().getTime();
uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
host.focus();
host.restoreSelection();
imagehtml = template({
url: M.util.image_url("i/loading_small", 'moodle'),
alt: M.util.get_string('uploading', COMPONENTNAME),
id: uploadid
});
host.insertContentAtFocusPoint(imagehtml);
self.markUpdated();
// Kick off a XMLHttpRequest.
xhr.onreadystatechange = function() {
var placeholder = self.editor.one('#' + uploadid),
result,
file,
newhtml,
newimage;
if (xhr.readyState === 4) {
if (xhr.status === 200) {
result = JSON.parse(xhr.responseText);
if (result) {
if (result.error) {
if (placeholder) {
placeholder.remove(true);
}
return new M.core.ajaxException(result);
}
file = result;
if (result.event && result.event === 'fileexists') {
// A file with this name is already in use here - rename to avoid conflict.
// Chances are, it's a different image (stored in a different folder on the user's computer).
// If the user wants to reuse an existing image, they can copy/paste it within the editor.
file = result.newfile;
}
// Replace placeholder with actual image.
newhtml = template({
url: file.url,
presentation: true
});
newimage = Y.Node.create(newhtml);
if (placeholder) {
placeholder.replace(newimage);
} else {
self.editor.appendChild(newimage);
}
self.markUpdated();
}
} else {
alert(M.util.get_string('servererror', 'moodle'));
if (placeholder) {
placeholder.remove(true);
}
}
}
};
xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
xhr.send(formData);
}
return false;
},
/**

View File

@ -163,6 +163,7 @@ var CSS = {
'{{#if presentation}}role="presentation" {{/if}}' +
'style="{{alignment}}{{margin}}{{customstyle}}"' +
'{{#if classlist}}class="{{classlist}}" {{/if}}' +
'{{#if id}}id="{{id}}" {{/if}}' +
'/>';
Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
@ -204,6 +205,7 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
_rawImageDimensions: null,
initializer: function() {
this.addButton({
icon: 'e/insert_edit_image',
callback: this._displayDialogue,
@ -211,6 +213,121 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDoubleClick, 'img', this);
this.editor.on('drop', this._handleDragDrop, this);
},
/**
* Handle a drag and drop event with an image.
*
* @method _handleDragDrop
* @param {EventFacade} e
* @private
*/
_handleDragDrop: function(e) {
var self = this,
host = this.get('host'),
template = Y.Handlebars.compile(IMAGETEMPLATE);
host.saveSelection();
e = e._event;
// Only handle the event if an image file was dropped in.
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length && /^image\//.test(e.dataTransfer.files[0].type)) {
var options = host.get('filepickeroptions').image,
savepath = (options.savepath === undefined) ? '/' : options.savepath,
formData = new FormData(),
timestamp = 0,
uploadid = "",
xhr = new XMLHttpRequest(),
imagehtml = "",
keys = Object.keys(options.repositories);
e.preventDefault();
e.stopPropagation();
formData.append('repo_upload_file', e.dataTransfer.files[0]);
formData.append('itemid', options.itemid);
// List of repositories is an object rather than an array. This makes iteration more awkward.
for (var i = 0; i < keys.length; i++) {
if (options.repositories[keys[i]].type === 'upload') {
formData.append('repo_id', options.repositories[keys[i]].id);
break;
}
}
formData.append('env', options.env);
formData.append('sesskey', M.cfg.sesskey);
formData.append('client_id', options.client_id);
formData.append('savepath', savepath);
formData.append('ctx_id', options.context.id);
// Insert spinner as a placeholder.
timestamp = new Date().getTime();
uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
host.focus();
host.restoreSelection();
imagehtml = template({
url: M.util.image_url("i/loading_small", 'moodle'),
alt: M.util.get_string('uploading', COMPONENTNAME),
id: uploadid
});
host.insertContentAtFocusPoint(imagehtml);
self.markUpdated();
// Kick off a XMLHttpRequest.
xhr.onreadystatechange = function() {
var placeholder = self.editor.one('#' + uploadid),
result,
file,
newhtml,
newimage;
if (xhr.readyState === 4) {
if (xhr.status === 200) {
result = JSON.parse(xhr.responseText);
if (result) {
if (result.error) {
if (placeholder) {
placeholder.remove(true);
}
return new M.core.ajaxException(result);
}
file = result;
if (result.event && result.event === 'fileexists') {
// A file with this name is already in use here - rename to avoid conflict.
// Chances are, it's a different image (stored in a different folder on the user's computer).
// If the user wants to reuse an existing image, they can copy/paste it within the editor.
file = result.newfile;
}
// Replace placeholder with actual image.
newhtml = template({
url: file.url,
presentation: true
});
newimage = Y.Node.create(newhtml);
if (placeholder) {
placeholder.replace(newimage);
} else {
self.editor.appendChild(newimage);
}
self.markUpdated();
}
} else {
alert(M.util.get_string('servererror', 'moodle'));
if (placeholder) {
placeholder.remove(true);
}
}
}
};
xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
xhr.send(formData);
}
return false;
},
/**