Skip to content

Instantly share code, notes, and snippets.

@richdougherty
Created September 22, 2024 08:22
Show Gist options
  • Save richdougherty/dd11a74a8daab2fde99026dd4e8ec3ac to your computer and use it in GitHub Desktop.
Save richdougherty/dd11a74a8daab2fde99026dd4e8ec3ac to your computer and use it in GitHub Desktop.
Connections Categoriser userscript
// ==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