Created
September 22, 2024 08:22
-
-
Save richdougherty/dd11a74a8daab2fde99026dd4e8ec3ac to your computer and use it in GitHub Desktop.
Connections Categoriser userscript
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 Connections Categoriser | |
// @description Adds a category function to the NYT Connections game | |
// @match https://www.nytimes.com/games/connections | |
// @namespace http://tampermonkey.net/ | |
// @version 0.1.0 | |
// @grant none | |
// ==/UserScript== | |
// MIT License | |
// | |
// Copyright (c) 2024 Rich Dougherty | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
(function() { | |
'use strict'; | |
// =============== CATEGORISER BUSINESS LOGIC =============== | |
const categories = [ | |
{ symbol: '🅐', color: '#ff4136' }, | |
{ symbol: '🅑', color: '#ff851b' }, | |
{ symbol: '🅒', color: '#ffdc00' }, | |
{ symbol: '🅓', color: '#2ecc40' }, | |
{ symbol: '🅔', color: '#0074d9' }, | |
{ symbol: '🅕', color: '#b10dc9' } | |
]; | |
const MAX_CATEGORIES_ON_CARD = 6; | |
function log(message) { | |
console.log(`[Category Mode] ${message}`); | |
} | |
// Function to reliably query elements by class prefix | |
function queryByClassPrefix(prefix) { | |
const elements = document.querySelectorAll(`[class^="${prefix}"]`); | |
return elements.length > 0 ? elements[0] : null; | |
} | |
function createCategoryModeUI() { | |
const existingUI = document.getElementById('category-mode'); | |
if (existingUI) { | |
existingUI.remove(); | |
} | |
const categoryModeDiv = document.createElement('div'); | |
categoryModeDiv.id = 'category-mode'; | |
categoryModeDiv.style.cssText = ` | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 10px; | |
font-family: 'Libre Franklin', sans-serif; | |
`; | |
// Add 'Select' mode | |
const selectLabel = createRadioLabel('select', 'Select', '#000000'); | |
categoryModeDiv.appendChild(selectLabel); | |
// Add category options | |
categories.forEach((category, index) => { | |
const label = createRadioLabel(index.toString(), category.symbol, category.color); | |
categoryModeDiv.appendChild(label); | |
}); | |
return categoryModeDiv; | |
} | |
function createRadioLabel(value, symbol, color) { | |
const label = document.createElement('label'); | |
label.style.cssText = ` | |
display: inline-flex; | |
align-items: center; | |
margin: 0 10px; | |
cursor: pointer; | |
`; | |
const radio = document.createElement('input'); | |
radio.type = 'radio'; | |
radio.name = 'category'; | |
radio.value = value; | |
radio.id = `category-${value}`; | |
radio.style.cssText = ` | |
appearance: none; | |
width: 20px; | |
height: 20px; | |
border: 2px solid ${color}; | |
border-radius: 50%; | |
margin-right: 5px; | |
cursor: pointer; | |
outline: none; | |
position: relative; | |
`; | |
radio.addEventListener('change', function() { | |
document.querySelectorAll('#category-mode input[type="radio"]').forEach(r => { | |
r.nextElementSibling.style.fontWeight = 'normal'; | |
r.style.backgroundColor = 'transparent'; | |
}); | |
this.nextElementSibling.style.fontWeight = 'bold'; | |
this.style.backgroundColor = color; | |
}); | |
if (value === 'select') { | |
radio.checked = true; | |
setTimeout(() => radio.dispatchEvent(new Event('change')), 0); | |
} | |
const span = document.createElement('span'); | |
span.textContent = symbol; | |
span.style.cssText = ` | |
font-size: 16px; | |
color: ${color}; | |
`; | |
label.appendChild(radio); | |
label.appendChild(span); | |
return label; | |
} | |
function insertCategoryModeUI() { | |
const categoryModeDiv = createCategoryModeUI(); | |
const gameBoard = queryByClassPrefix('Board-module_boardActionGroup__'); | |
if (gameBoard && gameBoard.parentNode) { | |
// Create a wrapper div that won't be affected by React re-renders | |
const wrapperDiv = document.createElement('div'); | |
wrapperDiv.id = 'category-mode-wrapper'; | |
wrapperDiv.appendChild(categoryModeDiv); | |
// Insert the wrapper after the game board | |
gameBoard.parentNode.insertBefore(wrapperDiv, gameBoard.nextSibling); | |
log('Category Mode UI created and injected'); | |
} else { | |
log('Game board not found, unable to insert Category Mode UI'); | |
} | |
} | |
function handleGameEvent(event) { | |
// Exit early if the event type is not pointerdown | |
if (event.type !== 'pointerdown') return; | |
const card = event.target.closest('[data-testid="card-label"]'); | |
if (!card) return; | |
const currentMode = document.querySelector('input[name="category"]:checked').value; | |
log(`Pointerdown event on card with mode ${currentMode}`); | |
if (currentMode !== 'select') { | |
event.preventDefault(); | |
event.stopPropagation(); | |
log(`Default behavior prevented for pointerdown event`); | |
toggleCategorySymbol(card, parseInt(currentMode)); | |
const checkbox = card.querySelector('input[type="checkbox"]'); | |
if (checkbox) { | |
log(`Current checkbox state: ${checkbox.checked}`); | |
} | |
logSubmitButtonState(); | |
return false; | |
} else { | |
log(`Normal mode (Select) - allowing default behavior for pointerdown event`); | |
const checkbox = card.querySelector('input[type="checkbox"]'); | |
if (checkbox) { | |
log(`Current checkbox state: ${checkbox.checked}`); | |
} | |
logSubmitButtonState(); | |
} | |
} | |
function logSubmitButtonState() { | |
const submitButton = queryByClassPrefix('ActionButton-module_button__'); | |
if (submitButton) { | |
log(`Submit button enabled: ${!submitButton.disabled}`); | |
} else { | |
log('Submit button not found'); | |
} | |
} | |
function toggleCategorySymbol(card, categoryIndex) { | |
const existingSymbol = card.querySelector(`.category-symbol[data-category="${categoryIndex}"]`); | |
if (existingSymbol) { | |
existingSymbol.remove(); | |
log(`Removed symbol for category index ${categoryIndex} from card`); | |
} else { | |
const category = categories[categoryIndex]; | |
const symbolSpan = document.createElement('span'); | |
symbolSpan.className = 'category-symbol'; | |
symbolSpan.dataset.category = categoryIndex; | |
symbolSpan.textContent = category.symbol; | |
symbolSpan.style.color = category.color; | |
card.appendChild(symbolSpan); | |
log(`Added symbol for category index ${categoryIndex} to card`); | |
} | |
updateCategorySymbolPositions(card); | |
} | |
function updateCategorySymbolPositions(card) { | |
const cardWidth = card.offsetWidth; | |
const symbolWidth = 20; // Approximate width of the symbol | |
const margin = 5; // Margin from the edges | |
const availableWidth = cardWidth - (2 * margin); | |
const numCategories = Math.min(categories.length, MAX_CATEGORIES_ON_CARD); | |
const spacing = (numCategories > 1) ? (availableWidth - (numCategories * symbolWidth)) / (numCategories - 1) : 0; | |
categories.forEach((category, index) => { | |
if (index < MAX_CATEGORIES_ON_CARD) { | |
const existingSymbol = card.querySelector(`.category-symbol[data-category="${index}"]`); | |
if (existingSymbol) { | |
const left = margin + (index * (symbolWidth + spacing)); | |
existingSymbol.style.cssText = ` | |
position: absolute; | |
bottom: 5px; | |
left: ${left}px; | |
font-size: 18px; | |
color: ${category.color}; | |
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; | |
pointer-events: none; | |
z-index: 1000; | |
`; | |
} | |
} | |
}); | |
} | |
function initCategoryMode() { | |
log('Initializing Category Mode'); | |
insertCategoryModeUI(); | |
document.addEventListener('pointerdown', handleGameEvent, true); | |
log('Category Mode initialized'); | |
} | |
function setupMutationObserver() { | |
const targetNode = document.body; | |
const config = { childList: true, subtree: true }; | |
const callback = function(mutationsList, observer) { | |
for(let mutation of mutationsList) { | |
if (mutation.type === 'childList') { | |
if (!document.getElementById('category-mode-wrapper')) { | |
log('Category Mode UI not found, re-inserting...'); | |
insertCategoryModeUI(); | |
} | |
// Update positions of category symbols on all cards | |
document.querySelectorAll('[data-testid="card-label"]').forEach(updateCategorySymbolPositions); | |
} | |
} | |
}; | |
const observer = new MutationObserver(callback); | |
observer.observe(targetNode, config); | |
} | |
function runScript() { | |
if (queryByClassPrefix('Board-module_board__')) { | |
initCategoryMode(); | |
setupMutationObserver(); | |
} else { | |
log('Game board not found, waiting...'); | |
setTimeout(runScript, 1000); | |
} | |
} | |
// Start the script | |
if (document.readyState === 'loading') { | |
document.addEventListener('DOMContentLoaded', runScript); | |
} else { | |
runScript(); | |
} | |
log('Script fully loaded and executed'); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment