Last active
March 15, 2026 19:20
-
-
Save kiranwayne/2ce1079be4a5da8968205bf288ef22bb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Towards Data Science Enhanced | |
| // @namespace https://gist.github.com/kiranwayne | |
| // @version 1.0.4 | |
| // @description Adds a custom Text width and justification toggle on towardsdatascience.com. | |
| // @author kiranwayne | |
| // @match https://towardsdatascience.com/* | |
| // @updateURL https://gist.github.com/kiranwayne/2ce1079be4a5da8968205bf288ef22bb/raw/towardsdatascience_enhanced.js | |
| // @downloadURL https://gist.github.com/kiranwayne/2ce1079be4a5da8968205bf288ef22bb/raw/towardsdatascience_enhanced.js | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // --- Configuration & Constants --- | |
| const SCRIPT_NAME = 'Towards Data Science Enhanced'; | |
| const SCRIPT_VERSION = '1.0.4'; | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| const CONFIG_PREFIX = 'tdsEnhanced_v1_'; | |
| // Settings Keys | |
| const WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx'; | |
| const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth'; | |
| const JUSTIFY_KEY = CONFIG_PREFIX + 'justifyEnabled'; | |
| const SETTINGS_UI_VISIBLE_KEY = CONFIG_PREFIX + 'settingsUiVisible'; | |
| // DOM & Styles | |
| const STYLE_ID = 'tds-enhanced-styles'; | |
| const SETTINGS_PANEL_ID = 'tds-userscript-settings-panel'; | |
| // Selectors | |
| const WIDTH_SELECTOR = '.entry-content > :not(.alignwide):not(.alignfull)'; | |
| const JUSTIFY_SELECTOR = '.entry-content p'; | |
| // Defaults | |
| const DEFAULT_WIDTH_PX = 1000; | |
| const MIN_WIDTH_PX = 600; | |
| const MAX_WIDTH_PX = 2500; | |
| const STEP_WIDTH_PX = 10; | |
| // --- State Variables --- | |
| let config = {}; | |
| let settingsPanel = null; | |
| let widthSlider = null, widthLabel = null, widthInput = null, defaultWidthCheckbox = null, justifyCheckbox = null; | |
| let menuCommandId = null; | |
| // --- Helper Functions --- | |
| async function loadSettings() { | |
| config.maxWidthPx = await GM_getValue(WIDTH_PX_KEY, DEFAULT_WIDTH_PX); | |
| config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, false); | |
| config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, false); | |
| config.settingsUiVisible = await GM_getValue(SETTINGS_UI_VISIBLE_KEY, false); | |
| } | |
| async function saveSetting(key, value) { | |
| const configKey = key.replace(CONFIG_PREFIX, ''); | |
| config[configKey] = value; | |
| await GM_setValue(key, value); | |
| } | |
| // --- Style Injection --- | |
| function applyStyles() { | |
| // Always remove any existing injected style element first for a clean slate | |
| const existingStyleEl = document.getElementById(STYLE_ID); | |
| if (existingStyleEl) { | |
| existingStyleEl.remove(); | |
| } | |
| let css = ''; | |
| // Width Logic | |
| if (!config.useDefaultWidth) { | |
| css += ` | |
| :root { | |
| --wp--style--global--content-size: ${config.maxWidthPx}px !important; | |
| --wp--style--global--wide-size: ${config.maxWidthPx + 200}px !important; | |
| } | |
| .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { | |
| max-width: ${config.maxWidthPx}px !important; | |
| } | |
| .entry-content { | |
| max-width: none !important; | |
| width: 100% !important; | |
| } | |
| ${WIDTH_SELECTOR} { | |
| max-width: ${config.maxWidthPx}px !important; | |
| } | |
| `; | |
| } | |
| // Justification Logic | |
| if (config.justifyEnabled) { | |
| css += ` | |
| ${JUSTIFY_SELECTOR} { | |
| text-align: justify !important; | |
| } | |
| `; | |
| } | |
| // Only inject a new style element if there's CSS to apply | |
| if (css) { | |
| const styleEl = document.createElement('style'); | |
| styleEl.id = STYLE_ID; | |
| styleEl.textContent = css; | |
| document.head.appendChild(styleEl); | |
| } | |
| } | |
| // --- UI Creation/Removal --- | |
| function removeSettingsUI() { | |
| document.removeEventListener('click', handleClickOutside, true); | |
| settingsPanel = document.getElementById(SETTINGS_PANEL_ID); | |
| if (settingsPanel) { | |
| settingsPanel.remove(); | |
| settingsPanel = null; | |
| } | |
| } | |
| async function handleClickOutside(event) { | |
| if (settingsPanel && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target)) { | |
| // Prevent closing if clicking the Tampermonkey menu | |
| await saveSetting(SETTINGS_UI_VISIBLE_KEY, false); | |
| removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID) || !config.settingsUiVisible) return; | |
| removeSettingsUI(); | |
| settingsPanel = document.createElement('div'); | |
| settingsPanel.id = SETTINGS_PANEL_ID; | |
| // Moved top to 60px to prevent cutoff | |
| Object.assign(settingsPanel.style, { | |
| position: 'fixed', top: '60px', right: '20px', zIndex: '2147483647', | |
| display: 'block', background: '#202021', color: '#ECECF1', | |
| border: '1px solid #565869', borderRadius: '6px', padding: '15px', | |
| boxShadow: '0 4px 15px rgba(0,0,0,0.6)', minWidth: '300px', | |
| fontFamily: 'sans-serif', fontSize: '14px' | |
| }); | |
| // --- Header Section (Title, Version, Author) --- | |
| const headerDiv = document.createElement('div'); | |
| headerDiv.style.marginBottom = '15px'; | |
| headerDiv.style.paddingBottom = '10px'; | |
| headerDiv.style.borderBottom = '1px solid #565869'; | |
| const titleEl = document.createElement('h4'); | |
| titleEl.textContent = SCRIPT_NAME; | |
| Object.assign(titleEl.style, { margin: '0 0 5px 0', fontSize: '1.1em', fontWeight: 'bold', color: '#FFFFFF' }); | |
| const versionEl = document.createElement('div'); | |
| versionEl.textContent = `Version: ${SCRIPT_VERSION}`; | |
| Object.assign(versionEl.style, { fontSize: '0.85em', opacity: '0.7', marginBottom: '2px' }); | |
| const authorEl = document.createElement('div'); | |
| authorEl.textContent = `Author: ${SCRIPT_AUTHOR}`; | |
| Object.assign(authorEl.style, { fontSize: '0.85em', opacity: '0.7' }); | |
| headerDiv.appendChild(titleEl); | |
| headerDiv.appendChild(versionEl); | |
| headerDiv.appendChild(authorEl); | |
| settingsPanel.appendChild(headerDiv); | |
| // --- Controls --- | |
| // 1. Default Width Toggle | |
| const defaultWidthDiv = document.createElement('div'); | |
| defaultWidthDiv.style.marginBottom = '10px'; | |
| defaultWidthCheckbox = document.createElement('input'); | |
| defaultWidthCheckbox.type = 'checkbox'; | |
| defaultWidthCheckbox.id = 'fe-default-width-toggle'; | |
| defaultWidthCheckbox.checked = config.useDefaultWidth; | |
| const defaultWidthLabel = document.createElement('label'); | |
| defaultWidthLabel.htmlFor = 'fe-default-width-toggle'; | |
| defaultWidthLabel.textContent = ' Use Default Width'; | |
| defaultWidthLabel.style.marginLeft = '8px'; | |
| defaultWidthLabel.style.cursor = 'pointer'; | |
| defaultWidthDiv.appendChild(defaultWidthCheckbox); | |
| defaultWidthDiv.appendChild(defaultWidthLabel); | |
| settingsPanel.appendChild(defaultWidthDiv); | |
| // 2. Width Slider | |
| const widthControlsDiv = document.createElement('div'); | |
| widthControlsDiv.style.display = 'flex'; | |
| widthControlsDiv.style.alignItems = 'center'; | |
| widthControlsDiv.style.gap = '10px'; | |
| widthControlsDiv.style.marginBottom = '15px'; | |
| widthLabel = document.createElement('span'); | |
| widthLabel.style.minWidth = '50px'; | |
| widthLabel.style.fontFamily = 'monospace'; | |
| widthLabel.textContent = `${config.maxWidthPx}px`; | |
| widthSlider = document.createElement('input'); | |
| widthSlider.type = 'range'; | |
| widthSlider.min = MIN_WIDTH_PX; | |
| widthSlider.max = MAX_WIDTH_PX; | |
| widthSlider.step = STEP_WIDTH_PX; | |
| widthSlider.value = config.maxWidthPx; | |
| widthSlider.style.flexGrow = '1'; | |
| widthSlider.style.cursor = 'pointer'; | |
| widthInput = document.createElement('input'); | |
| widthInput.type = 'number'; | |
| widthInput.min = MIN_WIDTH_PX; | |
| widthInput.max = MAX_WIDTH_PX; | |
| widthInput.value = config.maxWidthPx; | |
| Object.assign(widthInput.style, { width: '70px', padding: '4px', background: '#343541', color: '#fff', border: '1px solid #565869', borderRadius: '4px' }); | |
| widthControlsDiv.appendChild(widthLabel); | |
| widthControlsDiv.appendChild(widthSlider); | |
| widthControlsDiv.appendChild(widthInput); | |
| settingsPanel.appendChild(widthControlsDiv); | |
| // 3. Justify Toggle | |
| const justifyDiv = document.createElement('div'); | |
| justifyCheckbox = document.createElement('input'); | |
| justifyCheckbox.type = 'checkbox'; | |
| justifyCheckbox.id = 'fe-justify-toggle'; | |
| justifyCheckbox.checked = config.justifyEnabled; | |
| const justifyLabel = document.createElement('label'); | |
| justifyLabel.htmlFor = 'fe-justify-toggle'; | |
| justifyLabel.textContent = ' Justify Text'; | |
| justifyLabel.style.marginLeft = '8px'; | |
| justifyLabel.style.cursor = 'pointer'; | |
| justifyDiv.appendChild(justifyCheckbox); | |
| justifyDiv.appendChild(justifyLabel); | |
| settingsPanel.appendChild(justifyDiv); | |
| document.body.appendChild(settingsPanel); | |
| // --- Event Listeners --- | |
| const updateUIState = () => { | |
| const isCustom = !defaultWidthCheckbox.checked; | |
| widthSlider.disabled = !isCustom; | |
| widthInput.disabled = !isCustom; | |
| widthSlider.style.opacity = isCustom ? 1 : 0.5; | |
| widthInput.style.opacity = isCustom ? 1 : 0.5; | |
| widthLabel.style.opacity = isCustom ? 1 : 0.5; | |
| }; | |
| defaultWidthCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(USE_DEFAULT_WIDTH_KEY, e.target.checked); | |
| // If reverting to default, we must completely clear the injected CSS variables | |
| // from the root element so WordPress styles take over again | |
| if (e.target.checked) { | |
| const root = document.documentElement; | |
| root.style.removeProperty('--wp--style--global--content-size'); | |
| root.style.removeProperty('--wp--style--global--wide-size'); | |
| // Clear any direct overrides that might have been applied natively to .entry-content | |
| const entryContent = document.querySelector('.entry-content'); | |
| if (entryContent) { | |
| entryContent.style.removeProperty('max-width'); | |
| entryContent.style.removeProperty('width'); | |
| } | |
| } | |
| applyStyles(); | |
| updateUIState(); | |
| }); | |
| // Slider input (Live update logic) | |
| widthSlider.addEventListener('input', (e) => { | |
| const val = parseInt(e.target.value, 10); | |
| widthInput.value = val; | |
| widthLabel.textContent = `${val}px`; | |
| config.maxWidthPx = val; | |
| if (!config.useDefaultWidth) applyStyles(); | |
| }); | |
| // Slider change (Save logic) | |
| widthSlider.addEventListener('change', async (e) => { | |
| await saveSetting(WIDTH_PX_KEY, parseInt(e.target.value, 10)); | |
| }); | |
| // Text Input | |
| widthInput.addEventListener('change', async (e) => { | |
| let val = parseInt(e.target.value, 10); | |
| // Clamp value | |
| if (isNaN(val)) val = DEFAULT_WIDTH_PX; | |
| if (val < MIN_WIDTH_PX) val = MIN_WIDTH_PX; | |
| if (val > MAX_WIDTH_PX) val = MAX_WIDTH_PX; | |
| // Update UI to match clamped value | |
| e.target.value = val; | |
| widthSlider.value = val; | |
| widthLabel.textContent = `${val}px`; | |
| // Apply and Save | |
| config.maxWidthPx = val; | |
| if (!config.useDefaultWidth) applyStyles(); | |
| await saveSetting(WIDTH_PX_KEY, val); | |
| }); | |
| justifyCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(JUSTIFY_KEY, e.target.checked); | |
| applyStyles(); | |
| }); | |
| // Initialize state | |
| updateUIState(); | |
| setTimeout(() => document.addEventListener('click', handleClickOutside, true), 100); | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId) GM_unregisterMenuCommand(menuCommandId); | |
| const label = config.settingsUiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; | |
| menuCommandId = GM_registerMenuCommand(label, async () => { | |
| const newState = !config.settingsUiVisible; | |
| await saveSetting(SETTINGS_UI_VISIBLE_KEY, newState); | |
| if (newState) createSettingsUI(); | |
| else removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Main --- | |
| async function main() { | |
| await loadSettings(); | |
| applyStyles(); | |
| if (config.settingsUiVisible) createSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| main(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment