browser/clipboard.js

/**
 * @module browser/clipboard
 */

/**
 * Copy text to clipboard
 * - This uses the outdated/deprecated "execCommand" for non https environments if 
 *   window.navigator.clipboard is unavailable
 * @param {String} text Text to copy to clipboard
 * @return {Promise} A promise that will resolve once copy is successful or fails
 */
export async function copyText(text) {
  // Unavailable on http or some security environments
  if (navigator.clipboard) {
    return navigator.clipboard.writeText(text)
      .catch(err => {
        console.error(err);
        throw err;
      });
  } else {
    return new Promise((resolve, reject) => {
      try {
        const success = copyTextExecCommand(text);
        if (success) {
          resolve(); 
        } else {
          // If execCommand returns false, it means the copy operation failed.
          reject(new Error("Copy failed using execCommand fallback (e.g., not allowed in current context)."));
        }
      } catch (err) {
        // This catch block is for unexpected JavaScript errors that might be thrown
        console.error("An error occurred during execCommand fallback mechanism:", err); 
        reject(err); 
      }
    });
  }
}

/**
 * Copy text with the old (now deprecated execCommand)
 * - This is used as a fallback in copyTextToClipboard for non https environments
 * - Do not use in modern sites, not recommended
 * @param {String} text Text to copy to clipboard
 */
function copyTextExecCommand(text) {
  const element = document.createElement("textarea");
  element.value = text;
  element.setAttribute("readonly", "");
  element.style.position = "absolute";
  element.style.left = "-9999px";
  document.body.appendChild(element);
  element.select();

  let success = false;
  try {
    success = document.execCommand("copy"); // Capture the return value!
  } catch (err) {
    // Log any unexpected errors during execCommand, then re-throw.
    console.error("Error during document.execCommand('copy'):", err);
    throw err; // Re-throw so the outer Promise's catch can handle it
  } finally {
    // Ensure the element is removed even if execCommand throws an error
    document.body.removeChild(element);
  }
  return success;
}