Youtube Save to... playlist incremental search

This script injects a search field into the dialog where user can save a video to a playlist. When the user starts to type an incremental search is implemented and the playlists are filtered out

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Youtube Save to... playlist incremental search
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  This script injects a search field into the dialog where user can save a video to a playlist. When the user starts to type an incremental search is implemented and the playlists are filtered out
// @author       Jaq Drako
// @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;
    }

    // returns jQuery-wrapped dropdown element if the "Save to..." sheet is open, else null
    function findSaveDialog() {
        const candidates = $("tp-yt-iron-dropdown:visible");
        for (let i = 0; i < candidates.length; i++) {
            const dlg = $(candidates[i]);
            if (dlg.find("yt-list-view-model.ytListViewModelHost[role='list']").length > 0) {
                return dlg;
            }
        }
        return null;
    }

    function swallowEventsPreventClose($el) {
        // block all the "this is an outside click" detectors higher up
        const stopper = function (e) {
            e.stopPropagation();
            e.stopImmediatePropagation();
        };

        // mousedown/up/click + focus just in case
        $el.on("mousedown click mouseup touchstart touchend", stopper);
    }

    function ensureSearchBox(dialogRoot) {
        const contentWrapper = dialogRoot.find("#contentWrapper").first();
        if (!contentWrapper.length) {
            return;
        }

        const headerContainer = contentWrapper.find(".ytContextualSheetLayoutHeaderContainer").first();
        if (!headerContainer.length) {
            return;
        }

        if (contentWrapper.find("#ytPlaylistSearchWrapper").length > 0) {
            return; // already injected
        }

        const searchHtml = [
            "<div id='ytPlaylistSearchWrapper'",
            "     style='box-sizing:border-box;padding:8px 16px 0 16px;display:flex;flex-direction:row;align-items:center;gap:8px;'>",
            "   <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>"
        ].join("");

        const $injected = $(searchHtml).insertAfter(headerContainer);
        const input = $injected.find("#ytPlaylistSearch");

        // prevent dialog close on click/focus in our UI
        swallowEventsPreventClose($injected);
        swallowEventsPreventClose(input);

        // bind filtering
        input.on("input search", function () {
            filterPlaylists(dialogRoot);
        });
    }

    function filterPlaylists(dialogRoot) {
        const contentWrapper = dialogRoot.find("#contentWrapper").first();
        const termRaw = contentWrapper.find("#ytPlaylistSearch").val() || "";
        const term = termRaw.trim().toLowerCase();

        const rows = contentWrapper
            .find("yt-list-view-model.ytListViewModelHost[role='list']")
            .find("toggleable-list-item-view-model.toggleableListItemViewModelHost");

        rows.each(function () {
            const row = $(this);

            // try nice title span first
            const titleSpan = row.find(".yt-list-item-view-model__title").first();
            let name = "";

            if (titleSpan.length > 0) {
                name = (titleSpan.text() || "").trim().toLowerCase();
            } else {
                const item = row.find(".yt-list-item-view-model").first();
                name = (item.attr("aria-label") || "").trim().toLowerCase();
            }

            if (!term || name.indexOf(term) !== -1) {
                row.show();
            } else {
                row.hide();
            }
        });
    }

    function attachCloseHandler(dialogRoot) {
        if (dialogRoot.data("ytPlaylistFilterObserverAttached")) {
            return;
        }
        dialogRoot.data("ytPlaylistFilterObserverAttached", true);

        const observer = new MutationObserver(function () {
            if (!document.contains(dialogRoot[0])) {
                startPolling();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    let pollHandle = null;

    function pollStep() {
        const dlg = findSaveDialog();
        if (!dlg) {
            return;
        }

        stopPolling();

        ensureSearchBox(dlg);
        filterPlaylists(dlg);
        attachCloseHandler(dlg);
    }

    function startPolling() {
        stopPolling();
        pollHandle = setInterval(pollStep, 200);
    }

    function stopPolling() {
        if (pollHandle) {
            clearInterval(pollHandle);
            pollHandle = null;
        }
    }

    startPolling();
})();