Search posts by user

Search for posts by user(-s) in current topic

Versione datata 20/12/2017. Vedi la nuova versione l'ultima versione.

// ==UserScript==
// @name           Search posts by user
// @name:ru        Поиск постов пользователя
// @description    Search for posts by user(-s) in current topic
// @description:ru Найти посты пользователя(-ей) в текущей теме
// @version        2.0.4
// @date           20.12.2017
// @author         Halibut
// @namespace      https://greasyfork.org/en/users/145947-halibut
// @homepageURL    https://greasyfork.org/en/scripts/36319-search-posts-by-user
// @supportURL     https://forum.ru-board.com/topic.cgi?forum=2&topic=5673&glp
// @license        HUG-WARE
// @include        http*://forum.ru-board.com/topic.cgi?forum=*&topic=*
// @noframes
// @run-at         document-start
// @grant          none
// ==/UserScript==

/******************************************************************************
 * "THE HUG-WARE LICENSE" (Revision 2): As long as you retain this notice you *
 * can do whatever you want with this stuff. If we meet some day, and you     *
 * think this stuff is worth it, you can give me/us a hug.                    *
******************************************************************************/

window.addEventListener("DOMContentLoaded", function f() {
    'use strict';
    this.removeEventListener("DOMContentLoaded", f);
    const body = document.body,
          style = document.head.appendChild(document.createElement("style")),
          dats = [...document.getElementsByClassName("tb")].map(el => el.getElementsByClassName("dats")[0]),
          css = 
          [
              '#spu-spoiler-head {',
              '    font-family: Segoe UI Emoji, bitstreamcyberbit, quivira, Verdana, Arial, Helvetica, sans-serif;',
              '    -moz-user-select: none !important;',
              '    -webkit-user-select: none !important;',
              '    -ms-user-select: none !important;',
              '    user-select: none !important;',
              '    cursor: pointer !important',
              '}',
              '#spu-spoiler-head[notready] {',
              '    cursor: not-allowed !important;',
              '}',
              '#spu-spoiler-body {',
              '    border: 1px solid black !important;',
              '    padding: 1em .5em !important;',
              '}',
              '#spu-spoiler-body button.spu-search {',
              '    display: none !important;',
              '}',
              'button.spu-search {',
              '    padding: .05em .5em .1em !important;',
              '    background: rgb(251,251,251) !important;',
              '    color: rgb(51, 51, 51) !important;',
              '    border: 1px solid rgba(66,78,90,0.2) !important;',
              '    border-radius: 2px !important;',
              '    box-shadow: none !important;',
              '    font-size: 1em !important;',
              '}',
              'button.spu-search:hover {',
              '    background: rgb(235,235,235) !important;',
              '}',
              'button.spu-search:active {',
              '    background: rgb(225,225,225) !important;',
              '}'
          ].join('\n'),
          listener = {
              options: {
                  // !!!ОПЦИИ СКРИПТА!!! (пока неполные)
                  showAlerts: false // Показывать алерты по окончании поиска
                  , scrollToSearchResults: true  // Автоматически прокручивать страницу к результатам поиска
                                                 // (если showAlerts == true, то, при подтверждении, все-равно прокрутит к результатам)
                  , autoOpenSpoilerWithResult: false // Раскрывать спойлер с результатами поиска автоматически
                  , reversPostSorting: true // обратить сортировку по дате для найденных постов (от новых к старым)
                  , headToResult: false // Включать шапку темы (если она создана выбранным пользоватем) в результаты поиска
                  , createCnMenu: true // Добавить в контекстное меню пункт запускающий поиск (только для FF)
                  , searchOnProfileLinksByRMB: true // Поиск при клике по ссылкам ведущим на профиль пользователя 
                                                    // (только если нет выделенного текста и не нажаты клавиши модификаторы)
              }
              , names: []
              , ttlShow: '\u25BA Показать результаты поиска'
              , ttlHide: '\u25BC Скрыть результаты поиска'
              , ttlLdng: '\u23F3 Поиск...'
              , ttlNotFnd: '\u26A0 Постов не найдено!'
              , get name() {
                  return this.name = this.getName()
              }
              , set name(str) {
                  return this.setName(str)
              }
              , get win() {
                  delete this.win;
                  return this.win = window.top
              }
              , get loc() {
                  delete this.loc;
                  return this.loc = this.win.location.href
              }
              , get actnBox() {
                  delete this.actnBox;
                  return this.actnBox = [...document.getElementsByTagName("table")].find(el => el.querySelector("a[href^='post.cgi?action=new']")
                                                                                         || !el.getElementsByTagName('td')[0].children.length)
              }
              , get isFF() {
                  delete this.isFF;
                  return this.isFF = this.win.navigator && this.win.navigator.userAgent && this.win.navigator.userAgent.toLowerCase().includes("firefox")
              }
              , get spHd() {
                  delete this.spHd;
                  return this.spHd = this.getSpHd()
              }
              , get spBd() {
                  delete this.spBd;
                  return this.spBd = this.getSpBd()
              }
              , get spTTl() {
                  delete this.spTTl;
                  return this.spTTl = this.spHd && this.spHd.getElementsByTagName('td')[0]
              }
              , getSel() {
                  return this.win.getSelection && this.win.getSelection().toString() || ""
              }
              , prompt() {
                  return this.win.prompt('Задайте имя(-ена) для поиска, разделяя запятой', '')
              }
              , getPosts([url, page], name) {
                  return new Promise((res, rej) => {
                      let posts;
                      if (url == this.loc) {
                          posts = document.getElementsByClassName('tb');
                          if (posts && !!posts.length) {
                              posts = this.filterPosts(posts, name, page);
                              return res(posts.map(el => el.cloneNode(true)));
                          }
                      };
                      const xhr = new XMLHttpRequest();
                      xhr.open("GET", url, true);
                      xhr.onload = re => {
                          if (!(xhr.readyState == 4 && xhr.status == 200)) return rej("error");
                          posts = xhr.responseXML.getElementsByClassName('tb');
                          if (posts && !!posts.length) {
                              posts = this.filterPosts(posts, name, page);
                              return res(posts);
                          }
                      };
                      xhr.onerror = xhr.onabort = xhr.ontimeout = () => rej("error");
                      xhr.responseType = "document";
                      xhr.send();
                  })
              }
              , procesPosts(posts) {
                  return new Promise((res, rej) => {
                      posts = this.flatPosts(posts);
                      if (posts && !!posts.length) {
                          this.options.reversPostSorting && (posts = posts.reverse());
                          posts = this.stylePosts(posts);
                          for (const post of posts)
                              this.spBd.appendChild(post);
                          res(posts.length)
                      }
                      else
                          rej("not found");
                  })
              }
              , filterPosts(posts, name, page) {
                  return [...posts].filter(post => (this.options.headToResult && page == 1 || !post.querySelector('a.tpc[href$="&postno=1"]'))
                                           && name.includes(post.querySelector("td.dats b").textContent.toLowerCase().replace(/\s/g, '_')));
              }
              , stylePosts(posts) {
                  const color1 = "#EEEEEE",
                        color2 = "#FFFFFF"
                  return posts.map((post,indx) => {
                      const tdEls =  post.getElementsByTagName("td");
                      for (const td of tdEls) {
                          td.bgColor && (td.bgColor = ((indx + 1) % 2) ? color1 : color2);
                      }
                      return post
                  })
              }
              , flatPosts(posts) {
                  return posts.reduce((a, b) => a.concat(b), [])
              }
              , getName() {
                  const name = this.getSel() || [...new Set(this.names)].join(",");
                  if (name)
                      return name.trim().toLowerCase().replace(/\s/g, "_").split(",")
              }
              , setName(str) {
                  if (str)
                      return this.names.push(str);
              }
              , getPages() {
                  const paginator = [...document.getElementsByTagName("p")].find(el => el.textContent && el.textContent.startsWith("Страницы: "));
                  if (paginator)
                      return [...paginator.getElementsByTagName("td")[0].children].filter(el => !el.title).map((el, indx) => [el.href || this.loc, indx + 1]);
                  else
                      return [[this.loc, 1]];
              }
              , getSpHd() {
                  let spoilerHead = document.getElementById("spu-spoiler-head");
                  if (!spoilerHead) {
                      const dummyNode = this.actnBox.parentNode.insertBefore(document.createElement('div'), this.actnBox.nextElementSibling);
                      dummyNode.outerHTML = '<table id="spu-spoiler-head" width="95%" cellspacing="1" cellpadding="3" bgcolor="#999999" align="center" border="0"><tbody><tr><td valign="middle" bgcolor="#dddddd" align="left"></td></tr><td class="small" bgcolor="#dddddd" align="center">Найдено: <span id="spu-fndd"></span>   Ошибок: <span id="spu-errs"></span></td></tbody></table>';
                      spoilerHead = this.actnBox.nextElementSibling;
                      spoilerHead.id = "spu-spoiler-head";
                      spoilerHead.hidden = true;
                      const spTitle = spoilerHead.getElementsByTagName('td')[0];
                      spoilerHead.onclick = e => {
                          e.preventDefault(); e.stopPropagation();
                          const spoilerBody = this.spBd;
                          if (e.button != 0 || !spoilerBody || spoilerHead.hasAttribute("notready")) return;
                          spTitle.textContent = spoilerBody.hidden ? this.ttlHide : this.ttlShow;
                          spoilerBody.hidden = !spoilerBody.hidden;
                      }
                  }
                  return spoilerHead;
              }
              , getSpBd() {
                  let spoilerBody = document.getElementById("spu-spoiler-body");
                  if (!spoilerBody) {
                      const spoilerHead = this.spHd;
                      spoilerBody = spoilerHead.parentNode.insertBefore(document.createElement("table"), spoilerHead.nextElementSibling);
                      spoilerBody.id = "spu-spoiler-body";
                      spoilerBody.align = "center";
                      spoilerBody.width = "95%";
                      spoilerBody.hidden = true
                  }
                  return spoilerBody;
              }
              , endNotify(fndd,errs,ntfnd) {
                  if (fndd) {
                      const spoilerHead = this.spHd,
                            spoilerBody = this.spBd,
                            confirm = this.win.confirm("Поиск окончен!\nНайдено: " + fndd + (errs ? "\nОшибок: " + errs : "") + "\nПерейти к резульататам?");
                      if (confirm) {
                          spoilerHead.scrollIntoView({behavior:"smooth"});
                          if (this.options.autoOpenSpoilerWithResult) {
                              this.spTTl.textContent = this.ttlHide;
                              spoilerBody.hidden = false;
                          }
                      }
                  }
                  else
                      this.win.alert("Постов не найдено!" + (errs ? "\nОшибок: " + errs : ""));
              }
              , handleEvent(e, name, prmpt) {
                  e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                  this.name = prmpt ? this.prompt() : name;
                  if (name && e.ctrlKey) return;
                  const nmsToSearch = this.name;
                  if (!nmsToSearch || nmsToSearch && !nmsToSearch.length) return;
                  const pageLinks = this.getPages(),
                        spoilerHead = this.spHd,
                        spoilerBody = this.spBd,
                        spoilerTitle = this.spTTl,
                        errsLbl = document.getElementById("spu-errs"),
                        findedLbl = document.getElementById("spu-fndd");
                  spoilerHead.hidden = false; spoilerTitle.textContent = this.ttlLdng;
                  spoilerHead.setAttribute("notready", "true"); spoilerBody.hidden = true; 
                  this.options.scrollToSearchResults && !this.options.showAlerts && spoilerHead.scrollIntoView({behavior:"smooth"});
                  let errorsCntr = 0;
                  errsLbl.textContent = findedLbl.textContent = 0;
                  if (spoilerBody.children && !!spoilerBody.children.length)
                      for (const node of [...spoilerBody.children])
                          node.remove();
                  Promise.all(pageLinks.map(link => {
                      return this.getPosts(link, nmsToSearch)
                  })).then(
                      posts => {
                          return this.procesPosts(posts)
                      }
                      , error => {
                          errorsCntr += 1;
                          errsLbl.textContent = errorsCntr;
                      }
                  ).then(
                      founded => {
                          findedLbl.textContent = founded;
                          spoilerTitle.textContent = this.ttlShow;
                          spoilerHead.removeAttribute("notready");
                          if (this.options.showAlerts)
                              this.endNotify(founded, errorsCntr)
                              else if (this.options.autoOpenSpoilerWithResult) {
                                  spoilerBody.hidden = false
                                  spoilerTitle.textContent = this.ttlHide;
                              };
                          return
                      }
                      , notfound => {
                          spoilerTitle.textContent = this.ttlNotFnd;
                          this.options.showAlerts && this.endNotify(null, errorsCntr, notfound);
                          return
                      }
                  ).catch(err => {
                      errorsCntr += 1; errsLbl.textContent = errorsCntr
                  })
                  this.names.length = 0;
              }
          };
    style.type = "text/css"; style.textContent = css;
    if (dats)
        for (const dat of dats) {
            const button = dat.appendChild(document.createElement("br")) && dat.appendChild(document.createElement("br"))
                               && dat.appendChild(document.createElement("button")),
                  name = dat.getElementsByTagName("b")[0].textContent;
            button.className = "spu-search";
            button.textContent = "Найти посты";
            button.title = "Ctrl + ЛКМ: Добавить этого пользователя в задание для поиска\nЛКМ: Поиск постов для пользователя(-ей)/для выделенного\nПКМ: Показать форму для ввода имен пользователей";
            button.onclick = button.oncontextmenu = e => listener.handleEvent(e, name, e.button == 2);
        }
    if (listener.options.searchOnProfileLinksByRMB)
        body.addEventListener("contextmenu", e => {
            const target = e.target.closest("a[href*='?action=show&member=']"),
                  name = target && target.search.split("&member=")[1];
            if (!target || !name || e.altKey || e.shiftKey || e.ctrlKey || e.metaKey
                || !!listener.getSel() || target.closest("#spu-spoiler-body")) return;
            listener.handleEvent(e, name)
        });
    if (listener.isFF && listener.options.createCnMenu) {
        const context = body.getAttribute("contextmenu"),
              menu = context
                        ? document.getElementById(context)
                        : body.appendChild(document.createElement("menu")),
              mitem = menu.appendChild(document.createElement("menuitem"));
        if (!context) {
            menu.id = "GM_page-actions";
            menu.type = "context";
            body.setAttribute("contextmenu", "GM_page-actions");
        };
        mitem.label = "Найти собщения пользователя(-ей)";
        mitem.onclick = e => listener.handleEvent(e, null, !listener.getSel());
    };
});