Перенести скрипт Tampermonkey на JavaScript

Есть рабочий скрипт в Tampermonkey:

// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      2025-11-06
// @description  try to take over the world!
// @author       You
// @match        file:///C:/Users/soroc/Documents/vs/NewTab/index.html
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM.xmlHttpRequest
// ==/UserScript==

(function() {
    'use strict';
    //Переменные для настройки скрипта (остальные задаются в стилях HTML-страницы)
    var setLenVal = 3 //Минимальное кол-во символов для запроса подсказок
    var setColorSel = "#ccc7" //Цвет выделенного пункта

    //Ниже - собственно рабочий код
   var ro = new Object();
ro.method = "GET";
ro.onload = receiveYaSuggest;

var srch = document.getElementById('srch');
var rys = document.getElementById('rys');
var frmsrch = document.getElementById('frmsrch');

rys.style.left = srch.getBoundingClientRect().left + window.scrollX;
rys.style.top = srch.getBoundingClientRect().bottom + window.scrollY;
rys.style.width = srch.getBoundingClientRect().width;

srch.addEventListener('keyup', getYaSuggest, false);
srch.addEventListener('focus', getYaSuggest, false);
srch.addEventListener('mousedown', getYaSuggest, false);
srch.addEventListener('blur', function() {rys.style.display = "none"}, false);
srch.addEventListener('keydown', srchKeydown, false);

var i = 0;
var arrli = new Array();

function getYaSuggest(event) {
  if ((event.key == 'ArrowDown') ||
      (event.key == 'ArrowUp') ||
      (event.key == 'ArrowRight') ||
      (event.key == 'ArrowLeft') ||
      (event.key == 'Escape') ||
      (event.key == 'Enter')) {return};
  if (srch.value.length >= setLenVal) {
      ro.url = "https://suggest.yandex.ru/suggest-ya.cgi?part=" + srch.value;
      GM.xmlHttpRequest(ro)
  } else {rys.style.display = "none"};
}

function receiveYaSuggest(r)
{
  var arr = r.responseText.substring( r.responseText.indexOf("[",16)+1, r.responseText.indexOf("]") ).replace(new RegExp("\"","g"),"").split(",");
  if ((arr.length != 0) && (arr[0] != "")) {
      rys.style.display = "block";
      var arrs = "<ul class=\"rysul\" id=\"idrysul\">";
      arr.forEach(function(item) {arrs = arrs + "<li class=\"rysli\">"+item+"</li>"});
      arrs+="</ul>";
      document.getElementById('rys').innerHTML = arrs;
      arrli = Array.from( document.getElementById('idrysul').getElementsByTagName("li") );
      arrli.forEach( function(item) {item.addEventListener('mousedown', function() {srch.value = this.innerText; frmsrch.submit()}, false)} );
      arrli.forEach( function(item) {item.addEventListener('mouseenter', function() {i = arrli.indexOf(this); HlSel()}, false)} );
      i=0;
  } else {rys.style.display = "none"}
};

function srchKeydown(event) {
  if (arrli.length == 0) {return};
  if (event.key == 'ArrowDown') { i++; if (i>(arrli.length-1)) {i=0}; HlSel(); srch.value = arrli[i].innerText; };
  if (event.key == 'ArrowUp') { i--; if (i<0) {i=arrli.length-1}; HlSel(); srch.value = arrli[i].innerText; };
  if (event.key == 'Enter') { rys.style.display = "none" };
  if (event.key == 'Escape') { rys.style.display = "none" };
}

function HlSel() {
  if (arrli.length == 0) {return};
  arrli.forEach( function(item) {item.style.backgroundColor = "transparent"} );
  arrli[i].style.backgroundColor = setColorSel;
}
})();

Но при попытке перенести его в обычный файл js он перестаёт работать. Я знаю что в нём используется GM.xmlHttpRequest и при копировании заменил его на XMLHttpRequest. Но ничего не происходит. Вот HTML-код для которого я использую скрипт:

