Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paste extension and images #1120

Closed
gnemtsov opened this issue Jun 12, 2016 · 12 comments
Closed

Paste extension and images #1120

gnemtsov opened this issue Jun 12, 2016 · 12 comments
Labels

Comments

@gnemtsov
Copy link

I want to use fantastic extension "paste" to clean user's HTML pasted.

But I have my own paste handler which handles image paste. I upload image to S3 and insert img tag by javascript. If I enable paste extension, then my handler never fires and image is just ignored with error in console (see attachemnt).

What I need is to use my handler, when image is pasted (I can detect it with something like this $.inArray( 'Files', e.clipboardData.types )>-1). And to use medium handler to clean HTML, when 'text/html' content is pasted. Is that possible?
default

@j0k3r j0k3r added the question label Jun 13, 2016
@nmielnik
Copy link
Member

Is your paste handler a medium-editor extension, or something else externally? Can you provide how your implementing this paste handler?

It might be possible to implement a custom paste-handler that extends medium-editor's built-in paste handler, and override the doPaste() method. In there, if you detect a data type of 'Files' you could do your own code, and if you don't, just call MediumEditor.extensions.paste.prototype.doPaste() and let the built-in code run as per usual...but I think seeing what your custom paste handler does could help clarify.

@gnemtsov
Copy link
Author

gnemtsov commented Jun 14, 2016

