AtCoder Problems の Problem List について、検索対象の列をチェックボックスで制限します。
// ==UserScript==
// @name AtCoder Problems Column Limited Search
// @namespace http://tampermonkey.net/
// @version 2026.07.07
// @description AtCoder Problems の Problem List について、検索対象の列をチェックボックスで制限します。
// @author Not_Leonian
// @match https://kenkoooo.com/atcoder*
// @icon https://www.google.com/s2/favicons?domain=https://kenkoooo.com/atcoder
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
"use strict";
const CONTROL_ID = "acp-column-limited-search-controls";
const STYLE_ID = "acp-column-limited-search-style";
const STORAGE_KEY = "acp.columnLimitedSearch.selectedFields.v1";
const INF_LIKE = 9e17;
const FIELD_LABEL = {
contestDate: "Date",
title: "Problem",
contest: "Contest",
status: "Result",
lastAcceptedDate: "Last AC Date",
solverCount: "Solvers",
point: "Point",
problemModel: "Difficulty",
solveProbability: "Solve Prob",
timeEstimation: "Time",
executionTime: "Fastest",
codeLength: "Shortest",
firstUserId: "First",
};
const selectedFieldsByTable = new WeakMap();
let nextTableInstanceId = 1;
function getTableInstanceId(tableInstance) {
if (!tableInstance.__acpColumnLimitedSearchInstanceId) {
Object.defineProperty(
tableInstance,
"__acpColumnLimitedSearchInstanceId",
{
value: String(nextTableInstanceId++),
enumerable: false,
configurable: false,
},
);
}
return tableInstance.__acpColumnLimitedSearchInstanceId;
}
function isProblemListPage() {
return /^#\/list(?:\/|\?|$)/.test(window.location.hash || "");
}
function normalizeText(value) {
return String(value == null ? "" : value)
.replace(/\s+/g, " ")
.trim();
}
function finiteNumberText(value) {
return typeof value === "number" &&
Number.isFinite(value) &&
value < INF_LIKE
? String(value)
: "";
}
function collectionToText(value) {
if (value == null) return "";
if (value instanceof Set) return Array.from(value).join(" ");
if (Array.isArray(value)) return value.map(collectionToText).join(" ");
return String(value);
}
function genericObjectText(value) {
if (value == null) return "";
if (typeof value !== "object") return String(value);
const parts = [];
for (const key of [
"id",
"title",
"name",
"contest_id",
"problem_id",
"problem_index",
"result",
"user_id",
]) {
if (value[key] != null) parts.push(String(value[key]));
}
return parts.join(" ");
}
function statusToText(status) {
if (status == null || typeof status !== "object")
return genericObjectText(status);
const parts = [];
switch (status.label) {
case 0:
parts.push("AC", "Accepted", "Success");
break;
case 1:
parts.push("Trying", "Failed");
break;
case 2:
parts.push("Warning", status.result || "");
break;
case 3:
parts.push("None");
break;
default:
parts.push(genericObjectText(status));
break;
}
if (status.solvedRivals) parts.push(collectionToText(status.solvedRivals));
if (status.solvedLanguages)
parts.push(collectionToText(status.solvedLanguages));
if (status.submittedLanguages)
parts.push(collectionToText(status.submittedLanguages));
return parts.join(" ");
}
function problemModelToText(model) {
if (model == null || typeof model !== "object") return "";
const parts = [];
if (typeof model.difficulty === "number")
parts.push(String(model.difficulty));
if (typeof model.rawDifficulty === "number")
parts.push(String(Math.round(model.rawDifficulty)));
if (typeof model.discrimination === "number")
parts.push(String(model.discrimination));
if (typeof model.slope === "number") parts.push(String(model.slope));
if (typeof model.intercept === "number")
parts.push(String(model.intercept));
return parts.join(" ");
}
function searchValueForField(row, field) {
switch (field) {
case "contestDate":
return row.contestDate;
case "title":
return [row.title, row.id, row.mergedProblem?.id].join(" ");
case "contest":
return [
row.contestTitle,
row.contest?.id,
row.contest?.title,
row.mergedProblem?.contest_id,
].join(" ");
case "status":
return statusToText(row.status);
case "lastAcceptedDate":
return row.lastAcceptedDate;
case "solverCount":
return finiteNumberText(row.solverCount);
case "point":
return (
finiteNumberText(row.point) || (row.point >= INF_LIKE ? "-" : "")
);
case "problemModel":
return problemModelToText(row.problemModel) || "-";
case "solveProbability":
return collectionToText(
row.solveProbability || row.solveProb || row.probability,
);
case "timeEstimation":
return collectionToText(
row.timeEstimation || row.solveTime || row.predictedSolveTime,
);
case "executionTime":
return [finiteNumberText(row.executionTime), row.fastestUserId].join(
" ",
);
case "codeLength":
return [finiteNumberText(row.codeLength), row.shortestUserId].join(" ");
case "firstUserId":
return row.firstUserId;
default:
return genericObjectText(row[field]);
}
}
function getReactFiber(node) {
if (!node) return null;
const key = Object.keys(node).find(
(k) =>
k.startsWith("__reactFiber$") ||
k.startsWith("__reactInternalInstance$"),
);
return key ? node[key] : null;
}
function isBootstrapTableInstance(value) {
return (
value?.store &&
value.colInfos &&
typeof value.handleSearch === "function" &&
typeof value.getTableData === "function"
);
}
function findBootstrapTableInstance(fromNode) {
for (let node = fromNode; node; node = node.parentElement) {
let fiber = getReactFiber(node);
while (fiber) {
if (isBootstrapTableInstance(fiber.stateNode)) return fiber.stateNode;
fiber = fiber.return;
}
}
return null;
}
function patchSearch(tableInstance) {
const store = tableInstance?.store;
if (!store || store.__acpColumnLimitedSearchPatched) return;
const originalSearch = store._search.bind(store);
Object.defineProperty(store, "__acpColumnLimitedSearchPatched", {
value: true,
enumerable: false,
configurable: false,
});
Object.defineProperty(store, "__acpColumnLimitedSearchOriginalSearch", {
value: originalSearch,
enumerable: false,
configurable: false,
});
store._search = function acpColumnLimitedSearch(source) {
const rawSearchText =
this.searchText == null ? "" : String(this.searchText);
const searchText = rawSearchText.trim().toLowerCase();
const selectedFields = selectedFieldsByTable.get(tableInstance);
if (!Array.isArray(selectedFields)) {
originalSearch(source);
return;
}
if (searchText === "") {
this.filteredData = source;
this.isOnFilter = true;
return;
}
if (selectedFields.length === 0) {
this.filteredData = [];
this.isOnFilter = true;
return;
}
this.filteredData = source.filter((row) =>
selectedFields.some((field) =>
normalizeText(searchValueForField(row, field))
.toLowerCase()
.includes(searchText),
),
);
this.isOnFilter = true;
};
}
function findProblemListTable() {
return Array.from(document.querySelectorAll("table")).find(
(table) =>
table.querySelector('th[data-field="title"]') &&
table.querySelector('th[data-field="contestDate"]'),
);
}
function isVisible(element) {
const style = window.getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden";
}
function collectVisibleColumns(table) {
const seen = new Set();
return Array.from(table.querySelectorAll("thead th[data-field]"))
.filter(isVisible)
.map((th) => {
const field = th.getAttribute("data-field");
const label =
FIELD_LABEL[field] || normalizeText(th.textContent || field);
return { field, label };
})
.filter((column) => {
if (!column.field || seen.has(column.field)) return false;
seen.add(column.field);
return true;
});
}
function loadSelectedFields(availableFields) {
try {
const parsed = JSON.parse(
window.localStorage.getItem(STORAGE_KEY) || "null",
);
if (Array.isArray(parsed)) {
return availableFields.filter((field) => parsed.includes(field));
}
} catch (_) {
// Ignore broken localStorage data.
}
return availableFields.slice();
}
function saveSelectedFields(selectedFields) {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(selectedFields));
} catch (_) {
// localStorage can be unavailable in strict browser configurations.
}
}
function findSearchInput(table) {
const container =
table.closest(".react-bs-table-container") || table.parentElement;
return (
container?.querySelector(
'.react-bs-table-search-form input[type="text"], input[placeholder="Search"]',
) ||
document.querySelector(
'.react-bs-table-search-form input[type="text"], input[placeholder="Search"]',
)
);
}
function refreshSearch(tableInstance, input) {
const value = input ? input.value : tableInstance.store.searchText || "";
if (typeof tableInstance.handleSearch === "function") {
tableInstance.handleSearch(value);
} else if (input) {
input.dispatchEvent(new Event("input", { bubbles: true }));
}
}
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${CONTROL_ID} {
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
margin: 0.5rem 0;
padding: 0.5rem 0.75rem;
background: rgba(0, 0, 0, 0.02);
font-size: 0.875rem;
}
#${CONTROL_ID} .acp-cls-title {
font-weight: 600;
margin-right: 0.75rem;
white-space: nowrap;
}
#${CONTROL_ID} .acp-cls-body {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.75rem;
}
#${CONTROL_ID} label {
margin: 0;
white-space: nowrap;
user-select: none;
}
#${CONTROL_ID} input[type="checkbox"] {
margin-right: 0.25rem;
vertical-align: middle;
}
#${CONTROL_ID} button {
padding: 0.1rem 0.45rem;
line-height: 1.4;
}
#${CONTROL_ID} .acp-cls-count {
color: #666;
white-space: nowrap;
}
`;
document.head.appendChild(style);
}
function renderControls(table, tableInstance, columns) {
ensureStyle();
const availableFields = columns.map((column) => column.field);
const selectedFields = loadSelectedFields(availableFields);
selectedFieldsByTable.set(tableInstance, selectedFields);
const old = document.getElementById(CONTROL_ID);
if (old) old.remove();
const root = document.createElement("div");
root.id = CONTROL_ID;
const body = document.createElement("div");
body.className = "acp-cls-body";
root.appendChild(body);
const title = document.createElement("span");
title.className = "acp-cls-title";
title.textContent = "Column Limited Search:";
body.appendChild(title);
const inputs = [];
for (const column of columns) {
const label = document.createElement("label");
const input = document.createElement("input");
input.type = "checkbox";
input.dataset.field = column.field;
input.checked = selectedFields.includes(column.field);
inputs.push(input);
label.appendChild(input);
label.appendChild(document.createTextNode(column.label));
body.appendChild(label);
}
const allButton = document.createElement("button");
allButton.type = "button";
allButton.className = "btn btn-sm btn-outline-secondary";
allButton.textContent = "All";
body.appendChild(allButton);
const noneButton = document.createElement("button");
noneButton.type = "button";
noneButton.className = "btn btn-sm btn-outline-secondary";
noneButton.textContent = "None";
body.appendChild(noneButton);
const count = document.createElement("span");
count.className = "acp-cls-count";
body.appendChild(count);
const searchInput = findSearchInput(table);
const apply = () => {
const nextSelectedFields = inputs
.filter((input) => input.checked)
.map((input) => input.dataset.field)
.filter(Boolean);
selectedFieldsByTable.set(tableInstance, nextSelectedFields);
saveSelectedFields(nextSelectedFields);
count.textContent = `${nextSelectedFields.length}/${inputs.length} selected`;
refreshSearch(tableInstance, searchInput);
};
for (const input of inputs) {
input.addEventListener("change", apply);
}
allButton.addEventListener("click", () => {
inputs.forEach((input) => {
input.checked = true;
});
apply();
});
noneButton.addEventListener("click", () => {
inputs.forEach((input) => {
input.checked = false;
});
apply();
});
count.textContent = `${selectedFields.length}/${inputs.length} selected`;
const searchForm =
searchInput &&
(searchInput.closest(".react-bs-table-search-form") ||
searchInput.parentElement);
if (searchForm?.parentElement) {
searchForm.parentElement.insertBefore(root, searchForm.nextSibling);
} else {
const container =
table.closest(".react-bs-table-container") || table.parentElement;
container.parentElement.insertBefore(root, container);
}
}
function install() {
if (!isProblemListPage()) {
const old = document.getElementById(CONTROL_ID);
if (old) old.remove();
return;
}
const table = findProblemListTable();
if (!table) return;
const tableInstance = findBootstrapTableInstance(table);
if (!tableInstance) return;
const columns = collectVisibleColumns(table);
if (columns.length === 0) return;
patchSearch(tableInstance);
const existing = document.getElementById(CONTROL_ID);
const existingSignature = existing?.dataset.signature;
const nextSignature = `${getTableInstanceId(tableInstance)}:${columns
.map((column) => column.field)
.join("|")}`;
if (existing && existingSignature === nextSignature) {
selectedFieldsByTable.set(
tableInstance,
loadSelectedFields(columns.map((c) => c.field)),
);
return;
}
renderControls(table, tableInstance, columns);
const rendered = document.getElementById(CONTROL_ID);
if (rendered) rendered.dataset.signature = nextSignature;
}
let scheduled = false;
function scheduleInstall() {
if (scheduled) return;
scheduled = true;
window.setTimeout(() => {
scheduled = false;
install();
}, 200);
}
window.addEventListener("hashchange", scheduleInstall);
window.addEventListener("popstate", scheduleInstall);
const observer = new MutationObserver(scheduleInstall);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
scheduleInstall();
})();