Motywacja
Oglądam właśnie WWDC 2025 - konferencję Apple'a na temat ich nowego oprogramowania. Całkiem niezłe rzeczy dodali, np. dostęp do lokalnego modelu językowego. Zmienili też radykalnie interfejs wszystkiego na Liquid Glass, który wygląda jak Windows Vista (to synonim do "wygląda okropnie"). Ale nie o tym dzisiaj.
Dzisiaj miejsce miała też premiera raportu stworzonego przez Stowarzyszenie na rzecz Chłopców i Mężczyzn, w którego tworzeniu miałem swój drobny udział. Raport dotyczy problemu małej ilości mężczyzn na studiach medycznych, próbuje go diagnozować, proponuje rozwiązania, analogiczne np. do programu "Dziewczyny na politechniki", który ma duże sukcesy.
W związku z tym raportem, powstało kilka artykułów w internetowych gazetach. Jeden z nich, opublikowany na RP.pl, wywołał burzliwą dyskusję na Facebooku. Chciałem poczytać tę dyskusję. Ale po tym, jak musiałem po raz sto pięćdziesiąty kliknąć przycisk "Pokaż więcej odpowiedzi", trochę się zirytowałem.
Przypomniałem sobie jednak, że używałem kiedyś wspaniałej skryptozakładki, Expand Facebook Comments, która robiła właśnie to - ekspandowała fejsbukowe komenty, automatycznie klikając te guziczki. Niestety, jej twórca zirytował się bardziej ode mnie, bo przestał ją rozwijać. Jak tłumaczy w tym wpisie, nie rozwija jej:
(...) due to Facebook changes that, as a human, make no sense to me and have finally caused me to ignore Facebook (going on six months now).
Całkowicie go rozumiem, i też chciałbym nie używać Facebooka, ale na ten moment nadal jest tam zbyt dużo osób i organizacji, które cenię, a które publikują jedynie tam. Na szczęście, mam wszystko czego potrzebuję, żeby spróbować samemu taką skryptozakładkę stworzyć.
Założenia
Nie wgłębiałem się kod źródłowy istniejącej skryptozakładki, ale zakładam, że całe cierpienie jej autora spowodowane było tym, że do identyfikacji przycisków próbował używać selektorów CSS/XPath oraz atrybutów id
/class
. Ponieważ Facebook to jest jeden wielki skrypt JS, pradopodobnie te identyfikatory zmieniają się nawet z dnia na dzień. Dlatego zdecydowałem się na inne podejście, i moja skryptozakładka będzie udawała mnie.
Ja, żeby rozwinąć odpowiedzi, szukam przycisku "Wyświetl wszystkie X wiadomości", no i go klikam. I dokładnie tak napisałem mój kod. Teraz, gdy zobaczyłem przycisk "Wyświetl wszystkie X wiadomości", zamiast klikać jego, klikałem moją skryptozakładkę, a ona klikała ten przycisk - a także każdy inny przycisk o takim samym tytule. Działało ślicznie.
Kolejnym krokiem było rozbudowanie skrytu o rozpoznawanie kolejnych przycisków: "Wyświetl więcej", "Pokaż więcej odpowiedzi", oraz mój ulubiony specjalny przypadek, "Wyświetl 1 odpowiedź". Teraz po każdym kliknięciu rozwijało się praktycznie wszystko, co było zwinięte.
Ale po chwili zaczęło mnie irytować to, że muszę klikać skryptozakładkę xD. Dlatego do zrobienia została ostatnia rzecz: sprawić, żeby skryptozakładka klikała się sama. Oczywiście to nie jest możliwe, JS nigdy nie będzie klikał po interfejsie przeglądarki. Ale może nasłuchiwać zmian na stronie, i jeśli w nowej treści pojawi się jeden z przycisków, to niech skrypt go kliknie, i gotowe. Ze względu na to, jak działa obserwowanie zmian (MutationObserver
), skrypt może się wywoływać rekurencyjnie, co jest fajnym udogodnieniem, bo wtedy cały wątek rozwinie się "sam": Observer
wykrywa zmianę, skanuje stronę, klika przyciski, w wyniku czego pojawia się nowa zmiana, i lecimy od nowa - aż skończą się przyciski do klikania.
Teraz skryptozakładka jedyne co robi, to instaluje się na stronie, i po prostu działa. I to bez żadnych dodatkowych wtyczek, bez wstrzykiwania skryptów przez ViolentMonkey, i istnieje jedynie do kolejnego odświeżenia strony! Proste i piękne!
Kod źródłowy
Dla formalności: żeby to działało w skryptozakładce, trzeba zinlajnować ten cały kod, oraz dodać na początku javascript:
. Z moich obserwacji wynika, że - przynajmniej w Firefoxie - zakładki bardzo nie lubią mieć w sobie znaków nowej linii.
// Auto-clicking bookmarklet with MutationObserver
// This will automatically click "show more" buttons as they appear on the page
// Click again to stop the observer
(function () {
// Check if observer is already running (toggle off)
if (window.fbAutoClicker) {
closeAutoClickerBar();
return;
}
// Click existing buttons first
const existingButtons = Array.from(
document.querySelectorAll('div[role="button"]')
).filter((button) => isTargetButton(button.textContent));
console.log("Clicking", existingButtons.length, "existing buttons");
existingButtons.forEach((button, index) => {
setTimeout(() => clickButton(button), index * 200);
});
// Set up observer for new buttons
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
checkForButtons(node);
});
});
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Store observer globally so we can stop it
window.fbAutoClicker = observer;
// Add a persistent red bar at the top
createRedBar();
console.log("Auto-clicker started - monitoring for new buttons");
alert("Auto-clicker started! Click bookmarklet again to stop.");
// === FUNCTION DEFINITIONS ===
function isTargetButton(text) {
return (
(text.includes("Wyświetl wszystkie") && text.includes("odpowiedzi")) ||
text.includes("Wyświetl więcej") ||
text.includes("Pokaż więcej odpowiedzi") ||
text.includes("Wyświetl 1 odpowiedź")
);
}
function clickButton(button) {
const text = button.textContent.trim();
console.log("Auto-clicking:", text);
button.click();
}
function checkForButtons(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// Check if the node itself is a button we want to click
if (node.matches && node.matches('div[role="button"]')) {
const text = node.textContent;
if (isTargetButton(text)) {
setTimeout(() => clickButton(node), 100);
}
}
// Check child buttons
const buttons = node.querySelectorAll('div[role="button"]');
buttons.forEach((button) => {
const text = button.textContent;
if (isTargetButton(text)) {
setTimeout(() => clickButton(button), 100);
}
});
}
function createRedBar() {
if (!document.getElementById("fb-auto-clicker-bar")) {
const bar = document.createElement("a");
bar.id = "fb-auto-clicker-bar";
bar.href = "#";
bar.textContent = "autoclicker enabled ✕";
bar.style.position = "fixed";
bar.style.top = "0";
bar.style.background = "red";
bar.style.color = "white";
bar.style.padding = "0.5em";
bar.style.fontSize = "1.5em";
bar.onclick = function (e) {
e.preventDefault();
closeAutoClickerBar();
};
document.body.appendChild(bar);
}
}
function closeAutoClickerBar() {
if (window.fbAutoClicker) {
window.fbAutoClicker.disconnect();
delete window.fbAutoClicker;
}
const redBar = document.getElementById("fb-auto-clicker-bar");
if (redBar) {
redBar.remove();
}
console.log("Auto-clicker stopped");
alert("Auto-clicker stopped");
}
})();
Muzyczka
Pisaniu tego posta towarzyszyła mi poniższa muzyczka - usłyszana przeze mnie pierwszy raz w życiu dzisiaj, użyta w intro do WWDC. Fajna.