Search code examples
javascriptjquery-mobileknockout.jsclipboardknockout-3.0

Why does copying stop working from the context menu?


I'm having troubles debugging this, I suspect it's some sort of browser security deal.

This is a minimal example of some code that was refactored from having each piece have a clickable icon to copy text, which worked. to introducing a context menu which should be delegating to a view model that was previously responsible for the copying.

What I'm expecting to happen, is for blue, green, and red to output 0, 1, 2 to the clipboard when right clicked and copy is chosen.

However, since I introduced the context menu, things stopped working.

I know there is a restriction on browser exec copy, to User interactions, but surely left clicking on a menu option is a user interaction?

Or have I made a silly mistake that I can't see past?

ClickDirect works, as it binds the function directly to a click handler.

But none of the other divs copy.

var contextMenuVM = new function() {
  var self = this;
  var piece = {};
  var args = [];

  self.show = function(data, event) {
    console.log('showargs:', arguments);
    console.log('showthis:', this);
    event.stopPropagation(true);
    piece = this;
    args = arguments;
    event.stopPropagation();
    var posx = event.clientX + window.pageXOffset; //Left Position of Mouse Pointer
    var posy = event.clientY + window.pageYOffset; //Top Position of Mouse Pointer
    $('#contextMenu').popup('open', {
      x: posx,
      y: posy,
      positionTo: 'origin'
    });
    return false;
  };

  self.clickHandler = function(fn) {
    return function(vm, event) {
      event.stopPropagation();
      event.preventDefault();
      console.log('clickargs:', arguments);
      console.log('clickthis:', this);
      fn.apply(piece, args);
      //$('#contextMenu').popup('close');
      return false;
    };
  };
}();

copyToClipboard = function(pstrText) {

  // create hidden text element, if it doesn't already exist
  var targetId = "_hiddenCopyText_";
  var origSelectionStart, origSelectionEnd;

  // must use a temporary form element for the selection and copy
  var target = document.getElementById(targetId);
  if (!target) {
    target = document.createElement("textarea");
    target.id = targetId;
    document.body.appendChild(target);
  }
  target.textContent = pstrText;
  // select the content
  var currentFocus = document.activeElement;
  target.focus();
  target.setSelectionRange(0, target.value.length);

  // copy the selection
  var succeed;
  try {
    succeed = document.execCommand("copy");
    console.log('succeed:', succeed);
  } catch (e) {
    succeed = false;
    console.log('exception', e);
  }
  // restore original focus
  if (currentFocus && typeof currentFocus.focus === "function") {
    //currentFocus.focus();
  }

  // clear temporary content
  // target.textContent = "";

  return succeed;
};

toolbox = new function() {
  var self = this;
  self.copy = function() {
    console.log('toolboxargs: ', arguments);
    console.log('toolboxthis:', this);
    copyToClipboard(this.number);
  };
}();

var pieceVM = function(number) {
  var self = this;
  self.number = number;
};

