2

I have the following TypeScript defining a KnockoutJS binding handler for a clickable element on my view:

module MyModule {
    export interface ICopyButtonParams {
        dataUrl: string;
    }

    ko.bindingHandlers.copyButton = {
        init: (element: HTMLElement, valueAccessor: () => ICopyButtonParams) => {
            var options: any = ko.utils.unwrapObservable(valueAccessor());
            if (!options.dataUrl) {
                return;
            }

            new Clipboard(element, {
                text: () => {
                    var clipboardData: string;

                    $.ajax({
                        url: options.dataUrl,
                        type: "GET",
                        contentType: "application/json",
                        cache: false,
                        async: false,
                        success: (result: SubmitResult) => {
                            clipboardData = result.Data;
                        }
                    });

                    return clipboardData;
                }
            });
        }
    };
}

What this binding handler does is it turns the clickable element into a Clipboard.JS enabled element that stores a string in the clipboard when clicked. In my case, I want to make use of Clipboard.JS's dynamic text functionality whereby you pass a function into the Clipboard constructor that returns the text you want to store in the clipboard. Inside this function, I want to make a call to an API that returns the text to store.

Because of the nature of this architecture, I am not able to use a standard ajax call with a success callback as that would mean that the clipboard text would not be resolved in time.

As a stopgap remedy, you will notice in my code that I have made my ajax call asynchronous (bad, I know). As the 'async' flag is deprecated from JQuery 1.8 onward, I am trying to think of an alternative approach.

Any ideas?

aleonj
  • 163
  • 2
  • 7
  • 1
    Why don't you first call an async ajax and then in the success callback you can create Cliboard object? – mirage May 17 '16 at 12:49
  • The whole point is that I don't want to retrieve the data to put in the clipboard unless it's needed (i.e. someone clicks on the 'Copy to Clipboard' button). There could be potentially 100+ things on the page that can be copied to clipboard, each containing many characters of text. If I'm going to pre-initialise every clipboard button with the data it should store I might as well retrieve the clipboard text with the rest of the view model and do away with the ajax call entirely. It's worth pointing out that what is being copied to the clipboard is never something that is displayed on the page. – aleonj May 17 '16 at 13:32
  • Maybe I do not understand you right, but you are only supposed to put data-bind="copyButton: {}" on the elements you want to behave this way, or? – mirage May 17 '16 at 13:50
  • Correct, but then I only want those elements to load their corresponding clipboard text when they are clicked. – aleonj May 17 '16 at 13:56
  • In this case I would probably just create a function that would be called on click binding on the button for example. – mirage May 17 '16 at 14:07
  • Without seeing the rest of your proposal, I'm not sure how this would help as it's not the binding handler that's causing the issue, but rather it's the architecture of the Clipboard.js library. Can you elaborate, please? – aleonj May 17 '16 at 15:18
  • I should point out that the Clipboard.js library manages its own click event handlers, so I am not able to intercept the click event and do things before introducing Clipboard.js. – aleonj May 17 '16 at 15:19

1 Answers1

2

I think that a better approuch will be to handle click event by your own.

Then on your ajax callback create a textarea, set value, select and call document.execCommand('copy') as Clipboard.JS does. Something similar to this (sorry for javascript instead on typescript)

ko.bindingHandlers.copyButton = {
  init: function(element, valueAccessor) {
    var url = ko.utils.unwrapObservable(valueAccessor());

    $(element).click(function() {
      $.ajax({
        url: url,
        type: "GET",
        contentType: "application/json",
        cache: false,
        async: false,
        success: function(result) {
          var ta = document.createElement('textarea');
          document.body.appendChild(ta);
          ta.value = result;
          ta.select();
          var r = document.createRange();
          r.selectNode(ta);
          document.getSelection().addRange(r);
          document.execCommand('copy');
          document.body.removeChild(ta);
        }
      });
    });
  }
};

I have a similar working example here (without ajax request)

AldoRomo88
  • 2,056
  • 1
  • 17
  • 23
  • 1
    Yes, I came round to this approach myself in the end. Clipboard.JS's design is too prescriptive. It was nice that it was wrapping all the "hacky" stuff such as creating textareas, but it's not worth the hassle. Thanks for your reply! – aleonj May 26 '16 at 13:32
  • This approach doesn't work in Firefox anymore, resulting in error: `document.execCommand(‘cut’/‘copy’) was denied because it was not called from inside a short running user-generated event handler.` Having `"clipboardWrite"` permission may resolve that issue, according to https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard – Michael Rush Jan 08 '20 at 01:21