// ==UserScript==
// @name Declutter Pinterest
// @namespace August4067
// @version 0.5.0
// @description Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly
// @license MIT
// @match https://www.pinterest.com/*
// @match https://*.pinterest.com/*
// @match https://*.pinterest.co.uk/*
// @match https://*.pinterest.fr/*
// @match https://*.pinterest.de/*
// @match https://*.pinterest.ca/*
// @match https://*.pinterest.jp/*
// @match https://*.pinterest.it/*
// @match https://*.pinterest.au/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=pinterest.com
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @sandbox Javascript
// ==/UserScript==
/*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
that detects and handles AJAXed content.
Usage example:
waitForKeyElements (
"div.comments"
, commentCallbackFunction
);
//--- Page-specific function to do what we want when the node is found.
function commentCallbackFunction (jNode) {
jNode.text ("This comment changed by waitForKeyElements().");
}
IMPORTANT: This function requires your script to have loaded jQuery.
*/
// Pulled from: https://gist.github.com/raw/2625891/waitForKeyElements.js
function waitForKeyElements(
selectorTxt /* Required: The jQuery selector string that
specifies the desired element(s).
*/,
actionFunction /* Required: The code to run when elements are
found. It is passed a jNode to the matched
element.
*/,
bWaitOnce /* Optional: If false, will continue to scan for
new elements even after the first match is
found.
*/,
iframeSelector /* Optional: If set, identifies the iframe to
search.
*/,
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
else targetNodes = $(iframeSelector).contents().find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each(function () {
var jThis = $(this);
var alreadyFound = jThis.data("alreadyFound") || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound) btargetsFound = false;
else jThis.data("alreadyFound", true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey];
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function () {
waitForKeyElements(
selectorTxt,
actionFunction,
bWaitOnce,
iframeSelector,
);
}, 300);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
// We will set the Pinterest page title to this, to remove
// the flashing title notifications like Pinterest (2)
const ORIGINAL_TITLE = "Pinterest";
// SETTINGS
const SETTING_REMOVE_SHOPPABLE_PINS = "removeShoppablePins";
const SETTING_REMOVE_PIN_FOOTERS = "removePinFooters";
const SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON = "removeNotificationIcon";
const SETTING_REMOVE_NAVBAR_MESSAGES_ICON = "removeMessagesIcon";
const SETTING_REMOVE_COMMENTS = "removeComments";
class Setting {
constructor(settingName, settingDisplayName, settingDefault) {
this.settingName = settingName;
this.settingDisplayName = settingDisplayName;
this.settingDefault = settingDefault;
}
currentValue() {
return GM_getValue(this.settingName, this.settingDefault);
}
toggleSetting() {
var current = this.currentValue();
GM_setValue(this.settingName, !current);
}
}
class SettingRemoveShoppablePins extends Setting {
constructor() {
super(SETTING_REMOVE_SHOPPABLE_PINS, "Remove shoppable pins", false);
}
}
class SettingRemovePinFooters extends Setting {
constructor() {
super(SETTING_REMOVE_PIN_FOOTERS, "Remove pin footers", false);
}
}
class SettingRemoveNavbarMessagesIcon extends Setting {
constructor() {
super(
SETTING_REMOVE_NAVBAR_MESSAGES_ICON,
"Remove navbar messages icon",
false,
);
}
}
class SettingRemoveNavbarNotificationsIcon extends Setting {
constructor() {
super(
SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON,
"Remove navbar notifications icon",
false,
);
}
}
class SettingRemoveComments extends Setting {
constructor() {
super(SETTING_REMOVE_COMMENTS, "Remove pin comments", false);
}
}
const SETTINGS = {
[SETTING_REMOVE_SHOPPABLE_PINS]: new SettingRemoveShoppablePins(),
[SETTING_REMOVE_PIN_FOOTERS]: new SettingRemovePinFooters(),
[SETTING_REMOVE_COMMENTS]: new SettingRemoveComments(),
[SETTING_REMOVE_NAVBAR_MESSAGES_ICON]: new SettingRemoveNavbarMessagesIcon(),
[SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON]:
new SettingRemoveNavbarNotificationsIcon(),
};
// MENU SETTINGS
function toggleMenuSetting(settingName) {
var setting = SETTINGS[settingName];
setting.toggleSetting();
updateSettingsMenu();
console.debug(`Setting ${settingName} set to: ${setting.currentValue()}}`);
location.reload();
}
function updateSettingsMenu() {
for (const [setting_name, setting] of Object.entries(SETTINGS)) {
GM_registerMenuCommand(
`${setting.settingDisplayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`,
() => {
toggleMenuSetting(setting_name);
},
);
}
}
// HELPER FUNCTIONS
function waitAndRemove(selector, removeFunction) {
if (removeFunction == undefined) {
removeFunction = (elem) => elem.remove();
}
waitForKeyElements(selector, function (node) {
if (node && node.length > 0) {
removeFunction(node[0]);
}
});
}
/**
* Return an array of pins, filtering out those that already have
* style.display == "none" (pins that we have removed)
* @param {*} pins
* @returns array of pins
*/
function filterRemovedPins(pins) {
return pins ? pins.filter((pin) => pin.style.display !== "none") : [];
}
// DECLUTTER BOARDS
/**
* Clean the Shop button from the top of boards (from the 3 button group with "Shop", "Organize", and "More ideas")
*/
function cleanShopButtonsFromBoard() {
console.debug("Cleaning Shop buttons from board");
waitForKeyElements('div[data-test-id="board-tools"]', function (node) {
var shopButton = node[0].querySelector('div[data-test-id="Shop"]');
if (shopButton) {
shopButton.style.display = "none";
console.debug("Removed Shop button from top of board");
}
});
}
/**
* Remove the banner of shopping pins wherever we find it (with title "Shop products inspired by this board").
* They sometimes show up at the top of boards, at the bottom of boards, and at the top of searches.
*/
function cleanShopByBanners() {
waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) {
var shopByBannerAtTopOfBoard = node[0].closest(
'div[class="PKX zI7 iyn Hsu"]',
);
if (shopByBannerAtTopOfBoard) {
shopByBannerAtTopOfBoard.style.display = "none";
console.debug("Removed shop by banner from top of board");
}
if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) {
var shopByBannerAtBottomOfBoard = node[0].closest(
'div[class="gcw zI7 iyn Hsu"]',
);
if (shopByBannerAtBottomOfBoard) {
shopByBannerAtBottomOfBoard.style.display = "none";
console.debug("Removed shop by banner from bottom of board");
}
}
var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]');
if (shopByBannerAtTopOfSearch) {
shopByBannerAtTopOfSearch.style.display = "none";
console.debug("Removed shop by banner from top of search results");
}
});
}
// DECLUTTER NAVBAR
function removeBellIcon() {
if (SETTINGS[SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON].currentValue()) {
const bellIconDiv = document.querySelector('div[data-test-id="bell-icon"]');
if (bellIconDiv) {
bellIconDiv.style.display = "none";
console.debug("Removed bell icon from navbar.");
}
}
}
function removeMessagesIcon() {
if (SETTINGS[SETTING_REMOVE_NAVBAR_MESSAGES_ICON].currentValue()) {
const messagesIconDiv = document.querySelector(
'div[aria-label="Messages"]',
);
if (messagesIconDiv) {
var messagesParent = messagesIconDiv.closest(
'div[class="XiG zI7 iyn Hsu"]',
);
if (messagesParent) {
messagesParent.style.display = "none";
}
console.debug("Removed messages button");
}
}
}
/**
function removeNavBarTab(tabName) {
const navBarTab = document.querySelector(`div[data-test-id="${tabName}"]`);
navBarTab?.remove();
console.debug(`Removed navbar tab: ${tabName}`);
}
*/
function removeExploreTabNotificationsIcon() {
console.debug("Removing notifications icon from Explore tab");
var exploreTab = document.querySelector('div[data-test-id="today-tab"]');
if (exploreTab) {
var notificationsIcon = exploreTab.querySelector(
'div[aria-label="Notifications"]',
);
if (notificationsIcon) {
notificationsIcon.style.display = "none";
console.debug("Removed notifications icon from Explore tab");
}
}
}
function cleanNavBarCallback(mutationsList) {
console.debug("Cleaning navbar");
removeBellIcon();
removeMessagesIcon();
var callbackCount = 0;
waitForKeyElements('div[data-test-id="today-tab"]', function (nodes) {
new MutationObserver((mutations, observer) => {
removeExploreTabNotificationsIcon();
}).observe(nodes[0], {
childList: true,
subtree: true,
});
});
removeExploreTabNotificationsIcon();
// TODO - enable with settings
// removeNavBarTab("home-tab");
// removeNavBarTab("today-tab");
// removeNavBarTab("create-tab");
}
function cleanNavBar() {
const headerContent = document.getElementById("HeaderContent");
if (headerContent) {
var observerCallCount = 0;
const observer = new MutationObserver((mutations, observer) => {
cleanNavBarCallback(mutations);
if (++observerCallCount >= 5) {
observer.disconnect();
console.debug("Disconnected cleanNavBar() mutation observer");
}
});
observer.observe(headerContent, {
childList: true,
subtree: true,
});
cleanNavBarCallback([]);
}
}
// DECLUTTER PINS
function cleanShoppingAds(pins) {
const shoppingAdDivs = document.querySelectorAll(
"div.Ch2.zDA.IZT.CKL.tBJ.dyH.iFc.GTB.H2s",
);
shoppingAdDivs.forEach((adDiv) => {
let parent = adDiv.closest('div[role="listitem"]');
if (parent) {
parent.style.display = "none";
console.debug("Removed shopping container");
}
});
}
function cleanIdeasYouMightLove(pins) {
pins.forEach((pin) => {
if (
pin.textContent.toLowerCase().includes("ideas you might love") ||
pin.textContent.toLowerCase().includes("shop similar") ||
pin.textContent.toLowerCase().includes("shop featured boards")
) {
pin.style.display = "none";
console.debug('Removed "Ideas you might love" item:', pin);
}
});
}
function removePinFooters(pins) {
if (!SETTINGS[SETTING_REMOVE_PIN_FOOTERS].currentValue()) {
return;
}
pins.forEach((pin) => {
const footer = pin.querySelector('div[data-test-id="pinrep-footer"]');
if (!footer) {
return;
}
if (footer) {
footer.style.display = "none";
console.debug("Removed pin footer:", footer);
}
});
}
/**
* Remove Shoppable Pins by looking for a little tag ("Shoppable Pin indicator") in the pin somewhere.
* Shoppable pin indicators could be in the footer, or as an svg on top of the image
*/
function removeShoppablePins(pins) {
if (!SETTINGS[SETTING_REMOVE_SHOPPABLE_PINS].currentValue()) {
return;
}
pins.forEach((pin) => {
const shoppableIndicator = pin.querySelector(
'[aria-label="Shoppable Pin indicator"]',
);
if (shoppableIndicator) {
pin.style.display = "none";
console.debug("Removed shoppable pin:", pin);
}
});
}
function removeSponsoredPins(pins, mutations) {
var promotedPinSelector =
'a[aria-label="Promoted by"], a[aria-label="Promoted by; Opens a new tab"], div[title="Sponsored"]';
pins.forEach((pin) => {
if (pin && pin.querySelector(promotedPinSelector)) {
pin.style.display = "none";
}
});
/*
if (mutations) {
mutations.forEach((mutation) => {
var listItem = mutation.nextSibling?.closest('div[role="listitem"]');
if (listItem && listItem.querySelector(promotedPinSelector)) {
listItem.style.display = "none";
}
});
} else {
}
*/
}
/**
* Pinterest now has a "Shop now" module as the first result of some searches.
* We will remove this.
*/
function removeShoppingModule(pins) {
console.debug("Cleaning shopping modules");
if (pins) {
pins.forEach((pin) => {
var module = pin.querySelector("div.Module");
if (!module) {
return;
}
if (
module.innerText &&
module.innerText.trim().toLowerCase() == "shop now"
) {
pin.style.display = "none";
console.debug("Removed shopping module");
}
});
}
}
/**
* Pinterest now is promoting their own shopping boards (from the "Pinterest Shop" user). We
* will remove those.
*/
function removeFeaturedBoards(pins) {
if (pins) {
pins.forEach((pin) => {
if (pin.style.display === "none") {
return;
}
if (pin.querySelector('[data-test-id="pinRepPresentation"]')) {
return;
}
var innerText = pin.innerText ? pin.innerText.trim().toLowerCase() : "";
var splitText = innerText.split("\n").map((x) => x.trim().toLowerCase());
if (
splitText.includes("explore featured boards") ||
splitText.includes("pinterest shop")
) {
pin.style.display = "none";
console.debug("Removed featured boards module");
}
});
}
}
function cleanRelatedPinsSection(mutations) {
console.debug("Cleaning related pins");
var pins = document.querySelectorAll('div[role="listitem"]');
cleanIdeasYouMightLove(pins);
cleanShoppingAds(pins);
removePinFooters(pins);
removeShoppablePins(pins);
removeSponsoredPins(pins, mutations);
removeShoppingModule(pins);
removeFeaturedBoards(pins);
}
function cleanProductListingPinPage() {
waitAndRemove('div[data-test-id="product-price"]');
waitAndRemove('div[data-test-id="pdp-product-metadata-domain-owner"]');
waitAndRemove(
'div[data-test-id="product-shop-button"]',
(elem) => (elem.parentElement.style.display = "none"),
);
waitAndRemove(
'div[data-test-id="product-description"]',
(elem) => (elem.parentElement.style.display = "none"),
);
}
function cleanPinVisualContainer() {
var pinVisualContainer = document.querySelector(
'div[data-test-id="closeup-visual-container"]',
);
if (pinVisualContainer) {
var buttonsOnPinImage =
pinVisualContainer.querySelectorAll('div[role="button"]');
if (buttonsOnPinImage) {
for (var i = 0; i < buttonsOnPinImage.length; i++) {
buttonsOnPinImage[i].style.display = "none";
console.debug("Hiding button from pin image");
}
}
var shopButton = pinVisualContainer.querySelector(
'div[data-test-id="experimental-closeup-image-overlay-layer-shop-button"]',
);
if (shopButton) {
shopButton.style.display = "none";
}
}
}
/**
* The collapsible layout seems to be only used for the "Shop the look" section.
* So we will remove it.
*/
function removeShopTheLookBannerFromPinDescriptionContainer(mutationsList) {
waitForKeyElements(
'div[data-test-id="collapsible-layout"]',
function (nodes) {
nodes[0].style.display = "none";
console.debug('Hid "Shop the look" section');
},
);
}
function cleanComments() {
if (SETTINGS[SETTING_REMOVE_COMMENTS].currentValue()) {
waitForKeyElements("#canonical-card", function (canonicalCards) {
if (canonicalCards && canonicalCards.length == 1) {
var canonicalCard = canonicalCards[0];
if (!canonicalCard) {
return;
}
var hasCommentsHeading =
canonicalCard.querySelector("#comments-heading") != null;
if (hasCommentsHeading) {
canonicalCard.style.display = "none";
console.debug("Removed comments section from pin");
}
}
});
waitForKeyElements(
'div[data-test-id="inline-comment-composer-container"]',
function (nodes) {
var commentBox = nodes[0];
if (commentBox) {
commentBox.style.display = "none";
console.debug("Removed comment box from pin");
}
},
);
}
}
function cleanPinDescriptionContainer() {
var pinDescriptionContainer = document.querySelector(
'div[data-test-id="description-content-container"]',
);
if (pinDescriptionContainer) {
var shopButton = pinDescriptionContainer.querySelector(
'div[data-test-id="product-shop-button"]',
);
if (shopButton) {
shopButton.style.display = "none";
}
new MutationObserver(
removeShopTheLookBannerFromPinDescriptionContainer,
).observe(pinDescriptionContainer, {
childList: true,
subtree: true,
});
removeShopTheLookBannerFromPinDescriptionContainer([]);
}
}
/**
* Pinterest adds the number of notifications to the title
* in a distracting, flashing manner like Pinerest (2).
* So this will keep the page title at: Pinterest
*/
function enforceTitle() {
if (document.title !== ORIGINAL_TITLE) {
console.debug(
`Changing title from: ${document.title} to ${ORIGINAL_TITLE}`,
);
document.title = ORIGINAL_TITLE;
}
}
/**
* The search bar now has dynamic placeholder text with suggested searches. These are distracting, and we will remove them.
*/
function cleanSearchBar() {
document
.querySelector('div[data-test-id="dynamic-search-placeholder"]')
?.remove();
}
/**
* Pinterest now has a popup modal asking you to disable adblock, if it is present.
*
* We will remove this modal.
*/
function removeAntiAdblockModalIfExists() {
console.debug("Waiting for anti-Adblock modal");
waitForKeyElements('div[aria-label="Ad blocker modal"]', function (nodes) {
nodes[0]?.remove();
document.querySelector('div[name="trap-focus"]')?.remove();
document.body.style.overflow = "auto";
console.debug("Removed anti-Adblock modal");
});
}
// PAGE IDENTIFIERS
function isProductPin() {
return document.getElementById("product-sticky-container ") != null;
}
function hasDescriptionContentContainer() {
return (
document.querySelector(
'div[data-test-id="description-content-container"]',
) != null
);
}
function isBoard() {
return document.querySelector('div[data-test-id="board-header"]') != null;
}
function main() {
"use strict";
console.debug("Running main()");
updateSettingsMenu();
cleanNavBar();
cleanComments();
cleanSearchBar();
removeAntiAdblockModalIfExists();
if (isProductPin()) {
cleanProductListingPinPage();
}
cleanShopButtonsFromBoard();
cleanShopByBanners();
cleanPinDescriptionContainer();
cleanPinVisualContainer();
// The pin visual container changes to add those little "shop" buttons
// over the top of the image, so we need to watch for those and remove them
waitForKeyElements(
'div[data-test-id="closeup-visual-container"]',
function (nodes) {
new MutationObserver((mutations, observer) => {
cleanPinVisualContainer();
}).observe(nodes[0], {
childList: true,
subtree: true,
});
},
);
const relatedPinsSectionCleanerObserver = new MutationObserver(
(mutations) => {
cleanRelatedPinsSection(mutations);
},
);
waitForKeyElements('div[role="list"]', function (node) {
// Related pins section must be watched for changes,
// as new pins pop up as the user scrolls
cleanRelatedPinsSection([]);
relatedPinsSectionCleanerObserver.observe(node[0], {
childList: true,
});
});
enforceTitle();
const titleElement = document.querySelector("title");
new MutationObserver(enforceTitle).observe(titleElement, { childList: true });
}
main();
let lastUrl = window.location.href;
setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
console.debug(
`Detected new page, currentURL=${currentUrl}, previousURL=${lastUrl}`,
);
lastUrl = currentUrl;
main();
}
}, 750);