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

// ==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();
})();