Skip to content

Commit eb82465

Browse files
committed
Add copy-to-clipboard
1 parent 7161065 commit eb82465

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

lib/money/input/visualizer/assets.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,47 @@ defmodule Money.Input.Visualizer.Assets do
417417
overflow-x: auto;
418418
}
419419
420+
/* Copy-to-clipboard icon button — sits in the top-right of a
421+
code panel. The wrapping `.mi-code-wrap` provides the
422+
positioning context so the button anchors to the <pre>
423+
itself (not the surrounding card). */
424+
.mi-code-wrap { position: relative; }
425+
426+
.mi-copy-btn {
427+
position: absolute;
428+
top: 0.5rem;
429+
right: 0.5rem;
430+
width: 1.75rem;
431+
height: 1.75rem;
432+
display: inline-flex;
433+
align-items: center;
434+
justify-content: center;
435+
border: 1px solid var(--mi-border);
436+
background: var(--mi-surface);
437+
color: var(--mi-text-dim);
438+
border-radius: var(--mi-radius-sm);
439+
cursor: pointer;
440+
padding: 0;
441+
transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
442+
}
443+
.mi-copy-btn:hover { color: var(--mi-text); background: var(--mi-surface-2); }
444+
.mi-copy-btn svg {
445+
width: 14px;
446+
height: 14px;
447+
stroke: currentColor;
448+
fill: none;
449+
stroke-width: 2;
450+
stroke-linecap: round;
451+
stroke-linejoin: round;
452+
}
453+
.mi-copy-btn .mi-copy-icon-check { display: none; }
454+
.mi-copy-btn[data-copied="true"] {
455+
color: var(--mi-accent);
456+
border-color: var(--mi-accent);
457+
}
458+
.mi-copy-btn[data-copied="true"] .mi-copy-icon-clipboard { display: none; }
459+
.mi-copy-btn[data-copied="true"] .mi-copy-icon-check { display: inline; }
460+
420461
code, .mi-code-inline {
421462
font-family: var(--mi-mono);
422463
background: var(--mi-surface-2);

lib/money/input/visualizer/input_view.ex

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,42 @@ defmodule Money.Input.Visualizer.InputView do
212212
"<p class=\"mi-desc\">The HEEx call site that renders the money_input above. ",
213213
"Tweak the form controls and the code refreshes — copy straight into a ",
214214
"LiveView template.</p>",
215-
"<pre class=\"mi-code\">",
215+
"<div class=\"mi-code-wrap\">",
216+
"<pre class=\"mi-code\" id=\"money-input-heex\">",
216217
Render.escape(money_code),
217218
"</pre>",
219+
copy_button("#money-input-heex", "Copy HEEx call to clipboard"),
220+
"</div>",
218221
"</section>"
219222
]
220223
end
221224

225+
# Clipboard-icon button anchored to a `.mi-card`. The card itself
226+
# provides the positioning context; the script in render.ex
227+
# handles the click via the `data-mi-copy-target` attribute.
228+
defp copy_button(target_selector, label) do
229+
[
230+
"<button type=\"button\" class=\"mi-copy-btn\" ",
231+
"data-mi-copy-target=\"",
232+
Render.escape(target_selector),
233+
"\" aria-label=\"",
234+
Render.escape(label),
235+
"\" title=\"",
236+
Render.escape(label),
237+
"\">",
238+
# Clipboard icon (visible at rest).
239+
"<svg class=\"mi-copy-icon-clipboard\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">",
240+
"<rect x=\"9\" y=\"3\" width=\"6\" height=\"3\" rx=\"1\"/>",
241+
"<path d=\"M9 4.5H6.5A1.5 1.5 0 0 0 5 6v13.5A1.5 1.5 0 0 0 6.5 21h11a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H15\"/>",
242+
"</svg>",
243+
# Checkmark icon (shown briefly after a successful copy).
244+
"<svg class=\"mi-copy-icon-check\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">",
245+
"<polyline points=\"5 12 10 17 19 7\"/>",
246+
"</svg>",
247+
"</button>"
248+
]
249+
end
250+
222251
defp build_money_call(locale, default_currency, picker, preferred) do
223252
attrs =
224253
[

lib/money/input/visualizer/render.ex

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ defmodule Money.Input.Visualizer.Render do
6464
footer(),
6565
"</main>",
6666
theme_toggle_script(),
67+
clipboard_script(),
6768
"</body></html>"
6869
]
6970
end
@@ -201,6 +202,49 @@ defmodule Money.Input.Visualizer.Render do
201202
"""
202203
end
203204

205+
# Click-delegated clipboard handler. Any button with
206+
# `data-mi-copy-target="#selector"` copies that element's text
207+
# content. Flips `data-copied="true"` on the button for ~1.4s so
208+
# the CSS can swap the clipboard icon for a checkmark, then
209+
# clears it. No external lib — uses navigator.clipboard.writeText.
210+
defp clipboard_script do
211+
"""
212+
<script>
213+
(function () {
214+
document.addEventListener("click", function (event) {
215+
var button = event.target.closest("[data-mi-copy-target]");
216+
if (!button) return;
217+
var selector = button.getAttribute("data-mi-copy-target");
218+
var target = selector && document.querySelector(selector);
219+
if (!target) return;
220+
var text = target.innerText || target.textContent || "";
221+
var done = function () {
222+
button.setAttribute("data-copied", "true");
223+
setTimeout(function () {
224+
button.removeAttribute("data-copied");
225+
}, 1400);
226+
};
227+
if (navigator.clipboard && navigator.clipboard.writeText) {
228+
navigator.clipboard.writeText(text).then(done, function () {
229+
/* permission denied or insecure context — swallow */
230+
});
231+
} else {
232+
// Older-browser fallback via a transient textarea.
233+
var area = document.createElement("textarea");
234+
area.value = text;
235+
area.style.position = "fixed";
236+
area.style.opacity = "0";
237+
document.body.appendChild(area);
238+
area.select();
239+
try { document.execCommand("copy"); done(); } catch (e) {}
240+
document.body.removeChild(area);
241+
}
242+
});
243+
})();
244+
</script>
245+
"""
246+
end
247+
204248
defp error_block(nil), do: ""
205249

206250
defp error_block(message) do

0 commit comments

Comments
 (0)