From c4fc5b38fa69036eee93758a536f65e3ac8ccb76 Mon Sep 17 00:00:00 2001 From: Florian Zschocke Date: Sun, 20 Nov 2022 18:53:26 +0100 Subject: Replace SWF clippy with clipboardjs on repository page Shockwave Flash is dead. But Gitblit still uses it to copy the repository URLs to the clip board. Which doesn't work anymore since no browser uses Flash anymore, so this has degraded disgracefully. Instead, we can use JavaScript to copy directly to the clipboard, now that there are APIs for it. So replace the use of clippy.swf on the repository page with clipboard.js[1]. This right now only has the functionality to copy to clipboard but now visual feedback, yet. This addresses GH issue #1241. [1] https://clipboardjs.com --- .../java/com/gitblit/wicket/pages/SummaryPage.html | 11 ++++++-- .../gitblit/wicket/panels/RepositoryUrlPanel.html | 12 ++------- .../gitblit/wicket/panels/RepositoryUrlPanel.java | 30 ++++++++++++---------- 3 files changed, 28 insertions(+), 25 deletions(-) (limited to 'src/main/java/com/gitblit') diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html index 2d1b6a56..b6afc301 100644 --- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html +++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html @@ -61,7 +61,14 @@ - - + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html index a537277f..1cadd7d5 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html +++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html @@ -85,17 +85,9 @@ - + - + diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java index 207f1250..2411e750 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.wicket.Component; import org.apache.wicket.RequestCycle; +import org.apache.wicket.behavior.SimpleAttributeModifier; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.image.ContextImage; import org.apache.wicket.markup.html.panel.Fragment; @@ -140,8 +141,7 @@ public class RepositoryUrlPanel extends BasePanel { RepositoryUrl repoUrl = item.getModelObject(); // repository url Fragment fragment = new Fragment("repoUrl", "actionFragment", this); - Component content = new Label("content", repoUrl.url).setRenderBodyOnly(true); - WicketUtils.setCssClass(content, "commandMenuItem"); + Component content = new Label("content", repoUrl.url).setOutputMarkupId(true); fragment.add(content); item.add(fragment); @@ -150,7 +150,7 @@ public class RepositoryUrlPanel extends BasePanel { String tooltip = getProtocolPermissionDescription(repository, repoUrl); WicketUtils.setHtmlTooltip(permissionLabel, tooltip); fragment.add(permissionLabel); - fragment.add(createCopyFragment(repoUrl.url)); + fragment.add(createCopyFragment(repoUrl.url, content.getMarkupId())); } }; @@ -199,13 +199,15 @@ public class RepositoryUrlPanel extends BasePanel { } } - urlPanel.add(new Label("primaryUrl", primaryUrl.url).setRenderBodyOnly(true)); + Label primaryUrlLabel = new Label("primaryUrl", primaryUrl.url); + primaryUrlLabel.setOutputMarkupId(true); + urlPanel.add(primaryUrlLabel); Label permissionLabel = new Label("primaryUrlPermission", primaryUrl.hasPermission() ? primaryUrl.permission.toString() : externalPermission); String tooltip = getProtocolPermissionDescription(repository, primaryUrl); WicketUtils.setHtmlTooltip(permissionLabel, tooltip); urlPanel.add(permissionLabel); - urlPanel.add(createCopyFragment(primaryUrl.url)); + urlPanel.add(createCopyFragment(primaryUrl.url, primaryUrlLabel.getMarkupId())); return urlPanel; } @@ -317,12 +319,13 @@ public class RepositoryUrlPanel extends BasePanel { // command-line String command = substitute(clientApp.command, repoUrl.url, baseURL, user.username, repository.name); Label content = new Label("content", command); + content.setOutputMarkupId(true); WicketUtils.setCssClass(content, "commandMenuItem"); fragment.add(content); repoLinkItem.add(fragment); // copy function for command - fragment.add(createCopyFragment(command)); + fragment.add(createCopyFragment(command, content.getMarkupId())); } }}; appMenu.add(actionItems); @@ -346,16 +349,17 @@ public class RepositoryUrlPanel extends BasePanel { return permissionLabel; } - protected Fragment createCopyFragment(String text) { + protected Fragment createCopyFragment(String text, String target) { if (app().settings().getBoolean(Keys.web.allowFlashCopyToClipboard, true)) { - // clippy: flash-based copy & paste + // javascript: browser JS API based copy to clipboard Fragment copyFragment = new Fragment("copyFunction", "clippyPanel", this); - String baseUrl = WicketUtils.getGitblitURL(getRequest()); - ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf"); - clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text)); - copyFragment.add(clippy); + ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png"); + // Add the ID of the target element that holds the text to copy to clipboard + img.add(new SimpleAttributeModifier("data-clipboard-target", "#"+target)); + copyFragment.add(img); return copyFragment; - } else { + } + else { // javascript: manual copy & paste with modal browser prompt dialog Fragment copyFragment = new Fragment("copyFunction", "jsPanel", this); ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png"); -- cgit v1.2.3 From e4c23697dae5a27ec992608f0778ed7ac47a41e1 Mon Sep 17 00:00:00 2001 From: Florian Zschocke Date: Mon, 21 Nov 2022 01:11:52 +0100 Subject: Add a "Copied" tooltip to the copy-to-clipboard button This is not the ideal version, since the height is too low for the tooltip used for the drop-down menus. Probably has something to do with the container or something. But at least something is there now, even if not the most beautiful. --- .../com/gitblit/wicket/pages/RepositoryPage.java | 5 + .../java/com/gitblit/wicket/pages/SummaryPage.html | 7 - .../gitblit/wicket/panels/RepositoryUrlPanel.html | 10 +- src/main/resources/clipboard/gitblit-ctcbtn.js | 74 +++++++++ src/main/resources/gitblit.css | 172 ++++++++++++++++++++- 5 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/clipboard/gitblit-ctcbtn.js (limited to 'src/main/java/com/gitblit') diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java index 4d30e049..b30aee97 100644 --- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java @@ -171,6 +171,11 @@ public abstract class RepositoryPage extends RootPage { add(searchForm); searchForm.setTranslatedAttributes(); + // load clipboard library to copy repository URL + addBottomScript("../../clipboard/clipboard.min.js"); + // instantiate clipboard + addBottomScript("../../clipboard/gitblit-ctcbtn.js"); + // set stateless page preference setStatelessHint(true); } diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html index b6afc301..8915ecf5 100644 --- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html +++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html @@ -62,13 +62,6 @@ - - - - - \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html index 1cadd7d5..e4b7427a 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html +++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html @@ -12,12 +12,14 @@
-
+
[repository primary url] - + + +
[repository primary url permission]
@@ -33,7 +35,7 @@
-
+
@@ -87,7 +89,7 @@ - + diff --git a/src/main/resources/clipboard/gitblit-ctcbtn.js b/src/main/resources/clipboard/gitblit-ctcbtn.js new file mode 100644 index 00000000..ddb2ddad --- /dev/null +++ b/src/main/resources/clipboard/gitblit-ctcbtn.js @@ -0,0 +1,74 @@ +// Instantiate the clipboarding +var clipboard = new ClipboardJS('.ctcbtn'); + +clipboard.on('success', function (e) { + showTooltip(e.trigger, "Copied!"); +}); + +clipboard.on('error', function (e) { + showTooltip(e.trigger, fallbackMessage(e.action)); +}); + +// Attach events to buttons to clear tooltip again +var btns = document.querySelectorAll('.ctcbtn'); +for (var i = 0; i < btns.length; i++) { + btns[i].addEventListener('mouseleave', clearTooltip); + btns[i].addEventListener('blur', clearTooltip); +} + + +function findTooltipped(elem) { + do { + if (elem.classList.contains('tooltipped')) return elem; + elem = elem.parentElement; + } while (elem != null); + return null; +} + +// Show or hide tooltip by setting the tooltipped-active class +// on a parent that contains tooltipped. Since the copy button +// could be and image, or be hidden after clicking, the tooltipped +// element might be higher in the hierarchy. +var ttset; +function showTooltip(elem, msg) { + let ttelem = findTooltipped(elem); + if (ttelem != null) { + ttelem.classList.add('tooltipped-active'); + ttelem.setAttribute('data-tt-text', msg); + ttset=Date.now(); + } + else { + console.warn("Could not find any tooltipped element for clipboard button.", elem); + } +} + +function clearTooltip(e) { + let ttelem = findTooltipped(e.currentTarget); + if (ttelem != null) { + let now = Date.now(); + if (now - ttset < 500) { + // Give the tooltip some time to display + setTimeout(function(){ttelem.classList.remove('tooltipped-active')}, 1000) + } + else { + ttelem.classList.remove('tooltipped-active'); + } + } + else { + console.warn("Could not find any tooltipped element for clipboard button.", e.currentTarget); + } +} + +// If the API is not supported, at least fall back to a message saying +// that now that the text is selected, Ctrl-C can be used. +// This is still a problem in the repo URL dropdown. When it is hidden, Ctrl-C doesn't work. +function fallbackMessage(action) { + var actionMsg = ""; + if (/Mac/i.test(navigator.userAgent)) { + actionMsg = "Press ⌘-C to copy"; + } + else { + actionMsg = "Press Ctrl-C to copy"; + } + return actionMsg; +} diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css index f7271788..bd2befd3 100644 --- a/src/main/resources/gitblit.css +++ b/src/main/resources/gitblit.css @@ -2408,4 +2408,174 @@ table.filestore-status { font-weight: 200; font-size: 1em; font-variant: normal; -} \ No newline at end of file +} + + +/* + Copy-to-clipboard tooltip styling from Github's primer.css + https://primer.style/css/components/tooltips + Adjusted to not hover but fade-in/out on clipboard events. +*/ + +.tooltipped { + position:relative +} + +.tooltipped:after { + position: absolute; + z-index: 1000000; + padding: 5px 8px; + + font: normal normal 11px/1.5 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; + color: #fff; + background: rgba(42, 42, 42, .8); + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(data-tt-text); + border-radius: 3px; + -webkit-font-smoothing:subpixel-antialiased; + + opacity: 0; + transition: 0.5s opacity; +} + +.tooltipped:before { + position: absolute; + z-index: 1000001; + + width: 0; + height: 0; + color: rgba(42, 42, 42, .8); + pointer-events: none; + content: ""; + border:5px solid transparent; + + opacity: 0; + transition: 0.5s opacity; +} + +.tooltipped-active:before, .tooltipped-active:after { + opacity: 1; + text-decoration:none +} + +.tooltipped-s:after, .tooltipped-se:after, .tooltipped-sw:after { + top: 100%; + right: 50%; + margin-top:5px +} + +.tooltipped-s:before, .tooltipped-se:before, .tooltipped-sw:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color:rgba(42, 42, 42, .8) +} + +.tooltipped-se:after { + right: auto; + left: 50%; + margin-left:-15px +} + +.tooltipped-sw:after { + margin-right:-15px +} + +.tooltipped-n:after, .tooltipped-ne:after, .tooltipped-nw:after { + right: 50%; + bottom: 100%; + margin-bottom:5px +} + +.tooltipped-n:before, .tooltipped-ne:before, .tooltipped-nw:before { + top: -5px; + right: 50%; + bottom: auto; + margin-right: -5px; + border-top-color:rgba(42, 42, 42, .8) +} + +.tooltipped-ne:after { + right: auto; + left: 50%; + margin-left:-15px +} + +.tooltipped-nw:after { + margin-right:-15px +} + +.tooltipped-s:after, .tooltipped-n:after { + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform:translateX(50%) +} + +.tooltipped-w:after { + right: 100%; + bottom: 50%; + margin-right: 5px; + -webkit-transform: translateY(50%); + -ms-transform: translateY(50%); + transform:translateY(50%) +} + +.tooltipped-w:before { + top: 50%; + bottom: 50%; + left: -5px; + margin-top: -5px; + border-left-color:rgba(42, 42, 42, .8) +} + +.tooltipped-e:after { + bottom: 50%; + left: 100%; + margin-left: 5px; + -webkit-transform: translateY(50%); + -ms-transform: translateY(50%); + transform:translateY(50%) +} + +.tooltipped-e:before { + top: 50%; + right: -5px; + bottom: 50%; + margin-top: -5px; + border-right-color:rgba(42, 42, 42, .8) +} + + +.tooltipped-sticky:before, .tooltipped-sticky:after { + display:inline-block +} + + +.fullscreen-overlay-enabled.dark-theme .tooltipped:after { + color: #000; + background:rgba(200, 200, 200, .8) +} + +.fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-s:before, .fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-se:before, .fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-sw:before { + border-bottom-color:rgba(200, 200, 200, .8) +} + +.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-n:before, .fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-ne:before, .fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-nw:before { + border-top-color:rgba(200, 200, 200, .8) +} + +.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-e:before { + border-right-color:rgba(200, 200, 200, .8) +} + +.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-w:before { + border-left-color:rgba(200, 200, 200, .8) +} -- cgit v1.2.3 From f4c4030cf6e192217a53c470674a14f811868b7a Mon Sep 17 00:00:00 2001 From: Florian Zschocke Date: Mon, 21 Nov 2022 20:52:10 +0100 Subject: Replace SWF clippy with clipboard.js on ticket page The ticket page also has some copy-to-clipboard buttons, which get updated to work with JS instead of SWF. --- .../java/com/gitblit/wicket/pages/TicketPage.html | 20 +++++++------------- .../java/com/gitblit/wicket/pages/TicketPage.java | 11 +++++------ 2 files changed, 12 insertions(+), 19 deletions(-) (limited to 'src/main/java/com/gitblit') diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.html b/src/main/java/com/gitblit/wicket/pages/TicketPage.html index 46c0f7ee..fd64b776 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.html +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.html @@ -586,20 +586,14 @@ pt push - - - - - + + + + + + + diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java index 1750b859..d6d188df 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java @@ -102,7 +102,6 @@ import com.gitblit.wicket.panels.CommentPanel; import com.gitblit.wicket.panels.DiffStatPanel; import com.gitblit.wicket.panels.IconAjaxLink; import com.gitblit.wicket.panels.LinkPanel; -import com.gitblit.wicket.panels.ShockWaveComponent; import com.gitblit.wicket.panels.SimpleAjaxLink; /** @@ -1644,12 +1643,12 @@ public class TicketPage extends RepositoryPage { protected Fragment createCopyFragment(String wicketId, String text) { if (app().settings().getBoolean(Keys.web.allowFlashCopyToClipboard, true)) { - // clippy: flash-based copy & paste + // javascript: browser JS API based copy to clipboard Fragment copyFragment = new Fragment(wicketId, "clippyPanel", this); - String baseUrl = WicketUtils.getGitblitURL(getRequest()); - ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf"); - clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text)); - copyFragment.add(clippy); + ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png"); + // Add the ID of the target element that holds the text to copy to clipboard + img.add(new SimpleAttributeModifier("data-clipboard-text", text)); + copyFragment.add(img); return copyFragment; } else { // javascript: manual copy & paste with modal browser prompt dialog -- cgit v1.2.3