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