<head>
    <!-- Для выпадающих подсказок -->
    <style type="text/css">
     .rysul {list-style-type:none; margin:0px}
     .rysli {margin-left:-40px; padding-left:10px; padding-top:5px; padding-bottom:6px; cursor:pointer; font:16px sans-serif}
    </style>
    <!-- /Для выпадающих подсказок -->
   ...
   </head>
    
   ...
    
   <form action="https://yandex.ru/search" id="frmsrch" style="font-size:16; border-radius:10px; padding:10; background:yellow">
    <b>Поиск в <font color="red">Я</font>ндексе:</b>&nbsp;
    <input id="srch" name="text" size="140" style="font-size:16"/>
   </form>
   <div id="rys" style="position:absolute; display:none; border:1px solid black; background-color:#FFFBF0"></div>

Я не могу использовать это расширение потому что планирую сделать из этого полноценную страницу новой вкладки для браузера. Может ли кто-нибудь мне помочь?


Ответы (2 шт):

Автор решения: Ivan Shatsky

Я могу ошибаться, но мне кажется, что это невозможно, по крайней мере без использования дополнительных средств. Браузер не даст вам выполнить cross-origin AJAX запрос, если сервер, к которому вы обращаетесь, не будет включать в ответ CORS-заголовки (а он не будет этого делать, потому что ему это не надо, чтобы к нему обращались всякие сторонние скрипты).

В отличие от XMLHttpRequest, запросы, выполняемые с помощью GM.xmlHttpRequest, не ограничены рамками т.н. same-origin политики, о чём в явном виде сказано в первом же абзаце документации:

This method performs a similar function to the standard XMLHttpRequest object, but allows these requests to cross the same origin policy boundaries.

Можно где-то поднять свой собственный сервер, который будет проксировать запросы к яндексу и самостоятельно добавлять к его ответу CORS-заголовки перед тем, как отдавать ответ запрашивающей стороне (это один из примеров того, что я назвал "дополнительными средствами"). Другие способы лично мне неизвестны, но может быть кто-нибудь ещё подскажет какое-то альтернативное решение.


Как подсказали в комментариях, специально для таких случаев существуют специальные CORS прокси сервисы. Вот небольшой перечень тех, которые удалось найти в первых результатах выдачи поисковиков (в порядке убывания "выносливости" по результатам короткого тестирования):

Вот что у меня получилось сделать с вашим скриптом:

