diff --git a/agent/contact.php b/agent/contact.php
index 7366b5b3a..57aaf75f7 100644
--- a/agent/contact.php
+++ b/agent/contact.php
@@ -413,7 +413,7 @@ if (isset($_GET['contact_id'])) {
if (empty($asset_ip)) {
$asset_ip_display = "-";
} else {
- $asset_ip_display = "$asset_ip";
+ $asset_ip_display = "$asset_ip";
}
$asset_nat_ip = escapeHtml($row['interface_nat_ip']);
$asset_ipv6 = escapeHtml($row['interface_ipv6']);
diff --git a/agent/js/credential_show_password_via_id.js b/agent/js/credential_show_password_via_id.js
index 00c959c74..86dada1a9 100644
--- a/agent/js/credential_show_password_via_id.js
+++ b/agent/js/credential_show_password_via_id.js
@@ -29,10 +29,12 @@ function copyPasswordViaCredentialID(button, credential_id) {
function(data) {
const credential = JSON.parse(data);
- navigator.clipboard.writeText(credential.password).then(function() {
- // Same "Copied!" flash the ClipboardJS handler in app.js uses
- flashTooltip(button, 'Copied!');
- });
+ // itflowCopyText (js/app.js) also works on a plain-HTTP install,
+ // where navigator.clipboard does not exist and this used to throw
+ itflowCopyText(credential.password, button).then(
+ function () { flashTooltip(button, 'Copied!'); },
+ function () { flashTooltip(button, 'Failed!'); }
+ );
}
);
}
\ No newline at end of file
diff --git a/includes/footer.php b/includes/footer.php
index 495f44ea7..df0cb7380 100644
--- a/includes/footer.php
+++ b/includes/footer.php
@@ -68,7 +68,6 @@
-
diff --git a/js/app.js b/js/app.js
index b5e56a563..aeac55883 100644
--- a/js/app.js
+++ b/js/app.js
@@ -645,31 +645,44 @@ function itflowInit() {
});
});
- // Bootstrap 4 needed _enforceFocus patched out so ClipboardJS could reach
- // its textarea inside a modal - TinyMCE's popups needed it too, see
- // itflowTinyMceFocus. Bootstrap 5 registers no jQuery plugin, so
- // the old $.fn.modal line threw and killed everything below it. If copying
- // from inside a modal ever misbehaves, ClipboardJS's `container` option is
- // the lever, not a Bootstrap patch.
-
- // Clipboard
+ // Copy buttons: any element with class="clipboardjs" and its text in
+ // data-clipboard-text. That is ClipboardJS's markup contract, kept so none of
+ // the call sites change - the library itself is no longer used here.
+ //
+ // ClipboardJS copies by selecting a hidden textarea. Under Bootstrap 5 that
+ // textarea has to be inside the open modal or the focus trap pulls focus
+ // back out before the copy runs, so e5d1fe2 pinned ClipboardJS's `container`
+ // to the last .modal on the page. A container is fixed for the life of the
+ // instance, though, and the last .modal was usually the wrong one: a HIDDEN
+ // modal on any page that includes one up front (invoice, credentials, asset,
+ // contact), and a DETACHED one everywhere else once any ajax modal had been
+ // opened and closed - the notifications bell included. A hidden or detached
+ // textarea holds no selection, so nothing was copied, and since ClipboardJS
+ // reports success from the text rather than from the copy itself, all of
+ // those clicks still flashed "Copied!".
+ //
+ // So this resolves everything at click time. Bound once on document, as this
+ // file re-runs on every ajax modal open.
itflowStep('clipboard', function () {
- if (window.itflowClipboard) {
- window.itflowClipboard.destroy();
- }
+ itflowBindOnce('itflowCopy', 'click', '.clipboardjs', function (e) {
+ // Copy Guest URL (invoice, quote) is an - without this
+ // every copy also jumped the page back to the top.
+ e.preventDefault();
- var modals = document.querySelectorAll('.modal');
- var clipboard = new ClipboardJS('.clipboardjs', {
- container: modals.length ? modals[modals.length - 1] : document.body
- });
- window.itflowClipboard = clipboard;
+ const trigger = this;
+ const target = itflowCopyFeedbackTarget(trigger);
+ const text = trigger.getAttribute('data-clipboard-text') || '';
- clipboard.on('success', function(e) {
- flashTooltip(e.trigger, 'Copied!');
- });
+ // An empty field would otherwise wipe whatever is on the clipboard
+ if (text === '') {
+ flashTooltip(target, 'Nothing to copy');
+ return;
+ }
- clipboard.on('error', function(e) {
- flashTooltip(e.trigger, 'Failed!');
+ itflowCopyText(text, trigger).then(
+ function () { flashTooltip(target, 'Copied!'); },
+ function () { flashTooltip(target, 'Failed!'); }
+ );
});
});
@@ -863,6 +876,71 @@ function flashTooltip(button, message) {
}, 1000);
}
+/**
+ * Copy text to the clipboard. Resolves once it is really there and rejects
+ * when it could not be written, so callers can flash "Copied!" or "Failed!"
+ * honestly.
+ *
+ * The async Clipboard API involves no selection and no focus, so no modal's
+ * focus trap can get in its way. Browsers only offer it in a secure context
+ * (HTTPS or localhost), so a plain-HTTP install falls back to a textarea and
+ * execCommand('copy') - placed inside the trigger's own modal, worked out now
+ * rather than in advance.
+ */
+function itflowCopyText(text, trigger) {
+ if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
+ return navigator.clipboard.writeText(text);
+ }
+ return itflowCopyTextLegacy(text, trigger)
+ ? Promise.resolve()
+ : Promise.reject(new Error('Copy failed'));
+}
+
+function itflowCopyTextLegacy(text, trigger) {
+ // The textarea has to be rendered and focusable to hold a selection - a
+ // hidden or detached one is exactly what broke - and inside the open modal,
+ // or Bootstrap's focus trap takes focus back before the copy runs.
+ const host = (trigger && trigger.closest('.modal')) || document.body;
+ const area = document.createElement('textarea');
+ area.value = text;
+ area.setAttribute('readonly', ''); // no on-screen keyboard on touch devices
+ area.style.position = 'fixed';
+ area.style.top = '0';
+ area.style.left = '-9999px';
+ host.appendChild(area);
+ area.focus({ preventScroll: true });
+ area.select();
+ area.setSelectionRange(0, text.length); // iOS ignores select() alone
+
+ let copied = false;
+ try {
+ copied = document.execCommand('copy');
+ } catch (err) {
+ copied = false;
+ }
+ area.remove();
+
+ // Removing the focused textarea leaves focus on - hand it back.
+ if (trigger && trigger.isConnected) {
+ trigger.focus({ preventScroll: true });
+ }
+ return copied;
+}
+
+/**
+ * Where to flash a copy button's "Copied!". Bootstrap closes a dropdown on the
+ * same click, so a dropdown item like Copy Guest URL is hidden by the time the
+ * tooltip is positioned - and a tooltip on a hidden element is drawn in the
+ * top-left corner of the window. Use the dropdown's wrapper instead, which on
+ * invoice and quote is shrink-wrapped around the toggle. Not the toggle itself:
+ * it already holds the Dropdown instance, and Bootstrap refuses a second
+ * instance on one element.
+ */
+function itflowCopyFeedbackTarget(trigger) {
+ const menu = trigger.closest('.dropdown-menu');
+ return (menu && menu.parentElement) || trigger;
+}
+
/*
* The Tom Select layer (initTomSelect / refreshTomSelect / clearTomSelect /
* setTomSelectValue) lives in js/tom_select.js, which footer.php loads right