Clipboard
A control to allow you to copy content to the clipboard.
Usage
Example
Clipboard(success: "Copied!", error: "Copy failed!", class: "relative", options: {placement: "top"}) do ClipboardSource(class: "hidden") { span { "Born rich!!!" } } ClipboardTrigger do Link(href: "#", class: "gap-1") do Text(size: :small, class: "text-primary") { "Copy the secret of success!!!" } end end end
Copied!
Copy failed!
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Clipboard
Copied!
Copy failed!
Manual installation
1
Add RubyUI::Clipboard to app/components/ruby_ui/clipboard/clipboard.rb
# frozen_string_literal: true module RubyUI class Clipboard < Base def initialize(options: {}, success: "Copied!", error: "Copy Failed!", **attrs) @options = options @success = success @error = error super(**attrs) end def view_template(&block) div(**attrs) do div(&block) success_popover error_popover end end private def success_popover ClipboardPopover(type: :success) { @success } end def error_popover ClipboardPopover(type: :error) { @error } end def default_attrs { data: { controller: "ruby-ui--clipboard", action: "click@window->ruby-ui--clipboard#onClickOutside", ruby_ui__clipboard_success_value: @success, ruby_ui__clipboard_error_value: @error, ruby_ui__clipboard_options_value: @options.to_json } } end end end
Copied!
Copy failed!
2
Add RubyUI::ClipboardDocs to app/components/ruby_ui/clipboard/clipboard_docs.rb
# frozen_string_literal: true class Views::Docs::Clipboard < Views::Base def view_template component = "Clipboard" div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do render Docs::Header.new(title: "Clipboard", description: "A control to allow you to copy content to the clipboard.") Heading(level: 2) { "Usage" } render Docs::VisualCodeExample.new(title: "Example", context: self) do <<~RUBY Clipboard(success: "Copied!", error: "Copy failed!", class: "relative", options: {placement: "top"}) do ClipboardSource(class: "hidden") { span { "Born rich!!!" } } ClipboardTrigger do Link(href: "#", class: "gap-1") do Text(size: :small, class: "text-primary") { "Copy the secret of success!!!" } end end end RUBY end render Components::ComponentSetup::Tabs.new(component_name: component) render Docs::ComponentsTable.new(component_files(component)) end end end
Copied!
Copy failed!
3
Add RubyUI::ClipboardPopover to app/components/ruby_ui/clipboard/clipboard_popover.rb
# frozen_string_literal: true module RubyUI class ClipboardPopover < Base def initialize(type:, **attrs) @type = type super(**attrs) end def view_template(&block) div( class: "hidden", style: "width: max-content; position: absolute; top: 0; left: 0;", data: {ruby_ui__clipboard_target: clipboard_target} ) do div(**attrs, &block) end end private def clipboard_target case @type when :success "successPopover" when :error "errorPopover" end end def panel_target case @type when :success "successPanel" when :error "errorPanel" end end def default_attrs { data: { state: :open, ruby_ui__clipboard_target: panel_target }, class: "z-50 rounded-md text-sm border bg-background px-2 py-0.5 text-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:fill-mode-forwards data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" } end end end
Copied!
Copy failed!
4
Add RubyUI::ClipboardSource to app/components/ruby_ui/clipboard/clipboard_source.rb
# frozen_string_literal: true module RubyUI class ClipboardSource < Base def view_template(&) div(**attrs, &) end private def default_attrs { data: { ruby_ui__clipboard_target: "source" } } end end end
Copied!
Copy failed!
5
Add RubyUI::ClipboardTrigger to app/components/ruby_ui/clipboard/clipboard_trigger.rb
# frozen_string_literal: true module RubyUI class ClipboardTrigger < Base def view_template(&) div(**attrs, &) end private def default_attrs { data: { ruby_ui__clipboard_target: "trigger", action: "click->ruby-ui--clipboard#copy" } } end end end
Copied!
Copy failed!
6
Add clipboard_controller.js to app/javascript/controllers/ruby_ui/clipboard_controller.js
import { Controller } from "@hotwired/stimulus" import { computePosition, flip, shift } from "@floating-ui/dom"; // Connects to data-controller="accordion" export default class extends Controller { static targets = ['trigger', 'source', 'successPopover', 'successPanel', 'errorPopover', 'errorPanel'] static values = { options: { type: Object, default: {}, }, } disconnect() { // Nothing is left to wait for the exit animation, so apply the pending hide now. if (this.hasSuccessPanelTarget) this.settleExit(this.successPanelTarget); if (this.hasErrorPanelTarget) this.settleExit(this.errorPanelTarget); } copy() { let sourceElement = this.sourceTarget.children[0]; if (!sourceElement) { this.#showErrorPopover(); return; } let textToCopy = sourceElement.tagName === 'INPUT' ? sourceElement.value : sourceElement.innerText; navigator.clipboard.writeText(textToCopy).then(() => { this.#showSuccessPopover(); }).catch(() => { this.#showErrorPopover(); }) } onClickOutside() { this.#hidePopover(this.successPopoverTarget, this.successPanelTarget); this.#hidePopover(this.errorPopoverTarget, this.errorPanelTarget); } #computeTooltip(popoverElement) { computePosition(this.triggerTarget, popoverElement, { placement: this.optionsValue.placement || "top", middleware: [flip(), shift()], }).then(({ x, y }) => { Object.assign(popoverElement.style, { left: `${x}px`, top: `${y}px`, }); }); } #showSuccessPopover() { this.#showPopover(this.successPopoverTarget, this.successPanelTarget); } #showErrorPopover() { this.#showPopover(this.errorPopoverTarget, this.errorPanelTarget); } #showPopover(popover, panel) { this.#computeTooltip(popover); popover.classList.remove("hidden"); panel.dataset.state = "open"; } #hidePopover(popover, panel) { if (popover.classList.contains("hidden")) return; panel.dataset.state = "closed"; this.hideAfterExitAnimation(panel); } afterExit(panel) { const popover = panel === this.successPanelTarget ? this.successPopoverTarget : this.errorPopoverTarget; popover.classList.add("hidden"); } // Overlay exit — the same block in every overlay controller, so keep them in sync. exitAnimationNames = new WeakMap(); hideAfterExitAnimation(animated) { const exitAnimations = animated .getAnimations() .filter((animation) => animation instanceof CSSAnimation); // No exit animation, or no box to run it in: animationend would never fire. if (exitAnimations.length === 0) { this.settleExit(animated); return; } this.exitAnimationNames.set(animated, exitAnimations.map((animation) => animation.animationName)); animated.addEventListener("animationend", this.handleExitAnimationEnd); animated.addEventListener("animationcancel", this.handleExitAnimationEnd); } handleExitAnimationEnd = (event) => { // animationend bubbles — an animated child must not hide its container. if (event.target !== event.currentTarget) return; // Closing mid-open cancels the enter animation; only the exit run settles this. if (!this.exitAnimationNames.get(event.currentTarget)?.includes(event.animationName)) return; this.settleExit(event.currentTarget); }; settleExit(animated) { animated.removeEventListener("animationend", this.handleExitAnimationEnd); animated.removeEventListener("animationcancel", this.handleExitAnimationEnd); // Reopened mid-exit: it is on its way back in, leave it visible. if (animated.dataset.state !== "closed") return; this.afterExit(animated); } }
Copied!
Copy failed!
7
Update the Stimulus controllers manifest file
Importmap!
You don't need to run this command if you are using Importmap
rake stimulus:manifest:update
Copied!
Copy failed!
8
Install @floating-ui/dom Javascript dependency
// with yarn yarn add @floating-ui/dom // with npm npm install @floating-ui/dom // with importmaps bin/importmap pin @floating-ui/dom
Copied!
Copy failed!
Components
| Component | Built using | Source |
|---|---|---|
Clipboard | Phlex | |
ClipboardDocs | Phlex | |
ClipboardPopover | Phlex | |
ClipboardSource | Phlex | |
ClipboardTrigger | Phlex | |
ClipboardController | Stimulus JS |