My paste handler is not medium-editor extension. It is just my code.

 //paste image
    document.getElementById(content_id).addEventListener("paste", function(e) {
        if(e && e.clipboardData && e.clipboardData.items && $.inArray( 'Files', e.clipboardData.types )>-1){
            for (var i = 0; i < e.clipboardData.items.length; i++) {
                if (e.clipboardData.items[i].kind == "file" && e.clipboardData.items[i].type == "image/png") {
                    var imageFile = e.clipboardData.items[i].getAsFile();  // get the blob
                    var key = sysname+'/tasks/'+$('#t_id').text()+'/'+tm_id+'/embedded/' + 
                                  Date.now()+Math.floor(Math.random() * 1000)+'.png';
                    // create an image
                    var image = document.createElement("IMG");
                    image.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
                    image.setAttribute('class', 'loading');
                    image.setAttribute('key', key);
                    image.setAttribute('width', 50);
                    image.id = key.slice(0, -4).replace(/\//g,'');
                    // insert the image
                    var range = window.getSelection().getRangeAt(0);
                    $('#'+content_id).attr('waiting', true);
                    $('#'+content_id).removeClass('medium-editor-placeholder');
                    range.insertNode(image);
                    range.collapse(false);
                    // set the selection to after the image
                    var selection = window.getSelection();
                    selection.removeAllRanges();
                    selection.addRange(range);                  
                    new s3uploaderObject({
                        sysname:              sysname,
                        file:                 imageFile,
                        content_type:         'image/png',
                        content_disposition:  '',
                        key:                  key,
                        policy:               $('#tm_wrap_'+tm_id+'>.content-wrap>.s3-policy').text(),
                        signature:            $('#tm_wrap_'+tm_id+'>.content-wrap>.s3-signature').text(),      
                        onload: function(e, key) {
                            $.post( "ajax_get_s3_url.php", {
                                          t_id: $('#t_id').text(),
                                          key: key
                                      })
                              .done(function( data ) {
                                  image.src = data;
                                  image.onload = function() {
                                      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="'+image.naturalWidth+'" height="'+image.naturalHeight+'"/>';
                                      image.setAttribute('class', '');
                                      image.setAttribute('width', image.naturalWidth);
                                      image.setAttribute('data-dump-image', 'data:image/svg+xml;base64,'+btoa(svg));
                                      $('#'+content_id).attr('waiting', false);
                                  };
                              });
                          }
                        });
                    break;
                }
            } 
            e.preventDefault();
        }
        return false;
    });

My code create dump img with loading animation. Then it uploads the image to s3. When upload finished, src of the img is changed to the url from s3 and uctual image is shown.

For now I have to just disable extentions for my code to work:

        extensions: {
            'imageDragging': {},
            'paste': {},
            'table': new MediumEditorTable()
        }

@nmielnik
Copy link
Member

@gnemtsov I think you could get this to work by implementing your own paste extension and overriding the handlePaste and handlePasteBinPaste methods. Something like this:

1st, break your custom paste handler code into 2 helper functions, one to detect if it's a custom paste, and one to actually handle the logic

// Helper method containing your logic on detecting your special case for paste
var isFilePaste = function (event) {
    return event && 
               event.clipboardData && 
               event.clipboardData.items && 
               $.inArray( 'Files', event.clipboardData.types )>-1;
}
var handleFilePaste = function (event) {
    // ...
    // All your logic for handling your special paste
}

Next, define the custom paste handler and override the 2 methods which handle paste today, allowing you to call into your custom logic, or fallback to the default behavior:

// Define your custom paste handler and override:
// cleanPastedHTML -> true (to enable html cleaning)
// handlePaste -> method which is called for handling paste from the context menu
// handlePasteBinPaste -> method which is called for handling paste from the keyboard
var CustomPasteHandler = MediumEditor.extensions.paste.extend({
    cleanPastedHTML: true,
    handlePaste: function (event) {
        if (isFilePaste(event)) {
            handleFilePaste(event);
            return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    },
    handlePasteBinPaste: function (event) {
        if (isFilePaste(event)) {
           handleFilePaste(event);
           return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    }
});

Finally, pass your custom paste handler into the editor when instantiating.

// Define editor and pass the custom paste handler
var editor = new MediumEditor('.editable', {
    // other options
    extensions: {
        'imageDragging': {},
        'paste': new CustomPasteHandler(),
        'table': new MediumEditorTable()
    }
});

If I understand everything correctly, I think that will work.

@gnemtsov
Copy link
Author

gnemtsov commented Jun 15, 2016

Thanks a lot for help! Really appreciate.

This code works in general. But there are some problems..
I added additional parameter for my handlers since I need one attribute from the div, where image was pasted. Hope it is the right way to get an attribute of the div

var CustomPasteHandler = MediumEditor.extensions.paste.extend({
    cleanPastedHTML: true,
    handlePaste: function (event) {
        if (isFilePaste(event)) {
            handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
            return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    },
    handlePasteBinPaste: function (event) {
        if (isFilePaste(event)) {
           handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
           return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    }
});

So here are the problems

  1. My handler is executed only when Ctrl+V is pressed. While Medium default handler is executed only from mouse context menu. When handlers don't execute there are no messages in console.
  2. When I paste image with Ctrl+V and my handler is executed it seems that I can not operate selection range as it was before. Image is uploaded, and code is executed fine, but image is not inserted (actually nothing happens visually in the div). I've put a console.log(range); in my code to investigate:
            // insert the image
            var range = window.getSelection().getRangeAt(0);
    console.log(range);
            $('#'+content_id).attr('waiting', true);
            $('#'+content_id).removeClass('medium-editor-placeholder');
            range.insertNode(image);
            range.collapse(false);
            // set the selection to after the image
            var selection = window.getSelection();

That's what I get
image

I suppose selection range is not in my div. It is in some div#medium-editor-pastebin-.. div.

@nmielnik
Copy link
Member

For CTRL + V paste, try calling this.removePasteBin() before you run your custom paste-handler code:

    handlePasteBinPaste: function (event) {
        if (isFilePaste(event)) {
           this.removePasteBin();  //  <-- Try Calling This
           handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
           return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    }

this.removePasteBin() should remove the paste-bin element, and restore selection back to the editor element, I think this should work for you.

As for your issue with paste from the context menu, I would try debugging to ensure that your handlePaste method is getting called and that the check for file paste is working.

@gnemtsov
Copy link
Author

gnemtsov commented Jun 18, 2016

Yes! removePasteBin() helped to solve second problem.

Now about the first problem. I put the following console.log() calls to investigate:

var CustomPasteHandler = MediumEditor.extensions.paste.extend({
    cleanPastedHTML: true,
    handlePaste: function (event) {
        console.log(event);
        if (isFilePaste(event)) {
            handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
            return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        console.log('default methode from context-menu');
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    },
    handlePasteBinPaste: function (event) {
        console.log(event);
        if (isFilePaste(event)) {
           this.removePasteBin();
           handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
           return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        console.log('default method from ctr+v');
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    }
});

Here is what I have after that.

Having image in my clipboard
a) Press Ctrl+V. Image is pasted this my own handler - all is fine! Default handler is not executed.
image

b) Press Paste from context menu. My custom code is not excuted. In console I see that event is not the same event as for Ctrl+V. Thats the problem - my code can not work. Default handler executes and does nothing.
image

Having text/html in my clipboard
a) Press Ctrl+V. My code is not executed. Default handler is executed, but it does nothing unfortunately. Text is not inserted, no errors.
image

b) Press Paste from context menu. My code is not executed. Default handler is executed and it pastes as it should. All is fine!
image

It is hard for me to dig into source code of medium editor paste extension. Why default handler does nothing on Ctrl+V with text/html? Why event object is not the same for context menu and for Ctrl+V?

@nmielnik
Copy link
Member

Ah, this is related to 8bd8f8f. Since we use a setTimeout() for some of the paste scenarios we won't always have access to the full event object, we pass a fake event object when editablePaste is triggered to be consistent for all paste events.

You can work around this though by overriding the init method of the paste handler and attaching to paste directly.

Original init method of paste extension
init: function () {
    MediumEditor.Extension.prototype.init.apply(this, arguments);

    if (this.forcePlainText || this.cleanPastedHTML) {
        this.subscribe('editablePaste', this.handlePaste.bind(this));
        this.subscribe('editableKeydown', this.handleKeydown.bind(this));
    }
}

You can see the code attaches to editablePaste for detecting paste on the editor. You'll want to attach to paste directly and not the custom editablePaste event. So you'll want to update your code to do this:

Your paste-handler
var CustomPasteHandler = MediumEditor.extensions.paste.extend({
    cleanPastedHTML: true,
    init: function () {   // <-- override init as well
        MediumEditor.Extension.prototype.init.apply(this, arguments);

        if (this.forcePlainText || this.cleanPastedHTML) {
            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
            this.getEditorElements().forEach(function (element) {
                this.on(element, 'paste', this.handlePaste.bind(this));
            }, this);
        }
    },
    handlePaste: function (event) {
        if (isFilePaste(event)) {
            handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
            return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
    },
    handlePasteBinPaste: function (event) {
        if (isFilePaste(event)) {
           this.removePasteBin();
           handleFilePaste($(this.base.elements[0]).attr('tm_id'), event);
           return;
        }
        // If it's not a file paste, fallback to the default paste handler logic
        MediumEditor.extensions.paste.prototype.handlePasteBinPaste.apply(this, arguments);
    }
});

Also, inside your handlePasteBinPaste method, make sure you're calling handlePasteBinPaste on the paste handler's prototype as the fallback when isFilePaste() returns false (you're calling handlePaste in the code you shared).

I'm thinking those 2 changes should fix everything for you.

@nmielnik
Copy link
Member

@gnemtsov you've helped me find a bug in the paste handler that was introduced recently. If overriding init ends up fixing your issue, then you'll be able to remove that override once #1124 has been merged in and released.

@gnemtsov
Copy link
Author

gnemtsov commented Jun 18, 2016

Well, thanks @nmielnik, it is almost done. All previous problems were solved! But one new has appeared.

My current code looks like this

var AbtaskPasteHandler = MediumEditor.extensions.paste.extend({
    forcePlainText: true,
    cleanPastedHTML: true,
    cleanAttrs: ['class', 'style', 'dir'],
    cleanTags: ['meta'],
    init: function () {
        MediumEditor.Extension.prototype.init.apply(this, arguments);
        if (this.forcePlainText || this.cleanPastedHTML) {
            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
            this.getEditorElements().forEach(function (element) {
                this.on(element, 'paste', this.handlePaste.bind(this));
            }, this);
        }
    },
    handlePaste: function (event) {
        if (isFilePaste(event)) {
            this.removePasteBin();
            handleImagePaste($(this.document.activeElement).attr("tm_id"), event);
            return;
        }
        MediumEditor.extensions.paste.prototype.handlePaste.apply(this, arguments);
        ProcessPastedImages($(this.document.activeElement).attr("tm_id"));
    },
    handlePasteBinPaste: function (event) {
        if (isFilePaste(event)) {
           this.removePasteBin();
           handleImagePaste($(this.document.activeElement).attr("tm_id"), event);
           return;
        }
        MediumEditor.extensions.paste.prototype.handlePasteBinPaste.apply(this, arguments);
        ProcessPastedImages($(this.document.activeElement).attr("tm_id"));
    }
});

It works in all situations. But... I add elements dynamically and attach medium editor: editor.addElements('#tm_content_'+tm_id);. Paste with Ctrl+V works just fine both for images and other content. But paste with context menu fails for dynamically added medium editor elements. I suppose I need to modify init section, but can't figure out how..

@nmielnik
Copy link
Member

@gnemtsov #1124 has been merged to master. If you update your fork to the latest master, and delete your init function, then everything should work (I've updated the base init method to be almost identical to the init we've talked about here).

You can see the new code here

@nmielnik
Copy link
Member

@gnemtsov if you're not maintaining a fork, just update to the latest version of medium-editor (5.21.0) which has the updates to the paste extension you need.

@gnemtsov
Copy link
Author

Sorry for my silence.
I have updated medium script, deleted init section in my custom handler and everything is really cool now! All problems are gone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants