a-ono / redmine_ckeditor

Redmine plugin for integration CKEditor
304 stars 140 forks source link

Image paste support for google chrome on wiki pages (working code attached) #11

Closed danshryock closed 11 years ago

danshryock commented 12 years ago

Below is a script that I have put in place to enable pasting images into wiki pages using google chrome.

It listens for paste, uploads the image as an attachment, parses the response html for the url, then inserts an image tag into the editor.

I am not familiar with redmine / rails, so I integrated it in to my install via the theme.js for the skin I am using.

I thought it'd be nice if you could integrate this into the default plugin.

//mime helpers
function mimeBoundary() { return "MULTIPARTMIME--------------" + (new Date).getTime(); }
function buildMultipartMimeMessage(elements, boundary) {
    var CRLF = "\r\n";
    var parts = elements.map(function(element) {
        if (element.type == "file") {
            return (
                'Content-Disposition: form-data; ' +
                'name="' + element.name + '"; ' +
                'filename="'+ element.filename + '"' + CRLF +
                "Content-Type: " + (element.contentType || "application/octet-stream") +
                CRLF + CRLF +
                element.value + CRLF
            );
        } else {
            return (
                'Content-Disposition: form-data; ' +
                'name="' + element.name + '"' + CRLF + CRLF +
                element.value + CRLF
            );
        }
    });

    return (
        "--" + boundary + CRLF +
        parts.join("--" + boundary + CRLF) +
        "--" + boundary + "--" + CRLF
    );
}

Event.observe(document, "dom:loaded", function() {
    var isChrome12 = (parseInt( ( window.navigator.appVersion.match(/Chrome\/(\d+)\./) || [0] )[1]) >= 12)
    var isWikiEdit = window.location.href.indexOf("/wiki") != -1 && window.location.href.indexOf("/edit") != -1;
    if(isChrome12 && isWikiEdit) {
        //patch chrome to support send as binary...
        XMLHttpRequest.prototype.sendAsBinary = XMLHttpRequest.prototype.sendAsBinary || function(datastr) {
            var ui8a = new Uint8Array(datastr.length);
            for (var i = 0; i < datastr.length; i++)
                ui8a[i] = (datastr.charCodeAt(i) & 0xff);
            this.send(ui8a.buffer);
        }

        CKEDITOR.on('instanceCreated', function (e) { e.editor.on("instanceReady", function(e){ BindEditorPaste(e.editor) }); });
    }

});

function BindEditorPaste(editor){
    var clipboardFilename = null;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (xhr.readyState != 4 || xhr.status != 200)
            return;

        var responseHtml = xhr.responseText || "";

        //try to find the domain rooted url to the image so we can insert it
        var match = new RegExp("href=\"(.*)/"+clipboardFilename.replace(".","\\.")).exec(responseHtml);
        if(match != null){
            editor.insertHtml("<img src='"+match[1]+"/"+clipboardFilename+"' alt='"+clipboardFilename+"' title='"+clipboardFilename+"' />");
        } else {
            editor.insertHtml("<img src='"+clipboardFilename+"' alt='"+clipboardFilename+"' title='"+clipboardFilename+"' />");
        }
    }

    editor.document.$.addEventListener("paste",function(evt) {
        var found = false;
        var items = (evt.originalEvent || evt).clipboardData.items;
        for (var i = 0; i < items.length; i++) {
            if (items[i].type.indexOf("image/") == -1)
                continue;

            found = true;

            var authenticity_token = $$('[name="authenticity_token"]')[0].value;
            var uploadUrl = window.location.href.replace("/edit","/add_attachment");
            var description = "from clipboard";
            clipboardFilename = "clipboard-"+(new Date()).getTime()+".png";

            //use manual mime messages instead of using formdata because formdata doesn't support filename yet...
            var reader = new FileReader();
            reader.onload = function(){
                var formFields = [
                    {name:"attachments[1][file]",type:"file",contentType:"image/png",filename:clipboardFilename,value: reader.result},
                    {name:"attachments[1][description]",value: description},
                    {name:"authenticity_token",value:authenticity_token},
                    {name:"commit",value:"Add"}
                ];

                var boundary = mimeBoundary()
                xhr.open("POST", uploadUrl);
                xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
                xhr.sendAsBinary(buildMultipartMimeMessage(formFields,boundary));
            };
            reader.readAsBinaryString(items[i].getAsFile());
            /*
            var formData = new FormData();
            formData.append("attachments[1][file]", items[i].getAsFile(), clipboardFilename);
            formData.append("attachments[1][description]", description);
            formData.append("authenticity_token",authenticity_token)
            formData.append("commit","Add");
            xhr.open("POST", uploadUrl);
            xhr.send(formData);
            */
        }

        if (found) return false;
    });
}
cforce commented 12 years ago