document.addEventListener('DOMContentLoaded', function() {
  'use strict';
  // Переменные для настройки скрипта (остальные задаются в стилях HTML-страницы)
  var setLenVal = 3 // Минимальное кол-во символов для запроса подсказок
  var setColorSel = "#ccc7" // Цвет выделенного пункта
  var debounceDelay = 500; // 1/2 секунды задержки между окончанием ввода и запросом

  // CORS прокси с поддержкой fallback
  var corsProxies = [
    "https://corsproxy.io/?",
    "https://cors-anywhere.com/",
    "https://api.allorigins.win/raw?url=",
  ];

  var currentProxyIndex = 0;
  var lastSuccessfulProxy = 0;
  var attemptCount = 0;

  var debounceTimeout = null;
  var ro = null;
  var currentQuery = "";
  var currentSrch = "";

  var srch = document.getElementById('srch');
  var rys = document.getElementById('rys');
  var frmsrch = document.getElementById('frmsrch');

  rys.style.left = srch.getBoundingClientRect().left + window.scrollX;
  rys.style.top = srch.getBoundingClientRect().bottom + window.scrollY;
  rys.style.width = srch.getBoundingClientRect().width;

  srch.addEventListener('keyup', getYaSuggest, false);
  srch.addEventListener('focus', showYaSuggest, false);
  srch.addEventListener('mousedown', getYaSuggest, false);
  srch.addEventListener('blur', function() {rys.style.display = "none"}, false);
  srch.addEventListener('keydown', srchKeydown, false);

  var i;
  var arrli = new Array();

  // Для вывода proxy hostname в консольных логах
  function getProxyHostname(proxyIndex) {
      return new URL(corsProxies[proxyIndex]).hostname;
  }

  function getYaSuggest(event) {
    if (srch.value == currentSrch) { return }; // Если строка ввода не поменялась в результате нажатия клавиши
    currentSrch = srch.value;
    
    // Отменяем предыдущий таймер
    if (debounceTimeout) {
      clearTimeout(debounceTimeout);
    }
    
    if (currentSrch.length >= setLenVal) {
        // Откладываем запрос на debounceDelay миллисекунд
        debounceTimeout = setTimeout(function() {
          currentQuery = currentSrch;
          // Начинаем с последнего успешного прокси
          currentProxyIndex = lastSuccessfulProxy;
          attemptCount = 0;
          makeRequest();
        }, debounceDelay);
    } else {
        rys.style.display = "none";
    };
  }

  function showYaSuggest(event) {
    if (arrli.length && (rys.style.display == 'none')) {
      if (i >= 0) { arrli[i].style.backgroundColor = "transparent"; i = -1; }
      rys.style.display = 'block';
    }
  }

  function makeRequest() {
    // Отменяем предыдущий запрос, если он еще выполняется
    if (ro) {
      ro.abort();
    }

    ro = new XMLHttpRequest();
    ro.timeout = 5000; // 5 секунд таймаут
    
    var targetUrl = "https://suggest.yandex.ru/suggest-ya.cgi?part=" + encodeURIComponent(currentQuery);
    var proxyUrl = corsProxies[currentProxyIndex] + targetUrl;
    
    ro.open("GET", proxyUrl, true);
    
    ro.onload = function() {
      if (ro.status === 200) {
        lastSuccessfulProxy = currentProxyIndex; // Запоминаем успешный прокси
        receiveYaSuggest();
      } else {
        console.log('Proxy ' + getProxyHostname(currentProxyIndex) + ' failed with status:', ro.status);
        tryNextProxy();
      }
    };
    
    ro.onerror = function() {
      console.log('Proxy ' + getProxyHostname(currentProxyIndex) + ' network error');
      tryNextProxy();
    };
    
    ro.ontimeout = function() {
      console.log('Proxy ' + getProxyHostname(currentProxyIndex) + ' timeout');
      tryNextProxy();
    };
    
    ro.send();
  }

  function tryNextProxy() {
    attemptCount++;
    if (attemptCount < corsProxies.length) {
      currentProxyIndex = (currentProxyIndex + 1) % corsProxies.length;
      console.log('Trying fallback proxy ' + getProxyHostname(currentProxyIndex) + ' (attempt ' + (attemptCount + 1) + ')');
      makeRequest();
    } else {
      console.log('All proxies failed after ' + corsProxies.length + ' attempts');
      rys.style.display = "none";
    }
  }

  function receiveYaSuggest()
  {
    if (!ro.responseText) {
      rys.style.display = "none";
      return;
    }
    
    try {
      var arr = ro.responseText.substring( ro.responseText.indexOf("[",16)+1, ro.responseText.indexOf("]") ).replace(new RegExp("\"","g"),"").split(",");
      if ((arr.length != 0) && (arr[0] != "")) {
          var arrs = "<ul class=\"rysul\" id=\"idrysul\">";
          arr.forEach(function(item) {arrs = arrs + "<li class=\"rysli\">"+item+"</li>"});
          arrs+="</ul>";
          document.getElementById('rys').innerHTML = arrs;
          rys.style.display = "block";
          arrli = Array.from( document.getElementById('idrysul').getElementsByTagName("li") );
          arrli.forEach( function(item) {item.addEventListener('mousedown', function() {srch.value = this.innerText; frmsrch.submit()}, false)} );
          arrli.forEach( function(item) {item.addEventListener('mouseenter', function() {i = arrli.indexOf(this); HlSel()}, false)} );
          i=-1;
      } else {
          rys.style.display = "none";
      }
    } catch(e) {
      console.log('Error parsing suggestions:', e);
      rys.style.display = "none";
    }
  }

  function srchKeydown(event) {
    if (arrli.length == 0) {return};
    if (rys.style.display == 'block') {
      if (event.key == 'ArrowDown') { i++; if (i>(arrli.length-1)) {i=0}; HlSel(); srch.value = arrli[i].innerText; currentSrch = arrli[i].innerText; };
      if (event.key == 'ArrowUp') { i--; if (i<0) {i=arrli.length-1}; HlSel(); srch.value = arrli[i].innerText; currentSrch = arrli[i].innerText; };
      if ((event.key == 'Enter') || (event.key == 'Escape')) { rys.style.display = "none" };
    } else if (arrli.length && (event.key == 'ArrowDown')) {
      // Показать выпадающий список заново
      if (i >= 0) { arrli[i].style.backgroundColor = "transparent"; i = -1; }
      rys.style.display = 'block';
    }
  }

  function HlSel() {
    if (arrli.length == 0) {return};
    arrli.forEach( function(item) {item.style.backgroundColor = "transparent"} );
    if (i >= 0) { arrli[i].style.backgroundColor = setColorSel; }
  }
});
.rysul {list-style-type:none; margin:0px}
.rysli {margin-left:-40px; padding-left:10px; padding-top:5px; padding-bottom:6px; cursor:pointer; font:16px sans-serif}
<form action="https://yandex.ru/search" id="frmsrch" style="font-size:16; border-radius:10px; padding:10; background:yellow">
  <b>Поиск в <font color="red">Я</font>ндексе:</b>&nbsp;
  <input id="srch" name="text" size="140" style="font-size:16"/>
