Adds search + A–Z sorting + larger popup to the "Save to playlist" dialog. Lets you quickly filter your playlists, and keeps the popup usable without closing on input click. Updated for October 2025 layout.
// ==UserScript==
// @name YouTube Save to playlist incremental search
// @namespace http://tampermonkey.net/
// @version 1.6
// @description Adds search + A–Z sorting + larger popup to the "Save to playlist" dialog. Lets you quickly filter your playlists, and keeps the popup usable without closing on input click. Updated for October 2025 layout.
// @author Jaq Drako
// @license MIT
// @match *://www.youtube.com/*
// @grant none
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==
(function () {
'use strict';
const $ = window.$;
if (!$) {
console.warn("[YT Playlist Filter] jQuery missing");
return;
}
let originalOrderCache = null;
let globalStyleInjected = false;
function injectGlobalStyleOnce() {
if (globalStyleInjected) return;
globalStyleInjected = true;
const css = `
tp-yt-iron-dropdown #contentWrapper,
tp-yt-iron-dropdown .ytContextualSheetLayoutContentContainer,
tp-yt-iron-dropdown {
max-height: 600px !important;
height: 600px !important;
overflow-y: auto !important;
}
`;
const styleTag = document.createElement("style");
styleTag.setAttribute("id", "ytPlaylistToolsGlobalStyle");
styleTag.textContent = css;
document.head.appendChild(styleTag);
}
function findSaveDialog() {
const candidates = $("tp-yt-iron-dropdown:visible");
for (let i = 0; i < candidates.length; i++) {
const dlg = $(candidates[i]);
const listCount = dlg.find("yt-list-view-model.ytListViewModelHost[role='list']").length;
if (listCount > 0) {
return dlg;
}
}
return null;
}
function swallowEventsPreventClose($el) {
const stopper = function (e) {
e.stopPropagation();
e.stopImmediatePropagation();
};
$el.on("mousedown click mouseup touchstart touchend", stopper);
}
function enlargeDialogHeight(dialogRoot) {
dialogRoot.css({
"max-height": "600px",
"height": "600px"
});
dialogRoot.find("#contentWrapper").css({
"max-height": "600px",
"height": "600px",
"overflow-y": "auto"
});
dialogRoot.find(".ytContextualSheetLayoutContentContainer").css({
"max-height": "600px",
"height": "600px",
"overflow-y": "auto"
});
}
function getPlaylistRows(dialogRoot) {
const contentWrapper = dialogRoot.find("#contentWrapper").first();
const listHost = contentWrapper
.find("yt-list-view-model.ytListViewModelHost[role='list']")
.first();
const rows = listHost
.find("toggleable-list-item-view-model.toggleableListItemViewModelHost");
return { listHost, rows };
}
function getRowName($row) {
const selectors = [
".yt-list-item-view-model__title",
".yt-core-attributed-string",
"#label",
"[id='label']",
"[class*='title']",
"[class*='Title']",
"[class*='label']",
"[class*='Label']",
"span",
"yt-formatted-string"
];
for (const selector of selectors) {
const $el = $row.find(selector).filter(function () {
const text = ($(this).text() || "").replace(/\s+/g, " ").trim();
return text.length > 0;
}).first();
if ($el.length > 0) {
const text = ($el.text() || "").replace(/\s+/g, " ").trim();
if (text) {
return text;
}
}
}
const fallbackText = ($row.text() || "")
.replace(/\s+/g, " ")
.trim();
return fallbackText || "";
}
function filterPlaylists(dialogRoot) {
const contentWrapper = dialogRoot.find("#contentWrapper").first();
const termRaw = contentWrapper.find("#ytPlaylistSearch").val() || "";
const term = termRaw.trim().toLowerCase();
const { rows } = getPlaylistRows(dialogRoot);
rows.each(function () {
const $row = $(this);
const name = getRowName($row).toLowerCase();
if (!name) {
$row.show();
return;
}
if (!term || name.indexOf(term) !== -1) {
$row.show();
} else {
$row.hide();
}
});
}
function sortPlaylists(dialogRoot, mode) {
const { listHost, rows } = getPlaylistRows(dialogRoot);
if (!originalOrderCache) {
originalOrderCache = rows.toArray();
}
let newOrder;
if (mode === "az" || mode === "za") {
newOrder = rows.toArray().sort(function (a, b) {
const nameA = getRowName($(a)).toLowerCase();
const nameB = getRowName($(b)).toLowerCase();
if (nameA < nameB) return (mode === "az") ? -1 : 1;
if (nameA > nameB) return (mode === "az") ? 1 : -1;
return 0;
});
} else {
newOrder = originalOrderCache.slice();
}
listHost.empty();
newOrder.forEach(function (node) {
listHost.append(node);
});
}
function ensureSearchAndSortUI(dialogRoot) {
const contentWrapper = dialogRoot.find("#contentWrapper").first();
if (!contentWrapper.length) return;
const headerContainer = contentWrapper.find(".ytContextualSheetLayoutHeaderContainer").first();
if (!headerContainer.length) return;
if (contentWrapper.find("#ytPlaylistToolsWrapper").length > 0) return;
const toolsHtml = [
"<div id='ytPlaylistToolsWrapper'",
" style='box-sizing:border-box;padding:8px 16px 0 16px;",
" display:flex;flex-wrap:wrap;row-gap:8px;column-gap:12px;",
" align-items:flex-start;align-content:flex-start;'>",
" <div style='display:flex;flex-direction:row;align-items:center;gap:8px;flex:1;min-width:200px;'>",
" <label for='ytPlaylistSearch'",
" style='font-size:12px;font-weight:500;white-space:nowrap;color:var(--yt-spec-text-primary,#fff);'>Search:</label>",
" <input id='ytPlaylistSearch' type='search' placeholder='filter playlists...'",
" style='flex:1;font-size:12px;line-height:16px;padding:4px 6px;",
" color:var(--yt-spec-text-primary,#fff);",
" background-color:transparent;",
" border:1px solid var(--yt-spec-text-secondary,#888);",
" border-radius:4px;outline:none;'/>",
" </div>",
" <div style='display:flex;flex-direction:row;align-items:center;gap:6px;'>",
" <label for='ytPlaylistSort'",
" style='font-size:12px;font-weight:500;white-space:nowrap;color:var(--yt-spec-text-primary,#fff);'>Sort:</label>",
" <select id='ytPlaylistSort'",
" style='font-size:12px;line-height:16px;padding:4px 6px;",
" background-color:#222; color:#fff;",
" border:1px solid var(--yt-spec-text-secondary,#888);",
" border-radius:4px;outline:none;'>",
" <option value='orig'>Original</option>",
" <option value='az'>A–Z</option>",
" <option value='za'>Z–A</option>",
" </select>",
" </div>",
"</div>"
].join("");
const $tools = $(toolsHtml).insertAfter(headerContainer);
const $search = $tools.find("#ytPlaylistSearch");
const $sort = $tools.find("#ytPlaylistSort");
swallowEventsPreventClose($tools);
swallowEventsPreventClose($search);
swallowEventsPreventClose($sort);
$search.on("input search", function () {
filterPlaylists(dialogRoot);
});
$sort.on("change", function () {
const mode = $(this).val();
sortPlaylists(dialogRoot, mode);
filterPlaylists(dialogRoot);
});
}
function attachCloseHandler(dialogRoot) {
if (dialogRoot.data("ytPlaylistFilterObserverAttached")) return;
dialogRoot.data("ytPlaylistFilterObserverAttached", true);
const observer = new MutationObserver(function () {
if (!document.contains(dialogRoot[0])) {
originalOrderCache = null;
startPolling();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
let pollHandle = null;
function pollStep() {
const dlg = findSaveDialog();
if (!dlg) return;
stopPolling();
injectGlobalStyleOnce();
enlargeDialogHeight(dlg);
ensureSearchAndSortUI(dlg);
filterPlaylists(dlg);
attachCloseHandler(dlg);
}
function startPolling() {
stopPolling();
pollHandle = setInterval(pollStep, 200);
}
function stopPolling() {
if (pollHandle) {
clearInterval(pollHandle);
pollHandle = null;
}
}
startPolling();
})();