Source: CopyToClipboard.js

import BaseCustomElement from "./BaseCustomElement"

/** Wraps a `<BUTTON>` that will copy text from another element into the system clipboard.  It will set various attributes on itself
 * to allow styling the states of the process.
 *
 * Overall, the flow is as follows:
 *
 * 1. When the button is clicked, its default is prevented, and the element with the id of the `element` attribute is located.
 * 2. If found, this element gets the `copying` attribute set.
 * 3. If the copy completes successfully:
 *    a. `copying` is removed
 *    b. `copied` is set
 *    c. `copied` is scheduled for removal in 2000ms or the number of milliseconds in the `copied-lifetime` attribute.
 * 4. If the copy failed:
 *    a. `copying` is removed
 *    b. `errored` is set
 *    c. The `brut:copyfailed` event is fired. It's detail contains a `text:` value with the text that was attempted to be copied.
 *
 * The intention is to use these attributes to style whatever UX you want.
 *
 * @property {string} element - ID of the element whose `textContent` is what will be copied to the clipboard.
 * @property {number} copied-lifetime - number of milliseconds to wait before clearing the `copied` attribute after a successful copy.
 * @property {boolean} copying - Set after a copy is initiated, but before it completes
 * @property {boolean} copied - Set after a copy is completed successfully
 * @property {boolean} errored - Set after a copy is fails
 *
 * @fires brut:copyfailed Fired when the copy fails to complete
 *
 * @example
 * <pre><code id="code">dx/exec bin/setup</code></pre>
 * <brut-copy-to-clipboard element="code">
 *   <button>Copy</button>
 * </brut-copy-to-clipboard>
 *
 * @customElement brut-copy-to-clipboard
 */
class CopyToClipboard extends BaseCustomElement {
  static tagName = "brut-copy-to-clipboard"

  static observedAttributes = [
    "element",
    "copied-lifetime",
  ]

  #elementId = null
  #copiedLifetime = 2000

  #copyCode = (event) => {
    event.preventDefault()

    const element = document.getElementById(this.#elementId)

    if (!element) {
      this.logger.info("No element with id %s, so nothing to copy",this.#elementId)
      return
    }
    this.setAttribute("copying",true)

    const text = element.textContent

    navigator.clipboard.writeText(text).then( () => {
      this.setAttribute("copied", true)
    }).catch( (e) => {
      this.setAttribute("errored", true)
      this.dispatchEvent(new CustomEvent("brut:copyfailed", { detail: { text: text }}))
    }).finally( () => {
      this.removeAttribute("copying")
      setTimeout( () => this.removeAttribute("copied"), 2000)
    })
  }

  update() {
    const button = this.querySelector("button")
    if (button) {
      button.addEventListener("click", this.#copyCode)
    }
    else {
      this.logger.info("There is no button, so no way to initiate a copy")
    }
  }

  elementChangedCallback({newValue}) {
    this.#elementId = newValue
  }

  copiedLifetimeChangedCallback({newValue}) {
    const newValueAsInt = parseInt(newValue)
    if (!isNaN(newValueAsInt)) {
      this.#copiedLifetime = newValueAsInt
    }
    else {
      this.logger.info("Value '%s' for copied-lifetime is not a number. Ignoring it",newValue)
    }
  }
}
export default CopyToClipboard