// ==UserScript== // @name GitHub Dark Script // @version 2.0.18 // @description GitHub Dark in userscript form, with a settings panel // @namespace https://github.com/StylishThemes // @include /^https?://((gist|guides|help|raw|status|developer)\.)?github\.com((?!generated_pages\/preview).)*$/ // @include https://*.githubusercontent.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_info // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect githubusercontent.com // @connect raw.githubusercontent.com // @run-at document-start // @require https://greasyfork.org/scripts/15563-jscolor/code/jscolor.js?version=106439 // @downloadURL none // ==/UserScript== /* global GM_addStyle, GM_getValue, GM_setValue, GM_info, GM_xmlhttpRequest, GM_registerMenuCommand, jscolor */ /* jshint esnext:true, unused:true */ (() => { 'use strict'; const version = GM_info.script.version, // delay until package.json allowed to load delay = 8.64e7, // 24 hours in milliseconds // Keyboard shortcut to open ghd panel (only a two key combo coded) keyboardOpen = 'g+0', keyboardToggle = 'g+-', // keyboard shortcut delay from first to second letter keyboardDelay = 1000, // base urls to fetch style and package.json root = 'https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/', defaults = { attach : 'scroll', color : '#4183C4', enable : true, font : 'Menlo', image : 'url("data:image/png;base64,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")', tab : 4, theme : 'Twilight', type : 'tiled', wrap : false, // toggle buttons enableCodeWrap : true, enableMonospace : true, // diff toggle + accordion mode modeDiffToggle : '1', // internal variables date : 0, version : 0, rawCss : '', themeCss : '', processedCss : '', last : {} }, // extract style & theme name regex = /\/\*! [^\*]+ \*\//, // "themes/" prefix not included here themes = { 'Ambiance' : 'ambiance.min.css', 'Chaos' : 'chaos.min.css', 'Clouds Midnight' : 'clouds-midnight.min.css', 'Cobalt' : 'cobalt.min.css', 'GitHub Dark' : 'github-dark.min.css', 'Idle Fingers' : 'idle-fingers.min.css', 'Kr Theme' : 'kr-theme.min.css', 'Merbivore' : 'merbivore.min.css', 'Merbivore Soft' : 'merbivore-soft.min.css', 'Mono Industrial' : 'mono-industrial.min.css', 'Mono Industrial Clear' : 'mono-industrial-clear.min.css', 'Monokai' : 'monokai.min.css', 'Monokai Spacegray Eighties' : 'monokai-spacegray-eighties.min.css', 'Obsidian' : 'obsidian.min.css', 'Pastel on Dark' : 'pastel-on-dark.min.css', 'Solarized Dark' : 'solarized-dark.min.css', 'Terminal' : 'terminal.min.css', 'Tomorrow Night' : 'tomorrow-night.min.css', 'Tomorrow Night Blue' : 'tomorrow-night-blue.min.css', 'Tomorrow Night Bright' : 'tomorrow-night-bright.min.css', 'Tomorrow Night Eigthies' : 'tomorrow-night-eighties.min.css', 'Twilight' : 'twilight.min.css', 'Vibrant Ink' : 'vibrant-ink.min.css' }, type = { tiled : ` background-repeat: repeat !important; background-size: auto !important; background-position: left top !important; `, fit : ` background-repeat: no-repeat !important; background-size: cover !important; background-position: center top !important; ` }, wrapCss = { wrapped : ` white-space: pre-wrap !important; word-break: break-all !important; overflow-wrap: break-word !important; display: block !important; `, unwrap : ` white-space: pre !important; word-break: normal !important; display: block !important; ` }, // https://github.com/StylishThemes/GitHub-code-wrap/blob/master/github-code-wrap.css wrapCodeCss = ` /* GitHub: Enable wrapping of long code lines */ .blob-code-inner, .markdown-body pre > code, .markdown-body .highlight > pre { ${wrapCss.wrapped} } td.blob-code-inner { display: table-cell !important; } `, wrapIcon = ` `, monospaceIcon = ` `, fileIcon = ` `, $style = make({ el: 'style', cl4ss: 'ghd-style' }); let timer, mutationTimer, picker, // jscolor picker isInitialized = 'pending', // prevent mutationObserver from going nuts isUpdating = false, // set when css code to test is pasted into the settings panel testing = false, // debug = GM_getValue('debug', false), data = {}; function updatePanel() { if (!isInitialized || !$('#ghd-settings-inner')) { return; } // prevent multiple change events from processing isUpdating = true; let temp, el, body = $('body'), panel = $('#ghd-settings-inner'); $('.ghd-attach', panel).value = data.attach || defaults.attach; $('.ghd-font', panel).value = data.font || defaults.font; $('.ghd-image', panel).value = data.image || defaults.image; $('.ghd-tab', panel).value = data.tab || defaults.tab; $('.ghd-theme', panel).value = data.theme || defaults.theme; $('.ghd-type', panel).value = data.type || defaults.type; $('.ghd-enable', panel).checked = isBool('enable'); $('.ghd-wrap', panel).checked = isBool('wrap'); $('.ghd-codewrap-checkbox', panel).checked = isBool('enableCodeWrap'); $('.ghd-monospace-checkbox', panel).checked = isBool('enableMonospace'); el = $('.ghd-diff-select', panel); temp = '' + (data.modeDiffToggle || defaults.modeDiffToggle); el.value = temp; toggleClass(el, 'enabled', temp !== '0'); // update version tooltip $('.ghd-versions', panel).setAttribute('aria-label', getVersionTooltip()); temp = data.color || defaults.color; $('.ghd-color').value = temp; // update swatch color & color picker value $('#ghd-swatch').style.backgroundColor = temp; if (picker) { picker.fromString(temp); } $style.disabled = !data.enable; toggleClass(body, 'ghd-disabled', !data.enable); toggleClass(body, 'nowrap', !data.wrap); if (data.enableCodeWrap !== data.lastCW || data.enableMonospace !== data.lastMS || data.modeDiffToggle !== data.lastDT) { data.lastCW = data.enableCodeWrap; data.lastMS = data.enableMonospace; data.lastDT = data.modeDiffToggle; updateToggles(); } isUpdating = false; } function getStoredValues(init) { data = GM_getValue('data', defaults); try { data = JSON.parse(data); if (!Object.keys(data).length || ({}).toString.call(data) !== "[object Object]") { throw new Error(); } } catch(err) { // compat data = GM_getValue('data', defaults); } if (debug) { if (init) { console.log('GitHub-Dark Script initializing!'); } console.log('Retrieved stored values', data); } } function setStoredValues(reset) { data.processedCss = $style.textContent; GM_setValue('data', JSON.stringify(reset ? defaults : data)); updatePanel(); if (debug) { console.log((reset ? 'Resetting' : 'Saving') + ' current values', data); } } // modified from http://stackoverflow.com/a/5624139/145346 function hexToRgb(hex) { let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? [ parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16) ].join(", ") : ""; } // convert version "1.2.3" into "001002003" for easier comparison function convertVersion(val) { let index, parts = val ? val.split('.') : '', str = '', len = parts.length; for (index = 0; index < len; index++) { str += ('000' + parts[index]).slice(-3); } if (debug) { console.log(`Converted version "${val}" to "${str}" for easy comparison`); } return val ? str : val; } function checkVersion() { if (debug) { console.log('Fetching package.json'); } GM_xmlhttpRequest({ method : 'GET', url : root + 'package.json', onload : response => { let pkg = JSON.parse(response.responseText); // save last loaded date, so package.json is only loaded once a day data.date = new Date().getTime(); let ver = convertVersion(pkg.version); // if new available, load it & parse if (ver > data.version) { if (data.version !== 0 && debug) { console.log('Updating from ${data.version} to ${ver}'); } data.version = ver; fetchAndApplyStyle(); } else { addSavedStyle(); } // save new date/version GM_setValue('data', JSON.stringify(data)); } }); } function fetchAndApplyStyle() { if (debug) { console.log(`Fetching ${root}github-dark.css`); } GM_xmlhttpRequest({ method : 'GET', url : root + 'github-dark.css', onload : response => { data.rawCss = response.responseText; processStyle(); } }); } // load syntax highlighting theme function fetchAndApplyTheme() { if (!data.enable) { if (debug) { console.log('Disabled: stop theme processing'); } return; } if (data.lastTheme === data.theme && data.themeCss !== '') { return applyTheme(); } let name = data.theme || 'Twilight', themeUrl = root + 'themes/' + themes[name]; if (debug) { console.log(`Fetching ${name} theme`, themeUrl); } GM_xmlhttpRequest({ method : 'GET', url : themeUrl, onload : response => { let theme = response.responseText; if (response.status === 200 && theme) { data.themeCss = theme; data.lastTheme = name; applyTheme(); } else { throw Error(`Failed to load theme file: "${theme}"`); } } }); } function applyTheme() { if (debug) { console.log('Adding syntax theme "' + (data.themeCss || '').match(regex) + '" to css'); } let css = data.processedCss || ''; css = css.replace('/*[[syntax-theme]]*/', data.themeCss || ''); applyStyle(css); setStoredValues(); isUpdating = false; } function processStyle() { let url = /^url/.test(data.image || '') ? data.image : (data.image === 'none' ? 'none' : 'url("' + data.image + '")'); if (!data.enable) { if (debug) { console.log('Disabled: stop processing'); } return; } if (debug) { console.log('Processing set styles'); } let processed = (data.rawCss || '') // remove moz-document wrapper .replace(/@-moz-document regexp\((.*)\) \{(\n|\r)+/, '') // replace background image; if no 'url' at start, then use 'none' .replace(/\/\*\[\[bg-choice\]\]\*\/ url\(.*\)/, url) // Add tiled or fit window size css .replace('/*[[bg-options]]*/', type[data.type || 'tiled']) // set scroll or fixed background image .replace('/*[[bg-attachment]]*/ fixed', data.attach || 'scroll') // replace base-color .replace(/\/\*\[\[base-color\]\]\*\/ #\w{3,6}/g, data.color || '#4183C4') // replace base-color-rgb .replace(/\/\*\[\[base-color-rgb\]\]\*\//g, hexToRgb(data.color || '#4183c4')) // add font choice .replace('/*[[font-choice]]*/', data.font || 'Menlo') // add tab size .replace(/\/\*\[\[tab-size\]\]\*\/ \d+/g, data.tab || 4) // code wrap css .replace('/*[[code-wrap]]*/', data.wrap ? wrapCodeCss : '') // remove default syntax .replace(/\s+\/\* grunt build - remove to end of file(.*(\n|\r))+\}$/m, ''); // see https://github.com/StylishThemes/GitHub-Dark/issues/275 if (/firefox/i.test(navigator.userAgent)) { processed = processed // line in github-dark.css = "select, input:not(.btn-link), textarea" .replace('select, input:not(.btn-link)', 'input { color:#eee !important; } select') .replace(/input\[type=\"checkbox\"\][\s\S]+?}/gm, ''); } data.processedCss = processed; fetchAndApplyTheme(); } function applyStyle(css) { if (debug) { console.log('Applying style', '"' + (css || '').match(regex) + '"'); } $style.textContent = css || ''; } function addSavedStyle() { if (debug) { console.log('Adding previously saved style'); } // apply already processed css to prevent FOUC $style.textContent = data.processedCss; } function updateStyle() { isUpdating = true; if (debug) { console.log('Updating user settings'); } let body = $('body'), panel = $('#ghd-settings-inner'); data.attach = $('.ghd-attach', panel).value; // get hex value directly data.color = picker.toHEXString(); data.enable = $('.ghd-enable', panel).checked; data.font = $('.ghd-font', panel).value; data.image = $('.ghd-image', panel).value; data.tab = $('.ghd-tab', panel).value; data.theme = $('.ghd-theme', panel).value; data.type = $('.ghd-type', panel).value; data.wrap = $('.ghd-wrap', panel).checked; data.enableCodeWrap = $('.ghd-codewrap-checkbox', panel).checked; data.enableMonospace = $('.ghd-monospace-checkbox', panel).checked; data.modeDiffToggle = $('.ghd-diff-select', panel).value; $style.disabled = !data.enable; toggleClass(body, 'ghd-disabled', !data.enable); toggleClass(body, 'nowrap', !data.wrap); if (testing) { processStyle(); testing = false; } else { fetchAndApplyStyle(); } isUpdating = false; } // user can force GitHub-dark update function forceUpdate(css) { if (css) { // add raw css directly for style testing data.rawCss = css; processStyle(); } else { // clear saved date data.version = 0; data.themeCss = ''; GM_setValue('data', JSON.stringify(data)); closePanel('forced'); } } function getVersionTooltip() { let indx, ver = [], // convert stored css version from "001014049" into "1.14.49" for tooltip parts = String(data.version).match(/\d{3}/g), len = parts && parts.length || 0; for (indx = 0; indx < len; indx++) { ver.push(parseInt(parts[indx], 10)); } return `Script v${version}\nCSS ${(ver.length ? 'v' + ver.join('.') : 'unknown')}`; } function buildSettings() { if (debug) { console.log('Adding settings panel & GitHub Dark link to profile dropdown'); } // Script-specific CSS GM_addStyle(` #ghd-menu:hover { cursor:pointer } #ghd-settings { position:fixed; z-index:65535; top:0; bottom:0; left:0; right:0; opacity:0; visibility:hidden; } #ghd-settings.in { opacity:1; visibility:visible; background:rgba(0,0,0,.5); } #ghd-settings-inner { position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); width:25rem; box-shadow:0 .5rem 1rem #111; color:#c0c0c0 } #ghd-settings label { margin-left:.5rem; position:relative; top:-1px } #ghd-settings-close { height:1rem; width:1rem; fill:#666; float:right; cursor:pointer } #ghd-settings-close:hover { fill:#ccc } #ghd-settings .ghd-right { float:right; padding:5px; } #ghd-settings p { line-height:22px; } #ghd-settings .form-select, #ghd-settings input[type="text"] { height:30px; min-height:30px; } #ghd-swatch { width:25px; height:22px; display:inline-block; margin:3px 10px; border-radius:4px; } #ghd-settings input, #ghd-settings select { border:#555 1px solid; } #ghd-settings .ghd-attach, #ghd-settings .ghd-diff-select { padding-right:25px; } #ghd-settings input[type="checkbox"] { margin-top:3px; width: 16px !important; height: 16px !important; border-radius: 3px !important; } #ghd-settings .boxed-group-inner { padding:0; } #ghd-settings .ghd-footer { padding:10px; border-top:#555 solid 1px; } #ghd-settings .ghd-settings-wrapper { max-height:60vh; overflow-y:auto; padding:4px 10px; } #ghd-settings .ghd-tab { width:5em; } #ghd-settings .octicon { vertical-align:text-bottom !important; } #ghd-settings .ghd-paste-area { position:absolute; bottom:50px; top:37px; left:2px; right:2px; width:396px !important; height:-moz-calc(100% - 85px); resize:none !important; border-style:solid; z-index:0; } /* code wrap toggle: https://gist.github.com/silverwind/6c1701f56e62204cc42b icons next to a pre */ .ghd-wrap-toggle { position:absolute; right:1.4em; margin-top:.2em; -moz-user-select:none; -webkit-user-select:none; cursor:pointer; z-index:20; } /* file & diff code tables */ .ghd-wrap-table .blob-code-inner { white-space:pre-wrap !important; word-break:break-all !important; } .ghd-unwrap-table .blob-code-inner { white-space:pre !important; word-break:normal !important; } .ghd-wrap-toggle > *, .ghd-monospace > *, .ghd-file-toggle > * { pointer-events:none; vertical-align:middle !important; } /* icons inside a wrapper immediatly around a pre */ .highlight > .ghd-wrap-toggle { right:.5em; top:.5em; margin-top:0; } /* icons for non-syntax highlighted code blocks; see https://github.com/gjtorikian/html-proofer/blob/master/README.md */ .markdown-body:not(.comment-body) .ghd-wrap-toggle:not(:first-child) { right:3.4em; } .ghd-wrap-toggle svg { height:1.25em; width:1.25em; fill:rgba(110,110,110,.4); } /* wrap disabled (red) */ .ghd-wrap-toggle.unwrap:hover svg, .ghd-wrap-toggle:hover svg { fill:#8b0000; } /* wrap enabled (green) */ body:not(.nowrap) .ghd-wrap-toggle:not(.unwrap):hover svg, .ghd-wrap-toggle.wrapped:hover svg { fill:#006400; } .blob-wrapper, .markdown-body pre, .markdown-body .highlight { position:relative; } /* monospace font toggle */ .ghd-monospace-font { font-family:"${data.font}", Menlo, Inconsolata, "Droid Mono", monospace !important; font-size:1em !important; } /* file collapsed icon */ .ghd-file-collapsed > :not(.file-header) { display:none !important; } .ghd-file-collapsed .ghd-file-toggle svg { -webkit-transform:rotate(90deg); transform:rotate(90deg); } `); let indx, theme, icon, opts = '', ver = getVersionTooltip(), names = Object.keys(themes), len = names.length; for (indx = 0; indx < len; indx++) { theme = names[indx]; opts += ``; } // circle-question-mark icon icon = ` `; // Settings panel markup make({ el : 'div', appendTo: 'body', attr : { id: 'ghd-settings' }, html : `

