6

I know that you're not supposed to do blocking in Javascript and I've never been unable to refactor away from having to do that. But I've come across something that I don't know how to handle with callbacks. I'm trying to use Downloadify with html2canvas (this is for IE only, downloading data URIs doesn't work in IE). You have to specify a data function so the Flash object knows what to download. Unfortunately, html2canvas is asynchronous. I need to be able to wait until the onrendered event is filled in before I can get the data URI.

$('#snapshot').downloadify({
        filename: function(){
            return 'timeline.png';
        },
        data: function(){
            var d = null;
            html2canvas($('#timeline'),{
                onrendered:function(canvas){
                    d = canvas.toDataURL();
                }
            });

            //need to be able to block until d isn't null

            return d;
        },
        swf: '../static/bin/downloadify.swf',
        downloadImage: '../static/img/camera_icon_32.png?rev=1',
        width: 32,
        height: 32,
        transparent: true,
        append: false
});

I'm open to suggestions on other ways to do this, but I'm stuck.

EDIT - A couple of comments have made it seem that more information on Downloadify is needed (https://github.com/dcneiner/Downloadify). Downloadify is a Flash object that can be used to trigger a browser's Save As window. The downloadify() function is simply putting initializing the Flash object and sticking an <object/> tag in the element. Since it's a Flash object, you can't trigger events from Javascript without causing a security violation.

I'm using it for IE only to download an image of a Canvas element. In all other browsers, I can just use a data URI, but IE is a special flower.

Jared Farrish
  • 48,585
  • 17
  • 95
  • 104
monitorjbl
  • 4,280
  • 3
  • 36
  • 45
  • 1
    You can't. But can't you just call `downloadify` from the callback for `html2canvas`? – bfavaretto Feb 15 '13 at 01:45
  • No, Downloadify is a Flash object. You can't trigger it externally. – monitorjbl Feb 15 '13 at 01:46
  • 1
    Can you do a button click on the downloadify element when onRender is called? – Terrance Feb 15 '13 at 01:52
  • Sorry, I'm not sure I understand you. I mean wrap the whole `$('#snapshot').downloadify()` thing inside `onrendered`. – bfavaretto Feb 15 '13 at 01:55
  • @Terrance It's a Flash object, you can't interact with it programmatically. That was my first thought as well, and I ran into this: https://github.com/dcneiner/Downloadify/issues/6 – monitorjbl Feb 15 '13 at 01:57
  • @JaredFarrish The data function *has* to return the value that will be saved. There's no way around that. – monitorjbl Feb 15 '13 at 02:07
  • When do you run that code? Is it on page load, or in response to some kind of interaction? I'm not sure you understood what I suggested. – bfavaretto Feb 15 '13 at 02:14
  • Oh I see. That's why I asked, of course; I'm not familiar with the plugin. I don't think you can sensibly, without some kind of crude `sleep`, which is not a great idea. – Jared Farrish Feb 15 '13 at 02:14
  • @bfavaretto It's run on the page load. The downloadify() function creates the Flash in the element specified. The data function is what is run when the user clicks on the . – monitorjbl Feb 15 '13 at 02:17
  • IE is annoying. Could you two-step it (I don't know what `#timeline` has either), and not allow downloadify to run on `#snapshot` if the `canvas` isn't ready? So in essence, `onrendered` would "activate" downloadify on `#snapshot`? – Jared Farrish Feb 15 '13 at 02:27
  • @JaredFarrish that's what I was suggesting. But that would require two user clicks per download. – bfavaretto Feb 15 '13 at 02:29
  • @JaredFarrish #timeline is just a
    . html2canvas renders a DOM element in a , which I then read out into base64 as an image. I don't think there's a way to tie back into that, but I think I can just half-ass this. I leave my code the way it is, and put the 'd' variable one level up in scope. The first time the user clicks on the object, nothing happens. The second time, the canvas variable has been set and it returns that. It means the user will always be one step behind, but I bet IE users are used to that :)
    – monitorjbl Feb 15 '13 at 02:32
  • ... or change your UI to a clear two-step process for IE users. – bfavaretto Feb 15 '13 at 02:34
  • My question, why wait until downloadify has been called to render to canvas? I have no idea where or when that `#timeline` gets it's content, but if the button to save is hidden until `#timeline` is rendered to canvas, and that happens without user interaction (?), then there's only one click. – Jared Farrish Feb 15 '13 at 02:34
  • @bfavaretto's suggestion *isn't* (typo) a bad idea. But if you render `#timeline` to canvas as the same time it's appended to the DOM, AND the download button can't be seen/clicked until then, it is *possible* in some use cases there's only one click. Not knowing a whole lot about downloadify's limitations in showing/hiding/security/etc. – Jared Farrish Feb 15 '13 at 02:36
  • @bfavaretto It's a snapshot button. We want to take a picture when it's clicked. – monitorjbl Feb 15 '13 at 02:51

1 Answers1

5

For the poor soul that spends an entire night trying to get an HTML5 feature to work on IE9, here's what I ended up using. I can sorta-kinda get away with it because we aren't too terribly concerned about IE users getting a less user friendly experience and this is an internal application. But, YMMV.

Basically, Downloadify will do nothing when the return string is blank. So, due to the asynchronous nature of html2canvas's rendering, the first time a user clicks, nothing will happen. The second time (assuming the render is done, if not nothing will continue to happen until it is), the value is not blank and the save proceeds. I use the onCancel and onCoplete callbacks to blank out the value again to ensure that the next time the user tries to save, the image is not too stale.

This doesn't account for the event that the user changes the DOM in some way in between clicks, but I don't know what can be done for that. I'm not at all proud of this, but IE is what it is. It works, which is enough for now.

    var renderedPng = '';
    var rendering = false;

    $('#snapshot').downloadify({
        filename: function(){
            return 'timeline.png';
        },
        data: function(){
            if(!rendering && renderedPng == ''){
                rendering = true;
                html2canvas($('#timeline'),{
                    onrendered:function(canvas){
                        renderedPng = canvas.toDataURL().replace('data:image/png;base64,','');
                        rendering = false;
                    }
                });
            }
            return renderedPng;
        },
        onComplete:function(){
            renderedPng = '';
        },
        onCancel: function(){
            renderedPng = '';
        },
        dataType: 'base64',
        swf: '../static/bin/downloadify.swf',
        downloadImage: '../static/img/camera_icon_32.png?rev=1',
        width: 32,
        height: 32,
        transparent: true,
        append: false
    });
monitorjbl
  • 4,280
  • 3
  • 36
  • 45