</form>
<div id="rys" style="position:absolute; display:none; border:1px solid black; background-color:#FFFBF0"></div>

Скрипт поддерживает список из нескольких прокси, и в случае отказа одного из них переключается на следующий.

Для того, чтобы не перегружать прокси-сервер, сделана проверка, изменилась ли строка ввода в результате нажатия очередной клавиши (по событию keyup). Кроме того, между окончанием ввода и выполнением запроса предусмотрена задержка в 500 мс (по английски это называется debouncing, настраивается с помощью переменной debounceDelay).

Помимо этого, я внёс небольшие изменения в поведение UI, самое значительное из которых - повторное открытие закрытого списка подсказок по нажатию клавиши или по событию focus.

Обратите внимание, что главная функция вызывается по событию DOMContentLoaded. Пользовательские скрипты Greasemonkey/Tampermonkey тоже обычно запускаются по этому событию. Хотя для одной HTML страницы вы можете просто поместить содержимое этой функции внутрь тегов <script>...</script>, главное, чтобы оно находилось ниже DOM-элементов страницы, которые в ней используются.

→ Ссылка
Автор решения: stylok

По просьбам трудящихся… Прошу прощения, но крайне редко меня сюда заносит последнее время. Итак — минимальная версия "newtab" для демонстрации того, что при желании можно грабить корованы прямо с глагны.

Создаём три файла: manifest.json newtab.html newtab.js

Микро-манифест, где показано подключение обычной html-странички. Где параметром который нам разрешает доступ наружу — host_permissions (можно ограничить любым сайтом/сайтами в соответствии с перечисленными шаблонами) — я разрешил "всё", чтобы было с чем поиграть.

{
    "manifest_version": 3,
    "name": "#mini-newtab",
    "version": "0.0.1",
    "chrome_url_overrides": {
        "newtab": "newtab.html"
    },
    "host_permissions": ["<all_urls>"]
}

Сама страничка. Обращаю внимание на ВАЖНЫЙ момент — в расширениях для браузера весь js должен быть подключаемым из файла. Любой другой фокус с попыткой добавить его в код будет плеваться ошибками.

<!DOCTYPE html>
<html lang="ru"> 
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>#</title>
        <script src="newtab.js" defer></script>
    </head>
    <body>
        <input type="text" id="fetchUrl">
        <input type="button" value="Fetch" id="buttonFetch">
    </body>
</html>

Тот самый js из файла, который будет грабить указанные нами адреса и выводить их в консоль.

const url = document.getElementById('fetchUrl');
const button = document.getElementById('buttonFetch');

button.addEventListener('click', async () => {
  const myUrl = url.value;
  const response = await fetch(myUrl)
  if (response.ok) {
    const data = await response.text()
    console.log(data);
  } else {
    alert('Что-то пошло не так!');
  }
});
  1. Запихиваем все три файла в одну папку.
  2. Заходим в Хром на страницу расширений и включаем режим разработчика.
  3. Устанавливаем расширение (натравив на нашу папку).
  4. Открываем новую страницу. С высокой вероятностью при первом запуске всплывёт предупреждение о том, что вы меняете newtab и поинтересуется вашей уверенности в себе и дальнейших действиях.)

Собственно вся суть написанного мной расширения сводится к тому, что вы можете fetch-ить любую доступную страницу или небинарный файл из тырнета и получать ввывод содержимого в консоль.

Andrew_Sor, завязывай с XMLHttpRequest. "Наркотики — это плохо.)" Слишком он древний во всех смыслах.

→ Ссылка