GitHub-Dark Settings ${icon}

Background

${icon}

Code

${icon}

Toggles

` }); updateToggles(); } // Add code wrap toggle function buildCodeWrap() { // mutation events happen quick, so we still add an update flag isUpdating = true; let icon = make({ el : 'div', cl4ss : 'ghd-wrap-toggle tooltipped tooltipped-w', attr : { 'aria-label' : 'Toggle code wrap' }, html : wrapIcon }); $$('.blob-wrapper').forEach(el => { el.insertBefore(icon.cloneNode(true), el.childNodes[0]); }); $$('.markdown-body pre').forEach(el => { el.parentNode.insertBefore(icon.cloneNode(true), el); }); isUpdating = false; } // Add monospace font toggle function addMonospaceToggle() { isUpdating = true; let button = make({ el : 'button', cl4ss : 'ghd-monospace toolbar-item tooltipped tooltipped-n', attr : { 'type' : 'button', 'aria-label' : 'Toggle monospaced font', 'tabindex' : '-1' }, html : monospaceIcon }); $$('.toolbar-commenting').forEach(el => { if (!$('.ghd-monospace', el)) { // prepend el.insertBefore(button.cloneNode(true), el.childNodes[0]); } }); isUpdating = false; } // Add file diffs toggle function addFileToggle() { isUpdating = true; let firstButton, button = make({ el : 'button', cl4ss : 'ghd-file-toggle btn btn-sm tooltipped tooltipped-n', attr : { 'type' : 'button', 'aria-label' : 'Click to Expand or Collapse file', 'tabindex' : '-1' }, html : fileIcon }); $$('#files .file-actions').forEach(el => { if (!$('.ghd-file-toggle', el)) { el.appendChild(button.cloneNode(true)); } }); firstButton = $('.ghd-file-toggle'); // accordion mode = start with all but first entry collapsed if (firstButton && data.modeDiffToggle === '2') { toggleFile({ target: firstButton }, true); } isUpdating = false; } // Add toggle buttons after page updates function updateToggles() { if (data.enableCodeWrap) { buildCodeWrap(); } else { removeAll('.ghd-wrap-toggle'); toggleClass($$('.ghd-file-collapsed'), 'ghd-file-collapsed', false); } if (data.enableMonospace) { addMonospaceToggle(); } else { removeAll('.ghd-monospace'); toggleClass($$('.ghd-monospace-font'), 'ghd-monospace-font', false); } if (data.modeDiffToggle !== '0') { addFileToggle(); } else { removeAll('.ghd-file-toggle'); toggleClass($$('.ghd-file-collapsed'), 'ghd-file-collapsed', false); } } function makeRow(vals, str) { return make({ el : 'tr', cl4ss : 'ghd-shortcut', html : `${vals[0]} ${vals[1]}${str}` }); } // add keyboard shortcut to help menu (press "?") function buildShortcut() { let row, el = $('.keyboard-mappings tbody'); if (el && !$('.ghd-shortcut')) { row = makeRow(keyboardOpen.split('+'), 'GitHub-Dark: open settings'); el.appendChild(row); row = makeRow(keyboardToggle.split('+'), 'GitHub-Dark: toggle style'); el.appendChild(row); } } function toggleCodeWrap(el) { let css, overallWrap = data.wrap, code = next(el, '.highlight, .diff-table, code, pre'), tmp = code ? $('code', code) : ''; if (tmp) { // find code element code = tmp; } if (!code) { if (debug) { console.log('Code wrap icon associated code not found', el); } return; } // code with line numbers if (code.nodeName === 'TABLE') { if (code.className.indexOf('ghd-') < 0) { css = !overallWrap; } else { css = code.classList.contains('ghd-unwrap-table'); } toggleClass(code, 'ghd-wrap-table', css); toggleClass(code, 'ghd-unwrap-table', !css); toggleClass(el, 'wrapped', css); toggleClass(el, 'unwrap', !css); } else { css = code.getAttribute('style') || ''; if (css === '') { css = wrapCss[overallWrap ? 'unwrap' : 'wrapped']; } else { css = wrapCss[css === wrapCss.wrapped ? 'unwrap' : 'wrapped']; } code.setAttribute('style', css); toggleClass(el, 'wrapped', css === wrapCss.wrapped); toggleClass(el, 'unwrap', css === wrapCss.unwrap); } } function toggleMonospace(el) { let tmp = closest(el, '.previewable-comment-form'), // single comment textarea = $('.comment-form-textarea', tmp); if (textarea) { toggleClass(textarea, 'ghd-monospace-font'); textarea.focus(); tmp = textarea.classList.contains('ghd-monospace-font'); toggleClass(el, 'ghd-icon-active', tmp); } } function toggleSibs(target, state) { let el, isCollapsed = state || target.classList.contains('ghd-file-collapsed'), toggles = document.querySelectorAll('.file'), indx = toggles.length; while (indx--) { el = toggles[indx]; if (el !== target) { el.classList[isCollapsed ? 'add' : 'remove']('ghd-file-collapsed'); } } } function toggleFile(event, init) { isUpdating = true; let el = closest(event.target, '.file'); if (data.modeDiffToggle === '2') { if (!init) { el.classList.toggle('ghd-file-collapsed'); } toggleSibs(el, true); } else { el.classList.toggle('ghd-file-collapsed'); // shift+click toggle all files! if (event.shiftKey) { toggleSibs(el); } } isUpdating = false; } function bindEvents() { let el, menu, lastKey, panel = $('#ghd-settings-inner'), swatch = $('#ghd-swatch', panel); // finish initialization $('#ghd-settings-inner .ghd-enable').checked = data.enable; toggleClass($('body'), 'ghd-disabled', !data.enable); // Create our menu entry menu = make({ el : 'a', cl4ss : 'dropdown-item', html : 'GitHub Dark Settings', attr : { id : 'ghd-menu' } }); el = $$('.header .dropdown-item[href="/settings/profile"], .header .dropdown-item[data-ga-click*="go to profile"]'); // get last found item - gists only have the "go to profile" item; GitHub has both el = el[el.length - 1]; if (el) { // insert after el.parentNode.insertBefore(menu, el.nextSibling); on($('#ghd-menu'), 'click', () => { openPanel(); }); } on(document, 'keypress keydown', event => { clearTimeout(timer); // use "g+o" to open up ghd options panel let openKeys = keyboardOpen.split('+'), toggleKeys = keyboardToggle.split('+'), key = String.fromCharCode(event.which).toLowerCase(), panelVisible = $('#ghd-settings').classList.contains('in'); // press escape to close the panel if (event.which === 27 && panelVisible) { closePanel(); return; } // use event.which from keypress for shortcuts // prevent opening panel while typing "go" in comments if (event.type === 'keydown' || /(input|textarea)/i.test(document.activeElement.nodeName)) { return; } if (lastKey === openKeys[0] && key === openKeys[1]) { if (panelVisible) { closePanel(); } else { openPanel(); } } if (lastKey === toggleKeys[0] && key === toggleKeys[1]) { toggleStyle(); } lastKey = key; timer = setTimeout(() => { lastKey = null; }, keyboardDelay); // add shortcut to help menu if (key === '?') { // table doesn't exist until user presses "?" setTimeout(() => { buildShortcut(); }, 300); } }); // add ghd-settings panel bindings on($$('#ghd-settings, #ghd-settings-close'), 'click keyup', event => { // press escape to close settings if (event.type === 'keyup' && event.which !== 27) { return; } closePanel(); }); on(panel, 'click', event => { event.stopPropagation(); }); on($('.ghd-reset', panel), 'click', event => { event.preventDefault(); isUpdating = true; // pass true to reset values setStoredValues(true); // add reset values back to data getStoredValues(); // add reset values to panel updatePanel(); // update style updateStyle(); }); on($$('input[type="text"]', panel), 'focus', function() { // select all text when focused this.select(); }); on($$('select, input', panel), 'change', () => { if (!isUpdating) { updateStyle(); } }); on($('.ghd-update', panel), 'click', event => { event.preventDefault(); forceUpdate(); }); on($('.ghd-textarea-toggle', panel), 'click', function(event) { event.preventDefault(); let hidden, el; this.classList.remove('selected'); el = next(this, '.ghd-paste-area-content'); if (el) { hidden = el.style.display === 'none'; el.style.display = hidden ? '' : 'none'; if (el.style.display !== 'none') { el.classList.add('selected'); el = $('textarea', el); el.focus(); el.select(); } } return false; }); on($('.ghd-paste-area-content', panel), 'paste', event => { let toggle = $('.ghd-textarea-toggle', panel), textarea = event.target; setTimeout(() => { textarea.parentNode.style.display = 'none'; toggle.classList.remove('selected'); testing = true; forceUpdate(textarea.value); }, 200); }); // Toggles on($('body'), 'click', event => { let target = event.target; if (data.enableCodeWrap && target.classList.contains('ghd-wrap-toggle')) { // **** CODE WRAP TOGGLE **** event.stopPropagation(); toggleCodeWrap(target); } else if (data.enableMonospace && target.classList.contains('ghd-monospace')) { // **** MONOSPACE FONT TOGGLE **** event.stopPropagation(); toggleMonospace(target); return false; } else if (data.modeDiffToggle !== '0' && target.classList.contains('ghd-file-toggle')) { // **** CODE DIFF COLLAPSE TOGGLE **** event.stopPropagation(); toggleFile(event); } }); // style color picker picker = new jscolor($('.ghd-color', panel)); picker.zIndex = 65536; picker.hash = true; picker.backgroundColor = '#333'; picker.padding = 0; picker.borderWidth = 0; picker.borderColor = '#444'; picker.onFineChange = () => { swatch.style.backgroundColor = '#' + picker; }; } function openPanel() { $('.modal-backdrop').click(); updatePanel(); $('#ghd-settings').classList.add('in'); } function closePanel(flag) { $('#ghd-settings').classList.remove('in'); picker.hide(); if (flag === 'forced') { // forced update; partial re-initialization init(); } else { // apply changes when the panel is closed updateStyle(); } } function toggleStyle() { let isEnabled = !data.enable; data.enable = isEnabled; $('#ghd-settings-inner .ghd-enable').checked = isEnabled; // add processedCss back into style (emptied when disabled) if (isEnabled) { // data.processedCss is empty when ghd is disabled on init if (!data.processedCss) { processStyle(); } else { addSavedStyle(); } } $style.disabled = !isEnabled; } function init() { if (!document.head) return; document.head.appendChild($style); getStoredValues(true); $style.disabled = !data.enable; data.lastTheme = data.theme; data.lastCW = data.enableCodeWrap; data.lastMS = data.enableMonospace; data.lastDT = data.modeDiffToggle; // only load package.json once a day, or after a forced update if ((new Date().getTime() > data.date + delay) || data.version === 0) { // get package.json from GitHub-Dark & compare versions // load new script if a newer one is available checkVersion(); } else { addSavedStyle(); } isInitialized = false; } // add style at document-start init(); on(document, 'DOMContentLoaded', () => { if (isInitialized === 'pending') { // init after DOM loaded on .atom pages init(); } // add panel even if you're not logged in - open panel using keyboard shortcut // just not on githubusercontent pages (no settings panel needed) if (window.location.host.indexOf('githubusercontent.com') < 0) { buildSettings(); // add event binding on document ready bindEvents(); $$( `#js-repo-pjax-container, #js-pjax-container, .js-contribution-activity, .js-diff-progressive-container` ).forEach(target => { new MutationObserver(mutations => { mutations.forEach(mutation => { // preform checks before adding code wrap to minimize function calls if (!isUpdating && mutation.target === target) { clearTimeout(mutationTimer); mutationTimer = setTimeout(() => { updateToggles(); }, 400); } }); }).observe(target, { childList: true, subtree: true }); }); } isInitialized = true; }); /* utility functions */ function isBool(name) { let val = data[name]; return typeof val === 'boolean' ? val : defaults[name]; } function $(str, el) { return (el || document).querySelector(str); } function $$(str, el) { return Array.from((el || document).querySelectorAll(str)); } function next(el, selector) { while ((el = el.nextElementSibling)) { if (el && el.matches(selector)) { return el; } } return null; } function closest(el, selector) { while (el && el.nodeName !== 'BODY' && !el.matches(selector)) { el = el.parentNode; } return el && el.matches(selector) ? el : []; } function make(obj) { let key, el = document.createElement(obj.el); if (obj.cl4ss) { el.className = obj.cl4ss; } if (obj.html) { el.innerHTML = obj.html; } if (obj.attr) { for (key in obj.attr) { if (obj.attr.hasOwnProperty(key)) { el.setAttribute(key, obj.attr[key]); } } } if (obj.appendTo) { $(obj.appendTo).appendChild(el); } return el; } function removeAll(selector) { $$(selector).forEach(el => { el.parentNode.removeChild(el); }); } function on(els, name, callback) { els = Array.isArray(els) ? els : [els]; let events = name.split(/\s+/); els.forEach(el => { events.forEach(ev => { el.addEventListener(ev, callback); }); }); } function toggleClass(els, cl4ss, flag) { els = Array.isArray(els) ? els : [els]; els.forEach(el => { if (el) { if (typeof flag === 'undefined') { flag = !el.classList.contains(cl4ss); } if (flag) { el.classList.add(cl4ss); } else { el.classList.remove(cl4ss); } } }); } // Add GM options GM_registerMenuCommand("GitHub Dark Script debug logging", () => { let val = prompt('Toggle GitHub Dark Script debug log (true/false):', !debug); if (val) { debug = /^t/.test(val); GM_setValue('debug', debug); } }); })();