Skip to content

Instantly share code, notes, and snippets.

@DaniGuardiola
Last active February 24, 2023 18:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DaniGuardiola/dca6b75c5143e05247b05918dc23aa9f to your computer and use it in GitHub Desktop.
Save DaniGuardiola/dca6b75c5143e05247b05918dc23aa9f to your computer and use it in GitHub Desktop.
Khan Academy - math filter
/*
This is a script that can be dropped into the console of the Khan Academy
math page to filter their many courses by category.
It will also fetch and display the progress of the user on each course.
Usage: load https://khanacademy.org/math, open the console (F12), paste the
code, and hit enter.
You can customize the filters or the initially visible categories by editing
the config section below.
Pro tip: you can also save this script as a bookmarklet and run it from the
bookmark bar. To do this, create a new bookmark and set the following as the
URL:
javascript:(function()%7B(function()%7Bvar%20d%3Ddocument%2Cs%3Ddocument.createElement('script')%3Bs.src%3D%22https%3A%2F%2Fgist.githack.com%2FDaniGuardiola%2Fdca6b75c5143e05247b05918dc23aa9f%2Fraw%2Fe8f79465736d8a273c092fae6e70612ed385f073%2Fkhan-academy-math-filter.js%22%3Bd.body.appendChild(s)%3B%7D)()%7D)()
*/
{
// config
// ------
const FILTERS = [
{
id: "early-math",
name: "Early math",
regex: /^(?:Early math review|Kindergarten)$/,
},
{
id: "grades",
name: "Grades",
regex: /^[0-9](?:st|nd|rd|th) grade$/,
},
{
id: "topics",
name: "Topics",
regex: new RegExp(
`^(?:${[
"Arithmetic",
"Basic geometry and measurement",
"Pre-algebra",
"Algebra basics",
"Algebra 1",
"Algebra 2",
"Trigonometry",
"Statistics and probability",
"Precalculus",
"Differential Calculus",
"Integral Calculus",
"Calculus 1",
"Calculus 2",
"Multivariable calculus",
"Differential equations",
"Linear algebra",
].join("|")})$`
),
},
{
id: "high-school",
name: "High school",
regex: /^High school [a-z]+$/,
},
{
id: "college",
name: "College",
regex: /^College [a-zA-Z]+$/,
},
{
id: "college-ap",
name: "College (AP)",
regex: /^AP®︎\/College [a-zA-Z ]+$/,
},
{
id: "illustrative-math",
name: "Illustrative Mathematics",
regex: /^[a-zA-Z0-9 ]+ \(Illustrative Mathematics\)$/,
},
{
id: "eureka-math-engage-ny",
name: "Eureka Math/EngageNY",
regex: /^[a-zA-Z0-9 ]+ \(Eureka Math\/EngageNY\)$/,
},
{
id: "integrated-math",
name: "Integrated math",
regex: /^Integrated math [1-3]$/,
},
{
id: "all-content",
name: "All content",
regex: /^[a-zA-Z]+ \(all content\)+$/,
},
{
id: "get-ready",
name: "Get ready",
regex: /^Get ready for [a-zA-Z0-9 ]+$/,
},
{
id: "map",
name: "MAP",
regex: /^MAP Recommended Practice$/,
},
{
id: "get-ready-ap",
name: "Get ready (AP)",
regex: /^Get ready for AP® [a-zA-Z]+$/,
},
{
id: "fl-best",
name: "FL B.E.S.T.",
regex: /^[a-zA-Z0-9 ]+\(FL B.E.S.T.\)$/,
},
{
id: "india",
name: "India",
regex: /^High school math \(India\)$/,
},
];
const INITIALLY_VISIBLE_IDS = ["topics"];
// code
// ----
/*
Bookmarklet code:
(function(){
var d=document,s=document.createElement('script');
s.src="https://gist.githack.com/DaniGuardiola/dca6b75c5143e05247b05918dc23aa9f/raw/e8f79465736d8a273c092fae6e70612ed385f073/khan-academy-math-filter.js";
d.body.appendChild(s);
})()
*/
const INITIALLY_HIDDEN_IDS = FILTERS.filter(
({ id }) => !INITIALLY_VISIBLE_IDS.includes(id)
).map(({ id }) => id);
const TARGET_CONTAINER_SELECTOR =
'[data-test-id="curation-page"] > div:nth-child(2) > div';
const TOPIC_SELECTOR = "div";
const TOPIC_TITLE_SELECTOR = "h2 a";
const FILTER_PARENT_CLASS = "khan-academy-math-filter";
const STYLE_DATA_ATTR = "data-khan-academy-math-filter";
const STYLE = `
.${FILTER_PARENT_CLASS} {
background-color: #f7f8fa;
padding-top: 32px;
padding-bottom: 32px;
user-select: none;
}
.${FILTER_PARENT_CLASS} + div {
padding-top: 0 !important;
}
.${FILTER_PARENT_CLASS} .wrapper {
width: 1160px;
max-width: 100%;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.${FILTER_PARENT_CLASS} > label {
display: flex;
align-items: center;
font-size: 16px;
gap: 4px;
}
@media (min-width: 1024px) {
.${FILTER_PARENT_CLASS} {
padding-left: 20px;
padding-right: 20px;
}
}
.progress {
margin-left: 8px;
color: black;
font-weight: 800;
}
`.trim();
const PROGRESS_ENDPOINT_URL =
"https://www.khanacademy.org/api/internal/graphql/getLearnMenuProgress";
const STATE = Object.fromEntries(FILTERS.map(({ id }) => [id, true]));
function getTopics() {
const topics = [];
const topicEls = [
...document.querySelectorAll(
`${TARGET_CONTAINER_SELECTOR} > ${TOPIC_SELECTOR}`
),
];
topicEls.forEach((el) => {
const titleEl = el.querySelector(TOPIC_TITLE_SELECTOR);
if (!titleEl) return;
const title = titleEl.textContent.trim();
const slug = titleEl.href.split("/").pop();
if (!title) return;
topics.push({ el, title, titleEl, slug });
});
return topics;
}
function updateTopics() {
const topics = getTopics();
FILTERS.forEach(({ id, regex }) => {
const state = STATE[id];
topics.forEach(({ el, title }) => {
if (regex.test(title)) {
if (state) {
el.style.display = "";
} else {
el.style.display = "none";
}
}
});
});
}
function setState(id, state) {
STATE[id] = state;
}
function createFilterEls() {
return FILTERS.map(({ id, name }) => {
const filterEl = document.createElement("label");
const checkboxEl = document.createElement("input");
checkboxEl.type = "checkbox";
checkboxEl.checked = INITIALLY_VISIBLE_IDS.includes(id);
checkboxEl.addEventListener("change", () => {
setState(id, checkboxEl.checked);
updateTopics();
});
filterEl.appendChild(checkboxEl);
filterEl.appendChild(document.createTextNode(name));
return filterEl;
});
}
function hideInitial() {
INITIALLY_HIDDEN_IDS.forEach((id) => setState(id, false));
updateTopics();
}
function load() {
const targetContainerEl = document.querySelector(TARGET_CONTAINER_SELECTOR);
if (!targetContainerEl) throw new Error("Target container not found");
const filterParentEl = document.createElement("div");
filterParentEl.classList.add(FILTER_PARENT_CLASS);
const filterWrapperEl = document.createElement("div");
filterWrapperEl.classList.add("wrapper");
filterParentEl.appendChild(filterWrapperEl);
const filterEls = createFilterEls();
filterWrapperEl.append(...filterEls);
const prevFilterParentEl = document.querySelector(
`.${FILTER_PARENT_CLASS}`
);
if (prevFilterParentEl) prevFilterParentEl.remove();
targetContainerEl.prepend(filterParentEl);
const styleEl = document.createElement("style");
styleEl.textContent = STYLE;
styleEl.setAttribute(STYLE_DATA_ATTR, "true");
const prevStyleEl = document.querySelector(`style[${STYLE_DATA_ATTR}]`);
if (prevStyleEl) prevStyleEl.remove();
document.head.appendChild(styleEl);
hideInitial();
fetchAndDisplayProgress();
}
load();
// progress
// --------
function getProgressRequestBody(slugs) {
return {
operationName: "getLearnMenuProgress",
variables: { slugs: slugs },
query:
"query getLearnMenuProgress($slugs: [String!]) {\n user {\n id\n subjectProgressesBySlug(slugs: $slugs) {\n topic {\n id\n slug\n __typename\n }\n currentMastery {\n percentage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n",
};
}
async function fetchProgressBySlug(slugs) {
const response = await fetch(PROGRESS_ENDPOINT_URL, {
body: JSON.stringify(getProgressRequestBody(slugs)),
method: "POST",
});
const { data } = await response.json();
const progressBySlug = {};
data.user.subjectProgressesBySlug.forEach(({ topic, currentMastery }) => {
progressBySlug[topic.slug] = currentMastery.percentage;
});
return progressBySlug;
}
async function fetchAndDisplayProgress() {
const topics = getTopics();
const topicSlugs = topics.map(({ slug }) => slug);
const progressBySlug = await fetchProgressBySlug(topicSlugs);
topics.forEach(({ titleEl, slug }) => {
const percentage = progressBySlug[slug];
if (percentage) {
const progressEl = document.createElement("span");
progressEl.classList.add("progress");
progressEl.textContent = `${percentage}%`;
titleEl.parentElement.appendChild(progressEl);
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment