// ==UserScript== // @name GitHub Tweaks // @namespace noplanman // @description Userscript that adds tweaks to GitHub. // @include https://github.com* // @version 2.9.0 // @author Armando Lüscher // @oujs:author noplanman // @copyright 2019 Armando Lüscher // @grant GM_addStyle // @require https://code.jquery.com/jquery-1.12.4.min.js // @homepageURL https://github.com/noplanman/GitHub-Tweaks // @supportURL https://github.com/noplanman/GitHub-Tweaks/issues // ==/UserScript== (function (jQuery) { "use strict"; /** * Main GitHub Tweaks object. * * @type {Object} */ var GHT = {}; /** * Enable debug mode? * * @type {Boolean} */ GHT.debug = false; /** * Add a click event to the body tag. * * @param id * @param selector * @param cb */ GHT.addClickEvent = function (id, selector, cb) { jQuery('body') .not('.GHT-' + id) .addClass('GHT-' + id) .on('click', selector, cb); }; /** * Allow collapsing and expanding files in diff views. */ GHT.addToggleableFileDiffs = function () { var i = 0; // Get all the possible toolbars, to check which one we're using on this page. var $compare_toolbar = jQuery('#files_bucket #diff .BtnGroup.float-right'); var $pr_toolbar = jQuery('#files_bucket .pr-toolbar .diffbar .pr-review-tools'); var $commit_toolbar = jQuery('#toc .BtnGroup.float-right'); var has_tfd = $compare_toolbar.length || $pr_toolbar.length || $commit_toolbar.length; var $file_headers = jQuery('#files.diff-view .file-header').not('.GHT'); $file_headers.each(function () { var $file_header = jQuery(this).addClass('GHT'); $file_header.next().addClass('foldable-file-diff'); // When clicking the file actions, don't toggle the file contents. var $file_actions = $file_header.find('.file-actions'); $file_actions.click(function (event) { event.stopPropagation(); }); // Delete the default GitHub folding button, which has a different behaviour. $file_actions.find('button.js-details-target').remove(); i++; }); if (has_tfd) { GHT.addClickEvent('tfd', '#files.diff-view .file-header', function () { jQuery(this).next().toggle(); }); } GHT.debug && console.log('addToggleableFileDiffs: ' + i); if (has_tfd && !jQuery('.GHT-btn-group').length) { // Add collapse / expand buttons... var $folding_buttons = GHT.getFoldUnfoldButtons('tfd', '.foldable-file-diff', 'diffbar-item', 's'); // ...to PR toolbar. if ($pr_toolbar.length) { $pr_toolbar.prepend($folding_buttons); } // ...to compare toolbar. if ($compare_toolbar.length) { $folding_buttons.addClass('float-right'); $compare_toolbar.after($folding_buttons); } // ...to commit toolbar. if ($commit_toolbar.length) { $folding_buttons.addClass('float-right'); $commit_toolbar.after($folding_buttons); } } }; /** * Allow collapsing and expanding of comments. */ GHT.addToggleableComments = function () { var i = 0; var $comment_headers = jQuery('.timeline-comment-header').not('.GHT'); $comment_headers.each(function () { var $comment_header = jQuery(this).addClass('GHT'); $comment_header.next().addClass('foldable-comment'); // When clicking the comment header links, don't toggle the comment contents. $comment_header.find('.timeline-comment-header-text a').click(function (event) { event.stopPropagation(); }); // Set the mouse hover title of the header to the comment body to easily browse folded comments. var content = $comment_header.next('.edit-comment-hide').find('.comment-body').text().trim(); var content_slice = content.slice(0, 111); var $comment_header_text = $comment_header.find('.timeline-comment-header-text'); GHT.tooltipify($comment_header_text, 's', content_slice + ((content > content_slice) ? '...' : '')); // Don't show tooltip yet, only when content is hidden! $comment_header_text.removeClass('tooltipped'); // Show/hide the content tooltip in the header. $comment_header.next('.edit-comment-hide') .on('show', function () { $comment_header_text.removeClass('tooltipped'); }) .on('hide', function () { $comment_header_text.addClass('tooltipped'); }); // Add north-oriented tooltips to all reaction buttons. GHT.tooltipify($comment_header.find('.timeline-comment-actions button'), 'n'); i++; }); GHT.addClickEvent('tc', '.timeline-comment-header', function (event) { var $comment_header = jQuery(this); var $target = jQuery(event.target); if (!$target.closest('.timeline-comment-actions').length) { // Comment header toggles the comment. GHT.toggleShowHide($comment_header.nextAll(), !$comment_header.next(':visible').length); } else if ($target.hasClass('js-comment-edit-button')) { // Edit button shows the comment. $comment_header.nextAll('.foldable-comment').css('display', ''); $comment_header.nextAll('.js-comment-update').show(); } else if ($target.hasClass('timeline-comment-action')) { // Add Reaction button shows the reactions. $comment_header.nextAll('.comment-reactions').show(); } }); GHT.debug && console.log('addToggleableComments: ' + i); if (!jQuery('.GHT-btn-group').length) { var $folding_buttons = GHT.getFoldUnfoldButtons('tc', '.foldable-comment, .comment-reactions', '', 'n'); // Fix for non logged-in users, as there are no other actions that fix the positioning. $folding_buttons.css('padding', '8px 5px'); jQuery('.timeline-comment-actions') .first() .prepend($folding_buttons); } }; /** * Add a permalink for the current page to the user links at the top right. */ GHT.addPagePermalink = function () { var $permalink = $('#GHT-permalink'); var permalink = $('.js-permalink-shortcut').attr('href') || location.href; if ($permalink.length) { $permalink.attr('href', permalink); return; } $permalink = $('', { 'id': 'GHT-permalink', 'href': permalink, 'class': 'Header-link', 'html': GHT.getOcticon('link') }); GHT.tooltipify($permalink, 's', 'Permalink'); $('.notification-indicator').parent().before( $('
', { 'class': 'Header-item position-relative', 'html': $permalink }) ); }; /** * Start the party. */ GHT.init = function () { GHT.initCustomTriggers(); // Add the global CSS rules. GM_addStyle( '.GHT-btn-group { display: inline-block; }' + '.GHT.user-select-contain { cursor: pointer !important; }' ); var featureFunctions = [ GHT.addPagePermalink, GHT.addToggleableFileDiffs, GHT.addToggleableComments ]; featureFunctions.forEach(function (ff) { ff(); }); // Load all the features. GHT.Observer.add('body', featureFunctions); }; /************ * HELPERS! * ************/ /** * The MutationObserver to detect page changes. * * @type {Object} */ GHT.Observer = { /** * The mutation observer objects. * * @type {Array} */ observers: [], /** * Add an observer to observe for DOM changes. * * @param {String} queryToObserve Query string of elements to observe. * @param {Array|Function} cbs Callback function(s) for the observer. */ add: function (queryToObserve, cbs) { // Check if we can use the MutationObserver. if ('MutationObserver' in window) { var toObserve = document.querySelector(queryToObserve); if (toObserve) { if (!jQuery.isArray(cbs)) { cbs = [cbs]; } cbs.forEach(function (cb) { var mo = new MutationObserver(cb); // Observe child changes. mo.observe(toObserve, { childList: true, subtree: true }); GHT.Observer.observers.push(mo); }); } } } }; /** * Show / Hide / Toggle helper. * * @param {jQuery} $objects Object(s) to modify. * @param {Boolean|undefined} state State to set to (true=show, false=hide, undefined=toggle). */ GHT.toggleShowHide = function ($objects, state) { switch (state) { case true: $objects.show(); break; case false: $objects.hide(); break; default: $objects.toggle(); } }; /** * Add a pretty tooltip to the passed items. * * @param {jQuery} $items Items to tooltipify. * @param {String} ttdir Direction of the tooltip. * @param {String} title Static title to override current values. */ GHT.tooltipify = function ($items, ttdir, title) { ttdir = ttdir ? (' tooltipped-' + ttdir) : ''; title = title || ''; $items.each(function () { var $t = jQuery(this).addClass('tooltipped' + ttdir); // Override title text? if (title === '') { title = $t.attr('aria-title'); if (!$t.attr('aria-title') && $t.attr('title')) { title = $t.attr('title'); } } $t.attr('aria-label', title); $t.attr('title', ''); }); }; /** * Get a container with both the fold and unfold buttons. * * @param {String} fubid ID of the fold / unfold button group. * @param {String} selector Selector for items to fold / unfold. * @param {String} classes Class(es) to add to the container. * @param {String} ttdir Direction of the tooptip. * * @return {jQuery} Buttons container. */ GHT.getFoldUnfoldButtons = function (fubid, selector, classes, ttdir) { ttdir = ttdir ? ('tooltipped-' + ttdir) : ''; GHT.addClickEvent('fub-' + fubid, '.GHT-btn-group', function (event) { var $target = jQuery(event.target); if ($target.closest('.GHT-fold-button').andSelf().hasClass('GHT-fold-button')) { jQuery(selector).hide(); } else if ($target.closest('.GHT-unfold-button').andSelf().hasClass('GHT-unfold-button')) { jQuery(selector).show(); } }); return jQuery('
', {class: 'GHT-btn-group'}) .addClass(classes || '') .append( GHT.getFoldButton().addClass(ttdir), GHT.getUnfoldButton().addClass(ttdir) ); }; /** * Get fold button. * * @return {jQuery} The fold button. */ GHT.getFoldButton = function () { return jQuery('
', { html: GHT.getOcticon('fold'), class: 'GHT-fold-button btn btn-sm tooltipped' }).attr('aria-label', 'Collapse All'); }; /** * Get unfold button. * * @return {jQuery} The unfold button. */ GHT.getUnfoldButton = function () { return jQuery('
', { html: GHT.getOcticon('unfold'), class: 'GHT-unfold-button btn btn-sm tooltipped' }).attr('aria-label', 'Expand All'); }; /** * Get svg element of octicon. * * @param {String} icon Icon name. * @param {Number} height Icon height to get. * @param {Number} width Icon width to get. * * @return {String} Icon svg element as string. */ GHT.getOcticon = function (icon, height, width) { icon = icon || 'octoface'; height = height || 16; width = width || 16; return ''; }; /** * All GitHub icon SVG paths. * * https://raw.githubusercontent.com/primer/octicons/master/build/sprite.octicons.svg * * @type {Object} */ GHT.octicons = { 'alert': { 'viewbox': '0 0 16 16', 'path': '' }, 'arrow-down': {'viewbox': '0 0 10 16', 'path': ''}, 'arrow-left': {'viewbox': '0 0 10 16', 'path': ''}, 'arrow-right': {'viewbox': '0 0 10 16', 'path': ''}, 'arrow-small-down': {'viewbox': '0 0 6 16', 'path': ''}, 'arrow-small-left': {'viewbox': '0 0 6 16', 'path': ''}, 'arrow-small-right': {'viewbox': '0 0 6 16', 'path': ''}, 'arrow-small-up': {'viewbox': '0 0 6 16', 'path': ''}, 'arrow-up': {'viewbox': '0 0 10 16', 'path': ''}, 'beaker': { 'viewbox': '0 0 16 16', 'path': '' }, 'bell': { 'viewbox': '0 0 14 16', 'path': '' }, 'bold': { 'viewbox': '0 0 10 16', 'path': '' }, 'book': { 'viewbox': '0 0 16 16', 'path': '' }, 'bookmark': { 'viewbox': '0 0 10 16', 'path': '' }, 'briefcase': { 'viewbox': '0 0 14 16', 'path': '' }, 'broadcast': { 'viewbox': '0 0 16 16', 'path': '' }, 'browser': { 'viewbox': '0 0 14 16', 'path': '' }, 'bug': { 'viewbox': '0 0 16 16', 'path': '' }, 'calendar': { 'viewbox': '0 0 14 16', 'path': '' }, 'check': {'viewbox': '0 0 12 16', 'path': ''}, 'checklist': { 'viewbox': '0 0 16 16', 'path': '' }, 'chevron-down': { 'viewbox': '0 0 10 16', 'path': '' }, 'chevron-left': { 'viewbox': '0 0 8 16', 'path': '' }, 'chevron-right': { 'viewbox': '0 0 8 16', 'path': '' }, 'chevron-up': { 'viewbox': '0 0 10 16', 'path': '' }, 'circle-slash': { 'viewbox': '0 0 14 16', 'path': '' }, 'circuit-board': { 'viewbox': '0 0 14 16', 'path': '' }, 'clippy': { 'viewbox': '0 0 14 16', 'path': '' }, 'clock': { 'viewbox': '0 0 14 16', 'path': '' }, 'cloud-download': { 'viewbox': '0 0 16 16', 'path': '' }, 'cloud-upload': { 'viewbox': '0 0 16 16', 'path': '' }, 'code': { 'viewbox': '0 0 14 16', 'path': '' }, 'comment': { 'viewbox': '0 0 16 16', 'path': '' }, 'comment-discussion': { 'viewbox': '0 0 16 16', 'path': '' }, 'credit-card': { 'viewbox': '0 0 16 16', 'path': '' }, 'dash': {'viewbox': '0 0 8 16', 'path': ''}, 'dashboard': { 'viewbox': '0 0 16 16', 'path': '' }, 'database': { 'viewbox': '0 0 12 16', 'path': '' }, 'desktop-download': { 'viewbox': '0 0 16 16', 'path': '' }, 'device-camera': { 'viewbox': '0 0 16 16', 'path': '' }, 'device-camera-video': { 'viewbox': '0 0 16 16', 'path': '' }, 'device-desktop': { 'viewbox': '0 0 16 16', 'path': '' }, 'device-mobile': { 'viewbox': '0 0 10 16', 'path': '' }, 'diff': { 'viewbox': '0 0 13 16', 'path': '' }, 'diff-added': { 'viewbox': '0 0 14 16', 'path': '' }, 'diff-ignored': { 'viewbox': '0 0 14 16', 'path': '' }, 'diff-modified': { 'viewbox': '0 0 14 16', 'path': '' }, 'diff-removed': { 'viewbox': '0 0 14 16', 'path': '' }, 'diff-renamed': { 'viewbox': '0 0 14 16', 'path': '' }, 'ellipses': { 'viewbox': '0 0 12 16', 'path': '' }, 'ellipsis': { 'viewbox': '0 0 12 16', 'path': '' }, 'eye': { 'viewbox': '0 0 16 16', 'path': '' }, 'file': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-binary': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-code': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-directory': { 'viewbox': '0 0 14 16', 'path': '' }, 'file-media': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-pdf': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-submodule': { 'viewbox': '0 0 14 16', 'path': '' }, 'file-symlink-directory': { 'viewbox': '0 0 14 16', 'path': '' }, 'file-symlink-file': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-text': { 'viewbox': '0 0 12 16', 'path': '' }, 'file-zip': { 'viewbox': '0 0 12 16', 'path': '' }, 'flame': { 'viewbox': '0 0 12 16', 'path': '' }, 'fold': { 'viewbox': '0 0 14 16', 'path': '' }, 'gear': { 'viewbox': '0 0 14 16', 'path': '' }, 'gift': { 'viewbox': '0 0 14 16', 'path': '' }, 'gist': { 'viewbox': '0 0 12 16', 'path': '' }, 'gist-secret': { 'viewbox': '0 0 14 16', 'path': '' }, 'git-branch': { 'viewbox': '0 0 10 16', 'path': '' }, 'git-commit': { 'viewbox': '0 0 14 16', 'path': '' }, 'git-compare': { 'viewbox': '0 0 14 16', 'path': '' }, 'git-merge': { 'viewbox': '0 0 12 16', 'path': '' }, 'git-pull-request': { 'viewbox': '0 0 12 16', 'path': '' }, 'globe': { 'viewbox': '0 0 14 16', 'path': '' }, 'graph': { 'viewbox': '0 0 16 16', 'path': '' }, 'heart': { 'viewbox': '0 0 12 16', 'path': '' }, 'history': { 'viewbox': '0 0 14 16', 'path': '' }, 'home': { 'viewbox': '0 0 16 16', 'path': '' }, 'horizontal-rule': { 'viewbox': '0 0 10 16', 'path': '' }, 'hubot': { 'viewbox': '0 0 14 16', 'path': '' }, 'inbox': { 'viewbox': '0 0 14 16', 'path': '' }, 'info': { 'viewbox': '0 0 14 16', 'path': '' }, 'issue-closed': { 'viewbox': '0 0 16 16', 'path': '' }, 'issue-opened': { 'viewbox': '0 0 14 16', 'path': '' }, 'issue-reopened': { 'viewbox': '0 0 14 16', 'path': '' }, 'italic': { 'viewbox': '0 0 6 16', 'path': '' }, 'jersey': { 'viewbox': '0 0 14 16', 'path': '' }, 'key': { 'viewbox': '0 0 14 16', 'path': '' }, 'keyboard': { 'viewbox': '0 0 16 16', 'path': '' }, 'law': { 'viewbox': '0 0 14 16', 'path': '' }, 'light-bulb': { 'viewbox': '0 0 12 16', 'path': '' }, 'link': { 'viewbox': '0 0 16 16', 'path': '' }, 'link-external': { 'viewbox': '0 0 12 16', 'path': '' }, 'list-ordered': { 'viewbox': '0 0 12 16', 'path': '' }, 'list-unordered': { 'viewbox': '0 0 12 16', 'path': '' }, 'location': { 'viewbox': '0 0 12 16', 'path': '' }, 'lock': { 'viewbox': '0 0 12 16', 'path': '' }, 'logo-gist': { 'viewbox': '0 0 25 16', 'path': '' }, 'logo-github': { 'viewbox': '0 0 45 16', 'path': '' }, 'mail': { 'viewbox': '0 0 14 16', 'path': '' }, 'mail-read': { 'viewbox': '0 0 14 16', 'path': '' }, 'mail-reply': { 'viewbox': '0 0 12 16', 'path': '' }, 'mark-github': { 'viewbox': '0 0 16 16', 'path': '' }, 'markdown': { 'viewbox': '0 0 16 16', 'path': '' }, 'megaphone': { 'viewbox': '0 0 16 16', 'path': '' }, 'mention': { 'viewbox': '0 0 14 16', 'path': '' }, 'milestone': { 'viewbox': '0 0 14 16', 'path': '' }, 'mirror': { 'viewbox': '0 0 16 16', 'path': '' }, 'mortar-board': { 'viewbox': '0 0 16 16', 'path': '' }, 'mute': { 'viewbox': '0 0 16 16', 'path': '' }, 'no-newline': { 'viewbox': '0 0 16 16', 'path': '' }, 'octoface': { 'viewbox': '0 0 16 16', 'path': '' }, 'organization': { 'viewbox': '0 0 14 16', 'path': '' }, 'package': { 'viewbox': '0 0 16 16', 'path': '' }, 'paintcan': { 'viewbox': '0 0 12 16', 'path': '' }, 'pencil': { 'viewbox': '0 0 14 16', 'path': '' }, 'person': { 'viewbox': '0 0 8 16', 'path': '' }, 'pin': { 'viewbox': '0 0 16 16', 'path': '' }, 'plug': { 'viewbox': '0 0 14 16', 'path': '' }, 'plus': {'viewbox': '0 0 12 16', 'path': ''}, 'primitive-dot': { 'viewbox': '0 0 8 16', 'path': '' }, 'primitive-square': {'viewbox': '0 0 8 16', 'path': ''}, 'pulse': { 'viewbox': '0 0 14 16', 'path': '' }, 'question': { 'viewbox': '0 0 14 16', 'path': '' }, 'quote': { 'viewbox': '0 0 14 16', 'path': '' }, 'radio-tower': { 'viewbox': '0 0 16 16', 'path': '' }, 'repo': { 'viewbox': '0 0 12 16', 'path': '' }, 'repo-clone': { 'viewbox': '0 0 16 16', 'path': '' }, 'repo-force-push': { 'viewbox': '0 0 12 16', 'path': '' }, 'repo-forked': { 'viewbox': '0 0 10 16', 'path': '' }, 'repo-pull': { 'viewbox': '0 0 16 16', 'path': '' }, 'repo-push': { 'viewbox': '0 0 12 16', 'path': '' }, 'rocket': { 'viewbox': '0 0 16 16', 'path': '' }, 'rss': { 'viewbox': '0 0 10 16', 'path': '' }, 'ruby': { 'viewbox': '0 0 16 16', 'path': '' }, 'search': { 'viewbox': '0 0 16 16', 'path': '' }, 'server': { 'viewbox': '0 0 12 16', 'path': '' }, 'settings': { 'viewbox': '0 0 16 16', 'path': '' }, 'shield': { 'viewbox': '0 0 14 16', 'path': '' }, 'sign-in': { 'viewbox': '0 0 14 16', 'path': '' }, 'sign-out': { 'viewbox': '0 0 16 16', 'path': '' }, 'smiley': { 'viewbox': '0 0 16 16', 'path': '' }, 'squirrel': { 'viewbox': '0 0 16 16', 'path': '' }, 'star': { 'viewbox': '0 0 14 16', 'path': '' }, 'stop': { 'viewbox': '0 0 14 16', 'path': '' }, 'sync': { 'viewbox': '0 0 12 16', 'path': '' }, 'tag': { 'viewbox': '0 0 14 16', 'path': '' }, 'tasklist': { 'viewbox': '0 0 16 16', 'path': '' }, 'telescope': { 'viewbox': '0 0 14 16', 'path': '' }, 'terminal': { 'viewbox': '0 0 14 16', 'path': '' }, 'text-size': { 'viewbox': '0 0 18 16', 'path': '' }, 'three-bars': { 'viewbox': '0 0 12 16', 'path': '' }, 'thumbsdown': { 'viewbox': '0 0 16 16', 'path': '' }, 'thumbsup': { 'viewbox': '0 0 16 16', 'path': '' }, 'tools': { 'viewbox': '0 0 16 16', 'path': '' }, 'trashcan': { 'viewbox': '0 0 12 16', 'path': '' }, 'triangle-down': {'viewbox': '0 0 12 16', 'path': ''}, 'triangle-left': {'viewbox': '0 0 6 16', 'path': ''}, 'triangle-right': {'viewbox': '0 0 6 16', 'path': ''}, 'triangle-up': {'viewbox': '0 0 12 16', 'path': ''}, 'unfold': { 'viewbox': '0 0 14 16', 'path': '' }, 'unmute': { 'viewbox': '0 0 16 16', 'path': '' }, 'unverified': { 'viewbox': '0 0 16 16', 'path': '' }, 'verified': { 'viewbox': '0 0 16 16', 'path': '' }, 'versions': { 'viewbox': '0 0 14 16', 'path': '' }, 'watch': { 'viewbox': '0 0 12 16', 'path': '' }, 'x': { 'viewbox': '0 0 12 16', 'path': '' }, 'zap': {'viewbox': '0 0 10 16', 'path': ''} }; /** * Allow custom execution of internal triggers "show" and "hide". * * source: http://viralpatel.net/blogs/jquery-trigger-custom-event-show-hide-element/ */ GHT.initCustomTriggers = function () { jQuery.each(['show', 'hide'], function (i, ev) { var el = jQuery.fn[ev]; jQuery.fn[ev] = function () { this.trigger(ev); return el.apply(this, arguments); }; }); }; // Get the show on the road! GHT.init(); })(jQuery);