// ==UserScript==
// @name TFS Helper
// @namespace http://jonas.ninja
// @version 1.4.1
// @description Adds styles and moves things around so that oft-used functions are easier
// @author @_jnblog
// @match http://*/tfs/DefaultCollection/*/_backlogs/*
// @match http://*/tfs/DefaultCollection/*/_versionControl/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js
// @grant GM_addStyle
// ==/UserScript==
/* jshint -W097 */
/* global $ */
/* jshint asi: true, multistr: true */
'use strict'
var topClass = "makeTfsNotAwful"
$('body').addClass(topClass)
function doEverything(linksPane) {
if ($(linksPane).data('moved')) {
return
}
showLinksPane(linksPane)
stackAllTabs(linksPane)
window.setTimeout(function() {
addTaskIdCopyUtilities(linksPane)
changeDialogBorderColor(linksPane)
}, 250)
}
$(document).on('click', 'input.task-identifier', function clickToCopy(e) {
copy(this, $(this).next('span.copy-message'))
if (e.ctrlKey) {
// if CTRL is held down, copy both the commit ID and a commit message (useful if you have a clipboard manager like Ditto)
var optMessage = 't' + (this.value) + ' ' + $(this).siblings('.info-text').text().replace(/^dev: /i, "")
var that = this
window.setTimeout(function() {
copy(that, $(that).next('span.copy-message'), optMessage)
}, 1000)
}
})
function showLinksPane(linksPane) {
$(linksPane).data('moved', true)
.addClass('linksPanel')
.prepend($("<h3>").addClass('linksPanelHeader')
.text($(linksPane).attr('rawtitle')))
var link = $('a[rawtitle=Links]')
link.closest('td').parent().closest('td').prev().css('width', '30%')
link.closest('td').prepend(linksPane)
link.parent().remove()
}
function changeDialogBorderColor(linksPane) {
// depending on the type of this work item, color the border differently
var dialog = $(linksPane).closest('.workitem-dialog')
var caption = dialog.find('a.caption').text()
if (caption.indexOf('Product Backlog Item ') !== -1) {
dialog.css('border-color', '#009CCC') // blue
} else if (caption.indexOf('Bug ') !== -1) {
dialog.css('border-color', '#CC293D') // red
} else { // Task
dialog.css('border-color', '#E0C252') // yellow
}
}
function stackAllTabs(linksPane) {
var column2table = $(linksPane).closest('table.content')
if (column2table.width() < 882) {
// put att tabPanels in a single column
column2table.parent().siblings().append(column2table).css('width', '100%').children().css('overflow', 'hidden')
window.dispatchEvent(new Event('resize'));
}
}
function addTaskIdCopyUtilities(linksPane) {
$('.workitem-info-bar').find('.info-text-wrapper').each(function() {
var $header = $(this)
if ($header.hasClass('added')) {
return
}
$header.addClass('added')
var id = $header.find('a.caption').text().match(/\d+/)[0]
var $input = $('<input value="' + id + '"/>').addClass('task-identifier')
$header.find('span.info-text').after($('<span>').addClass('copy-message')).after($input)
});
}
function copy(elToCopy, $messageContainer, optionalMessage) {
var $fakeElem = $('<textarea>');
var succeeded
var message = optionalMessage || elToCopy.value
$fakeElem
.css({
position: 'absolute',
left: '-9999px',
top: (window.pageYOffset || document.documentElement.scrollTop) + 'px'
})
.attr('readonly', '')
.val(message)
.appendTo(document.body)
select($fakeElem[0])
try {
succeeded = document.execCommand('copy');
} catch (err) {
succeeded = false;
select(elToCopy)
}
if (succeeded) {
$messageContainer.text('Copied!')
$(elToCopy).css('cursor', 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="415.582" height="415.582" viewBox="0 0 415.582 415.582"><path d="M411.47 96.426l-46.32-46.32c-5.48-5.482-14.37-5.482-19.852 0l-192.95 192.952-82.066-82.064c-5.48-5.482-14.37-5.482-19.85 0l-46.32 46.32c-5.482 5.48-5.482 14.37 0 19.852l138.31 138.31a13.99 13.99 0 0 0 9.927 4.112c3.592 0 7.185-1.37 9.925-4.112l249.195-249.2a14.034 14.034 0 0 0 0-19.85z"/></svg>\')')
} else {
$messageContainer.text('Press Ctrl+C to copy!')
$(elToCopy).css('cursor', 'text')
}
$fakeElem.remove()
$messageContainer.show()
window.setTimeout(function() {
$messageContainer.fadeOut(500)
if (succeeded) {
$(elToCopy).css('cursor', 'pointer')
}
}, 1200)
}
waitForKeyElements("div.tab-page[rawtitle=Links]", doEverything, false)
var modalStyle = '.workitem-dialog { \
left: 10px !important;\
top: 10px !important;\
width: calc(100% - 20px) !important;\
height: calc(100% - 20px) !important;\
border: 4px solid grey;\
box-shadow: gray 0 0 30px 8px;\
box-sizing: border-box;}'
var uiDialogContentStyle = '.ui-dialog-content {height: calc(100% - 59px) !important}'
var otherStyles = '\
.linksPanel {\
display: block !important;\
}\
.linksPanelHeader {\
background-color: #e6e6e6;\
font-size: 11px;\
text-transform: uppercase;\
margin: 0;\
padding: 0 4px 0;\
border: 0;\
white-space: nowrap;\
height: 25px;\
line-height: 2.1;\
}\
input.task-identifier {\
cursor: pointer;\
text-align: center;\
width: 80px;\
margin: 0 16px;\
border: 1px solid #ccc;\
}\
.tbTile {\
width: 100%;\
margin: 0px 0px 3px;\
}\
.subColumn { \
width: calc(50% - 5px); \
margin-right: 5px; \
}\
.linksPanel .grid-cell:not(:only-child) {\
text-indent: 0 !important;\
}\
button.changeset-identifier {\
vertical-align: top;\
line-height: 0;\
padding: 0px 12px;\
height: 22px;\
margin-left: 8px;\
}\
.copy-message {\
font-weight: normal;\
}'
GM_addStyle("." + topClass + " " + modalStyle)
GM_addStyle("." + topClass + " " + uiDialogContentStyle)
GM_addStyle(otherStyles)
function select(element) {
// MIT licensed. Author: @zenorocha
var selectedText;
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
element.focus();
element.setSelectionRange(0, element.value.length);
selectedText = element.value;
} else {
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
function waitForKeyElements(
// CC BY-NC-SA 4.0. Author: BrockA
selectorTxt,
/* Required: The jQuery selector string that
specifies the desired element(s).
*/
actionFunction,
/* Required: The code to run when elements are
found. It is passed a jNode to the matched
element.
*/
bWaitOnce,
/* Optional: If false, will continue to scan for
new elements even after the first match is
found.
*/
iframeSelector
/* Optional: If set, identifies the iframe to
search.
*/
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined")
targetNodes = $(selectorTxt);
else
targetNodes = $(iframeSelector).contents()
.find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each(function() {
var jThis = $(this);
var alreadyFound = jThis.data('alreadyFound') || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound)
btargetsFound = false;
else
jThis.data('alreadyFound', true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey]
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function() {
waitForKeyElements(selectorTxt,
actionFunction,
bWaitOnce,
iframeSelector
);
},
300
);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}