This replaces clippy.sfw with Javascript for issue #1241pull/1442/head
@@ -7,6 +7,11 @@ r34: { | |||
date: ${project.buildDate} | |||
note: '' | |||
From 1.10.0 on Gitblit requires Java 8 as minimum Java version. | |||
Should you have disabled the Flash-based copy-to-clipboard function because it wasn't working anymore | |||
(web.allowFlashCopyToClipboard = false), you may want to rethink this and enable it again. The configuration | |||
property has the same name, but the mechanism was exchanged. Flash is gone, and a modern JavaScript solution | |||
is now used to copy text directly to the clipboard (via clipboard.js). | |||
'' | |||
html: ~ | |||
text: ~ | |||
@@ -14,7 +19,8 @@ r34: { | |||
fixes: | |||
- Fix crash in Gitblit Authority when users were deleted from Gitblit but still had entries (certificates) in the Authority. | |||
changes: | |||
- Minimum Java required increased to Java 8 | |||
- Minimum Java required increased to Java 8. | |||
- Replaced the Flash-based approach to copy text to the clipboard with a modern JavaScript solution. (issue-1241) | |||
additions: ~ | |||
dependencyChanges: | |||
- update to JavaMail 1.5.6 (pr-1217 by @paladox) | |||
@@ -24,6 +30,7 @@ r34: { | |||
- update to Apache commons-io 2.11.0 | |||
- update to Apache commons-compress 1.22 | |||
- update to libpam4j 1.11 | |||
- added clipboard.js, replacing clippy.swf | |||
contributors: | |||
- paladox | |||
} |
@@ -1076,7 +1076,11 @@ web.allowForking = true | |||
# SINCE 1.2.0 | |||
web.shortCommitIdLength = 6 | |||
# Use Clippy (Flash solution) to provide a copy-to-clipboard button. | |||
# Use a JavaScript browser API to provide a copy-to-clipboard button. | |||
# The clipboard.js library is used to copy text directly to the browser's | |||
# clipboard. | |||
# (This used to be done with a Flash based solution, but has been replaced | |||
# with a modern JavaScript approach, since Flash support is dying out.) | |||
# If false, a button with a more primitive JavaScript-based prompt box will | |||
# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. | |||
# |
@@ -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); | |||
} |
@@ -61,7 +61,7 @@ | |||
<wicket:fragment wicket:id="ownersFragment"> | |||
</wicket:fragment> | |||
</wicket:extend> | |||
</wicket:extend> | |||
</body> | |||
</html> |
@@ -586,20 +586,14 @@ pt push</pre> | |||
<img wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard"></img> | |||
</span> | |||
</wicket:fragment> | |||
<!-- flash-based button-press copy & paste --> | |||
<wicket:fragment wicket:id="clippyPanel"> | |||
<object wicket:message="title:gb.copyToClipboard" style="vertical-align:middle;" | |||
wicket:id="clippy" | |||
width="14" | |||
height="14" | |||
bgcolor="#ffffff" | |||
quality="high" | |||
wmode="transparent" | |||
scale="noscale" | |||
allowScriptAccess="sameDomain"></object> | |||
</wicket:fragment> | |||
<!-- JavaScript automatic copy to clipboard --> | |||
<wicket:fragment wicket:id="clippyPanel"> | |||
<span class="tooltipped tooltipped-n"> | |||
<img class="ctcbtn" wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard" /> | |||
</span> | |||
</wicket:fragment> | |||
</wicket:extend> | |||
</body> |
@@ -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 |
@@ -12,12 +12,14 @@ | |||
<wicket:fragment wicket:id="repositoryUrlFragment"> | |||
<div class="btn-toolbar" style="margin: 0px;"> | |||
<div class="btn-group repositoryUrlContainer"> | |||
<div class="btn-group repositoryUrlContainer tooltipped tooltipped-w"> | |||
<img style="vertical-align: middle;padding: 0px 0px 1px 3px;" wicket:id="accessRestrictionIcon"></img> | |||
<span wicket:id="menu"></span> | |||
<div class="repositoryUrl"> | |||
<span wicket:id="primaryUrl">[repository primary url]</span> | |||
<span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span> | |||
<span class="tooltipped tooltipped-n"> | |||
<span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span> | |||
</span> | |||
</div> | |||
<span class="hidden-phone hidden-tablet repositoryUrlRightCap" wicket:id="primaryUrlPermission">[repository primary url permission]</span> | |||
</div> | |||
@@ -33,7 +35,7 @@ | |||
<wicket:fragment wicket:id="applicationMenusFragment"> | |||
<div class="btn-toolbar" style="margin: 4px 0px 0px 0px;"> | |||
<div class="btn-group" wicket:id="appMenus"> | |||
<div class="btn-group tooltipped tooltipped-w" wicket:id="appMenus"> | |||
<span wicket:id="appMenu"></span> | |||
</div> | |||
</div> | |||
@@ -85,17 +87,9 @@ | |||
</span> | |||
</wicket:fragment> | |||
<!-- flash-based button-press copy & paste --> | |||
<!-- JavaScript automatic copy to clipboard --> | |||
<wicket:fragment wicket:id="clippyPanel"> | |||
<object wicket:message="title:gb.copyToClipboard" style="vertical-align:middle;" | |||
wicket:id="clippy" | |||
width="14" | |||
height="14" | |||
bgcolor="#ffffff" | |||
quality="high" | |||
wmode="transparent" | |||
scale="noscale" | |||
allowScriptAccess="sameDomain"></object> | |||
<img class="ctcbtn" wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard" /> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="workingCopyFragment"> |
@@ -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"); |
@@ -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; | |||
} |
@@ -2408,4 +2408,174 @@ table.filestore-status { | |||
font-weight: 200; | |||
font-size: 1em; | |||
font-variant: normal; | |||
} | |||
} | |||
/* | |||
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) | |||
} |