Why does this only work for chrome? What about IE and Fox?

danshryock commented 12 years ago

I have investigated IE, and there are no API's that allow access to image data from the clipboard that I can find. For Firefox, it may be possible, but I haven't looked into it.

danshryock commented 12 years ago

I found that with firefox, the paste command actually creates an image tag with the image data encoded into the src attribute. It is cool that they do this, and theoretically it should work, but for some reason the src attribute is removed when viewing the page, but not when editing.

Even if it would show up in the page, it would also cause some issues for users of older versions of IE. IE7 doesn't support inline image data AFAIK, and IE8 is limited to 32k of data.

It may be possible to detect these image tags with inline data, extract the data to upload it as an attachment and replace the tag with a reference to the attachment. This could be done in either ruby or javascript, but I don't have the time to do it now.

On the subject of IE, I have seen solutions that access the clipboard via java applet. I don't like this approach, but I believe it is the only possible solution at the moment.

cforce commented 12 years ago

I tried to make it work, but it runs into errors, cause i als use prototype.js (http://www.prototypejs.org/)

prototype.js:

function observe(element, eventName, handler) { element = $(element);

var responder = _createResponder(element, eventName, handler);

if (!responder) return element;

if (eventName.include(':')) {
  if (element.addEventListener)
    element.addEventListener("dataavailable", responder, false);
  else {

[LINE5644] element.attachEvent("ondataavailable", responder); has collision with

Event.observe(document, "dom:loaded", function() {

in ur code

Errors:

Resource interpreted as Other but transferred with MIME type undefined. prototype.js:5644Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5653Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5644Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5644Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5644Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5644Uncaught TypeError: Object [object Object] has no method 'attachEvent' prototype.js:5734Uncaught TypeError: Object [object Object] has no method 'dispatchEvent' prototype.js:828Uncaught TypeError: Object [object Object] has no method 'attachEvent'

Please help!

danshryock commented 12 years ago

I'm using the bitnami redmine 1.2.2 stack, the only version of prototype.js that I have installed is the one that came by default. Did you change your version of prototype, or do you have a different version of redmine maybe?

cforce commented 12 years ago

I am using a own theme with theme.js in javascripts theme folder which look like this. The js includes jquery to make a slidein slide out of the redmine sidebar.

// ---------------------------------------------------------------------------- // Image paste support for google chrome on wiki pages // https://github.com/a-ono/redmine_ckeditor/issues/11

//mime helpers function mimeBoundary() { return "MULTIPARTMIME--------------" + (new Date).getTime(); } function buildMultipartMimeMessage(elements, boundary) { var CRLF = "\r\n"; var parts = elements.map(function(element) { if (element.type == "file") { return ( 'Content-Disposition: form-data; ' + 'name="' + element.name + '"; ' + 'filename="'+ element.filename + '"' + CRLF + "Content-Type: " + (element.contentType || "application/octet-stream") + CRLF + CRLF + element.value + CRLF ); } else { return ( 'Content-Disposition: form-data; ' + 'name="' + element.name + '"' + CRLF + CRLF + element.value + CRLF ); } });

return (
    "--" + boundary + CRLF +
    parts.join("--" + boundary + CRLF) +
    "--" + boundary + "--" + CRLF
);

}

Event.observe(document, "dom:loaded", function() { var isChrome12 = (parseInt( ( window.navigator.appVersion.match(/Chrome\/(\d+)./) || [0] )[1]) >= 12) var isWikiEdit = window.location.href.indexOf("/wiki") != -1 && window.location.href.indexOf("/edit") != -1; if(isChrome12 && isWikiEdit) { //patch chrome to support send as binary... XMLHttpRequest.prototype.sendAsBinary = XMLHttpRequest.prototype.sendAsBinary || function(datastr) { var ui8a = new Uint8Array(datastr.length); for (var i = 0; i < datastr.length; i++) ui8a[i] = (datastr.charCodeAt(i) & 0xff); this.send(ui8a.buffer); }

    CKEDITOR.on('instanceCreated', function (e) { e.editor.on("instanceReady", function(e){ BindEditorPaste(e.editor) }); });
}

});

function BindEditorPaste(editor){ var clipboardFilename = null; var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if (xhr.readyState != 4 || xhr.status != 200) return;

    var responseHtml = xhr.responseText || "";

    //try to find the domain rooted url to the image so we can insert it
    var match = new RegExp("href=\"(.*)/"+clipboardFilename.replace(".","\\.")).exec(responseHtml);
    if(match != null){
        editor.insertHtml("<img src='"+match[1]+"/"+clipboardFilename+"' alt='"+clipboardFilename+"' title='"+clipboardFilename+"' />");
    } else {
        editor.insertHtml("<img src='"+clipboardFilename+"' alt='"+clipboardFilename+"' title='"+clipboardFilename+"' />");
    }
}

editor.document.$.addEventListener("paste",function(evt) {
    var found = false;
    var items = (evt.originalEvent || evt).clipboardData.items;
    for (var i = 0; i < items.length; i++) {
        if (items[i].type.indexOf("image/") == -1)
            continue;

        found = true;

        var authenticity_token = $$('[name="authenticity_token"]')[0].value;
        var uploadUrl = window.location.href.replace("/edit","/add_attachment");
        var description = "from clipboard";
        clipboardFilename = "clipboard-"+(new Date()).getTime()+".png";

        //use manual mime messages instead of using formdata because formdata doesn't support filename yet...
        var reader = new FileReader();
        reader.onload = function(){
            var formFields = [
                {name:"attachments[1][file]",type:"file",contentType:"image/png",filename:clipboardFilename,value: reader.result},
                {name:"attachments[1][description]",value: description},
                {name:"authenticity_token",value:authenticity_token},
                {name:"commit",value:"Add"}
            ];

            var boundary = mimeBoundary()
            xhr.open("POST", uploadUrl);
            xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
            xhr.sendAsBinary(buildMultipartMimeMessage(formFields,boundary));
        };
        reader.readAsBinaryString(items[i].getAsFile());
        /*
        var formData = new FormData();
        formData.append("attachments[1][file]", items[i].getAsFile(), clipboardFilename);
        formData.append("attachments[1][description]", description);
        formData.append("authenticity_token",authenticity_token)
        formData.append("commit","Add");
        xhr.open("POST", uploadUrl);
        xhr.send(formData);
        */
    }

    if (found) return false;
});

}

// ---------------------------------------------------------------------------- eof

// jQuery slider js document.write("<script type=\"text/javascript\"> if (typeof jQuery!=\"undefined\" && $==jQuery) jQuery.noConflict(); "); document.write("<script type=\"text/javascript\">"); document.write("jQuery(document).ready(function($){"); document.write("$('.slide-out-div').tabSlideOut({"); document.write("tabHandle: '.handle',"); document.write("pathToTabImage: '/themes/dsv-slider/images/sidebar.png',"); document.write("imageHeight: '122px',"); document.write("imageWidth: '20px', "); document.write("tabLocation: 'right',"); document.write("speed: 300,"); document.write("action: 'click',"); document.write("topPos: 'absolut',"); document.write("fixedPosition: true"); document.write("});"); document.write("});");

document.write("");

danshryock commented 12 years ago

I ran into odd issues trying to include jquery in my theme.js file. The noConflict option didn't help, I had to download an unminified version of jquery and remove the assignment to window.$ near the end of the file.

Hopefully this will help your problems.

cforce commented 12 years ago

My theme code is working fine with regular jquery, but as ssons as i add the image paste support for chrome i run into the problem. Which line i shall remove, in don't exactly understand what the cause is? .

danshryock commented 12 years ago

The problem is caused by both jquery and prototype.js using $ as a function to select elements. The code for the chrome paste is trying to use prototype.js, and the error looks like it is probably due protype failing after jquery replaces the $ function.

a-ono commented 11 years ago

I'm sorry for no reply. Image uploads was supported in version 1.0.0 and I have no plan to support it.