var arr = ['bluemenu', 'greenmenu', 'redmenu', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'];

var count = 0;
$(function() {
  $(".piece").each(function() {
    ko.applyBindings(new pieceVM(arr[count++]), this);
  });

  ko.applyBindings(contextMenuVM, document.getElementById('contextMenu'));

});
<!DOCTYPE html>
<html lang="en">

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <link rel="stylesheet" href="https://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css" />
  <script src="https://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js"></script>


</head>

<body>
  <div data-role="page">
    <div id="toolbox"></div>
    <div data-role="content">
      <div style="margin-top:100px">
        demo
        <div class="row">
          <div class="piece col-xs-4 bg-info" data-bind="event:{contextmenu: contextMenuVM.show}">
            blue contextmenu - not working (succeed: true)
          </div>
          <div class="piece col-xs-4 bg-success" data-bind="event:{contextmenu: contextMenuVM.show}">
            green contextmenu - not working (succeed: true)
          </div>
          <div class="piece col-xs-4 bg-danger" data-bind="event:{contextmenu: contextMenuVM.show}">
            red contextmenu - not working (succeed: true)
          </div>
        </div>
        <div class="row">
          <textarea name="_hiddenCopyText_" id="_hiddenCopyText_" cols="30" rows="1"></textarea>
        </div>
        <div class="row">
          <div class="col-xs-12" style="padding-top:5px;">
            <div class="row">
              <div class="col-xs-12">testing (scroll down)</div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-success">(expected: working)</div>
              <div class="result col-xs-3 bg-danger">(result: broken)</div>
              <div class="piece col-xs-6 bg-warning" data-bind="event: {contextmenu: contextMenuVM.show}">
                context menu (same as demo)
              </div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-success">(expected: working)</div>
              <div class="result col-xs-3 bg-danger">(result: broken)</div>
              <div class="piece col-xs-6 bg-warning" data-bind="click: contextMenuVM.show">
                left click menu
              </div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-success">(expected: working)</div>
              <div class="result col-xs-3 bg-success">(result: working)</div>
              <div class="piece col-xs-6 bg-primary" data-bind="click: function(){copyToClipboard('click direct')}">
                click direct copy
              </div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-danger">(expected: broken)</div>
              <div class="result col-xs-3 bg-danger">(result: broken)</div>
              <div class="piece col-xs-6 bg-warning" data-bind="event: {contextmenu: copyToClipboard('context direct')}">
                context direct copy
              </div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-success">(expected: working)</div>
              <div class="result col-xs-3 bg-success">(result: working)</div>
              <div class="piece col-xs-6 bg-primary" data-bind="click: contextMenuVM.clickHandler(function(){copyToClipboard('click indirect')})">
                click indirect
              </div>
            </div>
            <div class="row">
              <div class="expected col-xs-3 bg-danger">(expected: broken)</div>
              <div class="result col-xs-3 bg-danger">(result: broken)</div>
              <div class="piece col-xs-6 bg-warning" data-bind="event: {contextmenu: contextMenuVM.clickHandler(function(){copyToClipboard('context indirect')})}">
                context indirect
              </div>
            </div>
          </div>
        </div>
      </div>

      <div id="contextMenu" class="contextMenu ui-content" data-role="popup" data-theme="c" data-dismissible="true">
        <div title="Copy" data-bind="click: clickHandler(toolbox.copy)"><i style="cursor: pointer;" class="fa fa-copy"></i><span class="contextMenuItemText">Copy</span></div>
        <div title="Copy"><a data-bind="click: clickHandler(toolbox.copy)"><i style="cursor: pointer;" class="fa fa-copy"></i><span class="contextMenuItemText">Copy</span></a></div>
        <div title="Copy" data-bind="event: {click: clickHandler(toolbox.copy)}"><i style="cursor: pointer;" class="fa fa-copy"></i><span class="contextMenuItemText">Copy</span></div>
        <div title="Copy"><a data-bind="event: {click: clickHandler(toolbox.copy)}"><i style="cursor: pointer;" class="fa fa-copy"></i><span class="contextMenuItemText">Copy</span></a></div>
        <a data-bind="event: {click: clickHandler(toolbox.copy)}" class="ui-btn">Copy</a>
        <div title="Copy" data-bind="click: clickHandler(function(){copyToClipboard('click indirect')})">click indirect</div>
      </div>
    </div>
  </div>

</body>

</html>


Solution

  • It stops working because JQuery Mobile Popups prevent focus from going to elements outside the popup.

    The code you are using (which has been popularized in a few places, including stack overflow to copy) relies on a hidden element gaining focus.

    In your case you might be somewhat out of luck since you have a hard coded utility function that assumes focus can be lost.

    If you could edit the utility, I would recommend having multiple of these fields in each of your popups, and either using closest, or provide the id of the element in the popup when you call the copy function.

    Luckily your question doesn't ask for a solution, but why it occurs. Otherwise I wouldn't be able to help you :P. 6 hours spent... Sucks to be us.