// ==UserScript== // @name Chess Compass Analysis for Chess.com // @namespace AndyVuj24 // @match https://www.chess.com/* // @run-at document-end // @grant none // @version 1.3.0 // @author AndyVuj24 // @description This plugin adds buttons next to the chess board allowing for a quick post-game analysis of the current game on screen // @supportURL https://github.com/andyvuj24/Chess-Compass-Analysis-for-Chess.com/issues // @homepageURL https://github.com/andyvuj24/Chess-Compass-Analysis-for-Chess.com // @downloadURL https://update.greasyfork.icu/scripts/416737/Chess%20Compass%20Analysis%20for%20Chesscom.user.js // @updateURL https://update.greasyfork.icu/scripts/416737/Chess%20Compass%20Analysis%20for%20Chesscom.meta.js // ==/UserScript== var counter = 0; const $ = document.querySelector.bind(document); const $$ = document.querySelectorAll.bind(document); const log = (message, ...data) => { if (data.length > 0) { return console.log(`[Chess.com Plugin Log]: ${message}`, data); } return console.log(`[Chess.com Plugin Log]: ${message}`); }; const isElement = (element) => { return element instanceof Element; }; const isQueryString = (query) => { return ( !isElement(query) && (typeof query == "string" || query instanceof String) ); }; const getDOMElement = (request) => { if (isElement(request)) { return request; } else if (isQueryString(request)) { return $(request); } return null; }; const addStyling = async () => { // button styling log("Adding styling to page for plugin buttons"); const styleElement = document.createElement("style"); styleElement.innerHTML = `.gf-chess-compass-button-container{margin:auto;display:flex;align-items:center;justify-content:center;border-radius:3px;color:#fff;font-size:16px}.gf-chess-compass-button-container>a{width:100%}.gf-chess-compass-button{background-color:#489e5d;width:100%;margin:auto;height:40px;display:flex;align-items:center;justify-content:center;border-radius:3px 3px 0 0;color:#fff;cursor:pointer;font-size:16px;font-weight:500;}.gf-chess-compass-button:hover{background-color:#57b26e}`; document.head?.appendChild?.(styleElement); }; const addButtons = async (element) => { // for button element const target = getDOMElement(element); if (target === null) { log("Failed to add buttons to target: ", target); return; } log("Adding buttons to sidebar"); target?.insertAdjacentHTML( "beforebegin", '
' ); target?.insertAdjacentHTML( "beforebegin", '
' ); }; const setupButton = async (id) => { log(`Configuring button -> ${id}`); const btn = $(id); if (id.indexOf("PGN") !== -1) { btn.addEventListener("click", function () { const data = ($("chess-board") ?.game.getPGN?.() .replace(/\[[^\]]*\]|\{[^\}]*\}/g, "") || [ ...$$( "div.vertical-move-list-component span.vertical-move-list-column:not(.move-timestamps-component)" ), ] .map(({ innerText }) => innerText) .join(" ") .replace(/\[[^\]]*\]|\{[^\}]*\}/g, "")) ?? null; if (!data) { log("Unable to find data for PGN"); return; } log("PGN: ", data); fetch("https://www.chesscompass.com/api/get_game_id", { method: "post", body: JSON.stringify({ gameData: data, }), }) .then((response) => { return response.json(); }) .then(({ gameId }) => { window.open( "https://www.chesscompass.com/analyze/" + gameId, "_blank" ); }); }); } if (id.indexOf("FEN") !== -1) { btn.addEventListener("click", function () { const data = ($("chess-board")?.game.getFEN?.() || $("div.v-board")?.getChessboardInstance?.().state.selectedNode.fen) ?? null; if (!data) { log("Unable to find data for FEN"); return; } log("FEN: " + data); fetch("https://www.chesscompass.com/api/get_game_id", { method: "post", body: JSON.stringify({ gameData: data, }), }) .then((response) => { return response.json(); }) .then(({ gameId }) => { window.open( "https://www.chesscompass.com/analyze/" + gameId, "_blank" ); }); }); } }; const waitForContainer = async () => { const selectors = [ ".sidebar-component", ".sidebar-v5-component", "vertical-move-list", ]; return new Promise((resolve, reject) => { for (const selector of selectors) { log("Initially trying: ", selector); const element = $(selector); if (element) { log("Found container: ", element); resolve(main(selector)); return; } } const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const nodes = Array.from(mutation.addedNodes); for (const node of nodes) { for (const selector of selectors) { if (node.matches && node.matches(selector)) { log("Observer found container: ", node); observer.disconnect(); resolve(main(selector)); return; } } } }); }); log("Made container observer"); observer.observe(document.documentElement, { childList: true, subtree: true, }); }); }; const clearAds = async () => { log("Clearing ads..."); const adsToRemove = [ "#tall-sidebar-ad", "#adblocker-check", "#board-layout-ad", ]; adsToRemove.forEach((selector) => { $$(selector).forEach((el) => { el.remove(); log("Removed Ad: ", el); }); }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const nodes = Array.from(mutation.addedNodes); for (const node of nodes) { for (const selector of adsToRemove) { if (node.matches && node.matches(selector)) { node.remove(); } } } }); }); observer.observe(document.documentElement, { childList: true, subtree: true, }); log("Ads cleared!"); }; async function main(element) { await addStyling(); await addButtons(element); await setupButton("#btnPGN"); await setupButton("#btnFEN"); await clearAds(); } if (["complete", "interactive"].indexOf(document.readyState) > -1) { waitForContainer(); } else { document.addEventListener("DOMContentLoaded", waitForContainer); }