mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 09:33:08 +01:00
feat: optimize search.php for the mobile view
search.php had no mobile treatment: the query form rendered as an unstyled desktop table, and a submitted search fell back to the regular forum-listing view on the untested assumption that results looked like topic rows. The query form is now recognised by its forum-picker select, the one element the small "search this forum/topic" widgets embedded on other pages don't share — they reuse the same form action and keyword field name and would otherwise be picked up by mistake. Results in the "Posts" display mode the in-thread search bar produces turned out to carry post excerpts, not topic rows, and needed their own card layout instead of running through the list view, which would have misread the topic and profile links inside each hit. Submit/reset controls kept native iOS Safari chrome even under -webkit-appearance:none once restyled, clipping their labels — only visible on device, not in the emulator this project is developed against. Replacing them with real <button> elements (same name/value, so the submitted form data is unchanged) fixed it where the CSS-only attempt didn't; touch targets were sized up to match.
This commit is contained in:
+308
-8
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name RIN Mobile
|
||||
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
||||
// @version 3.9
|
||||
// @version 3.13
|
||||
// @match *://cs.rin.ru/forum/*
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
@@ -17,6 +17,23 @@
|
||||
function txt(el) { return (el ? el.textContent || '' : '')
|
||||
.replace(/\s+/g, ' ').trim(); }
|
||||
|
||||
// input[type=submit]/[type=reset] behalten in iOS Safari trotz
|
||||
// -webkit-appearance:none teils ihre native Knopfoptik und setzen eigenes
|
||||
// Padding und Zentrierung nur unzuverlaessig um — ein echtes <button> wird
|
||||
// unter appearance:none dagegen zuverlaessig respektiert. name und value
|
||||
// wandern mit, damit sich am uebermittelten Formularfeld nichts aendert.
|
||||
function inputToButton(input) {
|
||||
if (!input) return null;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = input.type;
|
||||
if (input.name) btn.name = input.name;
|
||||
if (input.hasAttribute('value')) btn.value = input.value;
|
||||
btn.className = input.className;
|
||||
btn.textContent = input.value;
|
||||
input.replaceWith(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Der Seitentyp entscheidet, welche Ansicht gebaut wird. Beide Ansichten
|
||||
// liegen in einem Script, weil sie sich Grundstile und Klassennamen teilen.
|
||||
// Als getrennte Scripts waren auf Threadseiten beide Stylesheets aktiv und
|
||||
@@ -131,8 +148,12 @@
|
||||
font-size:16px; padding:9px 12px; border-radius:10px;
|
||||
color:#eee; background:rgba(120,120,120,.18);
|
||||
border:1px solid rgba(160,160,160,.35); }
|
||||
.rm-search-bar input[type="submit"] {
|
||||
flex:none; font-size:14px; font-weight:650; padding:9px 16px;
|
||||
.rm-search-bar button[type="submit"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
flex:none; min-height:44px; font-size:14px; font-weight:650;
|
||||
padding:12px 16px;
|
||||
border-radius:10px; color:#fff; border:0;
|
||||
background:rgba(120,170,255,.85); }
|
||||
|
||||
@@ -297,10 +318,104 @@
|
||||
text-decoration:none !important; color:inherit !important; }
|
||||
.rm-login form input[type="checkbox"] { width:18px; height:18px;
|
||||
vertical-align:middle; margin-right:8px; }
|
||||
.rm-login form input[type="submit"] {
|
||||
width:100%; font-size:16px; font-weight:650; padding:13px;
|
||||
margin-top:20px; border-radius:10px; color:#fff; border:0;
|
||||
.rm-login form button[type="submit"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
width:100%; min-height:48px; font-size:16px; font-weight:650;
|
||||
padding:16px 13px; margin-top:20px; border-radius:10px; color:#fff;
|
||||
border:0; background:rgba(120,170,255,.85); }
|
||||
|
||||
/* === Suchformular (search.php ohne Treffer) === */
|
||||
|
||||
.rm-search-shortcuts { display:flex; flex-wrap:wrap; gap:8px;
|
||||
padding:0 14px 16px; }
|
||||
.rm-search-shortcuts a { font-size:13px; padding:6px 12px; border-radius:99px;
|
||||
border:1px solid rgba(128,128,128,.4);
|
||||
text-decoration:none !important; color:inherit !important; }
|
||||
|
||||
/* Derselbe Tabelle-zu-Block-Trick wie bei .rm-login form, eigener Name
|
||||
statt Wiederverwendung: das Formular hier ist ein anderes und hat
|
||||
andere Feldtypen (radio, select statt checkbox). */
|
||||
.rm-searchform form { display:block;
|
||||
padding:4px 14px calc(80px + env(safe-area-inset-bottom)); }
|
||||
.rm-searchform form table, .rm-searchform form tbody,
|
||||
.rm-searchform form tr, .rm-searchform form td {
|
||||
display:block !important; width:100% !important; }
|
||||
.rm-searchform form td { padding:0 !important; }
|
||||
/* Bleibt sichtbar statt wie beim Login versteckt: die beiden th-Zeilen
|
||||
("Search query", "Search options") sind hier Abschnittstrenner, keine
|
||||
Formular-Kopfzelle ohne eigenen Inhalt. */
|
||||
.rm-searchform form th { display:block !important; padding:16px 0 4px;
|
||||
font-size:12px; font-weight:700;
|
||||
text-transform:uppercase; letter-spacing:.06em;
|
||||
opacity:.55; }
|
||||
.rm-searchform form b.genmed { display:block; margin:14px 0 4px;
|
||||
font-size:14px; font-weight:650; }
|
||||
.rm-searchform form span.gensmall { display:block; margin-bottom:6px;
|
||||
font-size:12px; opacity:.6; }
|
||||
.rm-searchform form input[type="text"] {
|
||||
box-sizing:border-box; width:100%; font-size:16px; padding:11px 12px;
|
||||
border-radius:10px; color:#eee; background:rgba(120,120,120,.18);
|
||||
border:1px solid rgba(160,160,160,.35); }
|
||||
.rm-searchform form select {
|
||||
box-sizing:border-box; width:100%; font-size:16px; padding:9px 12px;
|
||||
margin-top:4px; border-radius:10px; color:#eee;
|
||||
background:rgba(120,120,120,.18); border:1px solid rgba(160,160,160,.35); }
|
||||
.rm-searchform form select[multiple] { font-size:15px; padding:4px; }
|
||||
.rm-searchform form input[type="radio"] { width:18px; height:18px;
|
||||
vertical-align:middle; margin-right:6px; }
|
||||
/* Einzige Zeile ohne Block-Stapelung: Suchen/Zuruecksetzen sollen
|
||||
nebeneinander stehen, nicht wie der Rest des Formulars untereinander. */
|
||||
.rm-searchform form td.cat { display:flex !important; gap:10px;
|
||||
padding-top:10px !important; }
|
||||
/* input[type=submit]/[type=reset] behalten in iOS Safari trotz
|
||||
-webkit-appearance:none teils die native Knopfoptik und setzen Padding,
|
||||
Rahmen und Zentrierung nur unzuverlaessig um — deshalb echte <button>
|
||||
statt der Original-<input>-Elemente, siehe inputToButton(). */
|
||||
.rm-searchform form button[type="submit"],
|
||||
.rm-searchform form button[type="reset"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
min-height:48px; font-size:16px; font-weight:650;
|
||||
padding:16px 13px; border-radius:10px;
|
||||
border:0; }
|
||||
.rm-searchform form button[type="submit"] { flex:1; color:#fff;
|
||||
background:rgba(120,170,255,.85); }
|
||||
.rm-searchform form button[type="reset"] { flex:none; color:#eee;
|
||||
background:rgba(120,120,120,.18);
|
||||
border:1px solid rgba(160,160,160,.45) !important; }
|
||||
|
||||
/* === Suchtreffer (search.php mit Ergebnissen, Modus "Posts") === */
|
||||
|
||||
.rm-hits-info { padding:0 14px 14px; font-size:13px; opacity:.75;
|
||||
overflow-wrap:break-word; }
|
||||
.rm-hits-info a { color:inherit !important; }
|
||||
|
||||
.rm-hit { box-sizing:border-box !important; display:block !important;
|
||||
width:100% !important; padding:14px !important;
|
||||
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||||
.rm-hit-ctx { display:block; margin-bottom:8px; font-size:12px;
|
||||
font-weight:650; text-transform:uppercase; letter-spacing:.03em;
|
||||
opacity:.6; text-decoration:none !important;
|
||||
color:inherit !important; overflow-wrap:break-word; }
|
||||
.rm-hit-head { display:flex; align-items:baseline; justify-content:space-between;
|
||||
gap:8px; }
|
||||
.rm-hit-user { font-size:14px; font-weight:650; }
|
||||
.rm-hit-when { flex:none; font-size:12px; opacity:.55; }
|
||||
/* Wiederverwendet: dieselbe .rm-meta-Regel aus der Themenliste, gleiche
|
||||
"X Antworten · Y Views"-Optik. */
|
||||
.rm-hit .rm-meta { margin-top:4px; }
|
||||
/* .rm-body wird 1:1 vom Thread uebernommen (gleiches Beitragsmarkup),
|
||||
nur der verschobene Container ist hier ein td statt div/span und
|
||||
braucht die Umstellung auf Block, die .rm-body selbst nicht mitbringt. */
|
||||
.rm-root.rm-hits .rm-body { margin-top:8px; }
|
||||
.rm-root.rm-hits .rm-body > td { display:block !important; padding:0 !important; }
|
||||
.rm-hit-go { display:inline-block; margin-top:10px; font-size:12px;
|
||||
padding:5px 10px; border:1px solid rgba(128,128,128,.4);
|
||||
border-radius:99px; text-decoration:none !important;
|
||||
color:inherit !important; }
|
||||
|
||||
/* === Seitennavigation, beide Ansichten === */
|
||||
|
||||
@@ -1005,7 +1120,190 @@
|
||||
return n;
|
||||
}
|
||||
|
||||
// search.php ohne Treffer zeigt nur das Suchformular — keine topictitle-
|
||||
// oder forumlink-Zeilen, an denen buildList() sonst seine Karten festmacht.
|
||||
// Erkannt wird das Formular am Forenwaehler select[name="fid[]"]: der
|
||||
// steckt nur im grossen Suchformular, nicht in den kleinen "Search this
|
||||
// forum/topic..."-Formularen auf Foren- und Themenseiten, die denselben
|
||||
// Formularnamen "keywords" verwenden wuerden und sonst faelschlich
|
||||
// getroffen werden koennten.
|
||||
function buildSearchForm() {
|
||||
var picker = document.querySelector('select[name="fid[]"]');
|
||||
var form = picker && picker.closest('form');
|
||||
if (!form) return false;
|
||||
|
||||
var root = document.createElement('div');
|
||||
root.className = 'rm-root rm-searchform';
|
||||
|
||||
var title = document.createElement('div');
|
||||
title.className = 'rm-login-title';
|
||||
title.textContent = 'Suche';
|
||||
root.appendChild(title);
|
||||
|
||||
// Schnellzugriffe (View new posts, View your posts, ...) stehen als
|
||||
// eigene Linkliste ausserhalb des Formulars und würden sonst mit dem Rest
|
||||
// der Originalseite verschwinden.
|
||||
var quick = document.querySelector('p.searchbar');
|
||||
if (quick) {
|
||||
var links = quick.querySelectorAll('a');
|
||||
if (links.length) {
|
||||
var chips = document.createElement('div');
|
||||
chips.className = 'rm-search-shortcuts';
|
||||
links.forEach(function (a) {
|
||||
var c = document.createElement('a');
|
||||
c.href = a.href;
|
||||
c.textContent = txt(a);
|
||||
chips.appendChild(c);
|
||||
});
|
||||
root.appendChild(chips);
|
||||
}
|
||||
}
|
||||
|
||||
// Die Textfelder tragen ein inline style="width: 300px", das jede externe
|
||||
// Breitenregel ohne !important sticht.
|
||||
form.querySelectorAll('input[type="text"]').forEach(function (inp) {
|
||||
inp.removeAttribute('style');
|
||||
});
|
||||
|
||||
form.querySelectorAll('input[type="submit"], input[type="reset"]')
|
||||
.forEach(inputToButton);
|
||||
|
||||
root.appendChild(form);
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
log.push('suche=formular');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Eine Suche im Modus "Posts" (Standard beim Suchen ueber die zweite Leiste
|
||||
// im Thread, da deren Formular kein sr setzt) liefert etwas ganz anderes
|
||||
// als eine Themenliste: pro Treffer Autor, Zeitstempel und ein Auszug aus
|
||||
// dem Beitrag selbst, keine topictitle-/forumlink-Zeilen. buildList() wuerde
|
||||
// die darin enthaltenen viewtopic.php-Links trotzdem einsammeln und an
|
||||
// rowKind() falsch klassifizieren, ohne diese eigene Behandlung davor.
|
||||
// Erkannt wird der Treffer-Modus an p.topictitle — als <p>, nicht als <a>
|
||||
// wie das gleichnamige forumlink-Gegenstueck einer Themenzeile.
|
||||
function buildSearchResults() {
|
||||
var hits = document.querySelectorAll('p.topictitle');
|
||||
if (!hits.length) return false;
|
||||
|
||||
var root = document.createElement('div');
|
||||
root.className = 'rm-root rm-hits';
|
||||
|
||||
var summary = document.querySelector('span.titles');
|
||||
var title = document.createElement('div');
|
||||
title.className = 'rm-login-title';
|
||||
title.textContent = summary ? txt(summary) : 'Suchergebnisse';
|
||||
root.appendChild(title);
|
||||
|
||||
// "Searched topic: ... / Search term used: ..." traegt funktionierende
|
||||
// Links (erneute Suche, Sprung zum Thema) und wird deshalb wie das
|
||||
// Formular selbst uebernommen statt aus dem Text nachgebaut.
|
||||
var form = hits[0].closest('form');
|
||||
var infoCell = form && form.querySelector('td.genmed');
|
||||
if (infoCell) {
|
||||
infoCell.classList.add('rm-hits-info');
|
||||
root.appendChild(infoCell);
|
||||
}
|
||||
|
||||
var built = 0;
|
||||
hits.forEach(function (p) {
|
||||
try {
|
||||
var card = buildHit(p);
|
||||
if (card) { root.appendChild(card); built++; }
|
||||
} catch (e) { log.push('treffer: ' + e.message); }
|
||||
});
|
||||
log.push('treffer=' + built);
|
||||
if (!built) return false;
|
||||
|
||||
// Muss vor replaceBody() laufen: pageState() liest das Original-DOM.
|
||||
var pg = pageState();
|
||||
if (pg.total > 1) pageNav(root, pg);
|
||||
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ein Treffer besteht aus drei aufeinanderfolgenden <tr> derselben Tabelle:
|
||||
// Titelzeile (Forum/Thema), Autor- und Betreffzeile, Antworten/Views samt
|
||||
// Beitragsauszug. Die Zuordnung laeuft ueber nextElementSibling statt ueber
|
||||
// Klassen, weil alle drei row1/row2-Zeilen sonst gleich aussehen.
|
||||
function buildHit(p) {
|
||||
var titleRow = p.closest('tr');
|
||||
var infoRow = titleRow && titleRow.nextElementSibling;
|
||||
var bodyRow = infoRow && infoRow.nextElementSibling;
|
||||
|
||||
var forumA = p.querySelector('a[href*="viewforum.php"]');
|
||||
var topicA = p.querySelector('a[href*="viewtopic.php"]');
|
||||
if (!topicA) return null;
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'rm-hit';
|
||||
|
||||
var ctx = document.createElement('a');
|
||||
ctx.className = 'rm-hit-ctx';
|
||||
ctx.href = topicA.href;
|
||||
ctx.textContent = breakable((forumA ? txt(forumA) + ' › ' : '') + txt(topicA));
|
||||
el.appendChild(ctx);
|
||||
|
||||
var author = '', posted = '', subjectA = null;
|
||||
if (infoRow) {
|
||||
var ua = infoRow.querySelector('.postauthor');
|
||||
author = ua ? txt(ua) : '';
|
||||
subjectA = infoRow.querySelector('a[href*="viewtopic.php"]');
|
||||
var pm = txt(infoRow).match(/Posted:\s*(.+)$/i);
|
||||
if (pm) posted = pm[1].trim();
|
||||
}
|
||||
|
||||
var head = document.createElement('div');
|
||||
head.className = 'rm-hit-head';
|
||||
var who = document.createElement('span');
|
||||
who.className = 'rm-hit-user';
|
||||
who.textContent = author || 'Gast';
|
||||
head.appendChild(who);
|
||||
if (posted) {
|
||||
var when = document.createElement('span');
|
||||
when.className = 'rm-hit-when';
|
||||
when.textContent = posted;
|
||||
head.appendChild(when);
|
||||
}
|
||||
el.appendChild(head);
|
||||
|
||||
if (bodyRow) {
|
||||
// txt() haengt "Replies:" und "Views:" ohne Trennzeichen aneinander,
|
||||
// weil das <br /> dazwischen keinen Leerraum hinterlaesst — deshalb
|
||||
// einzeln herausgegriffen und ueber dieselbe metaRow() wie bei
|
||||
// Themenkarten mit " · " verbunden.
|
||||
var metaTxt = txt(bodyRow.querySelector('.postdetails'));
|
||||
var reM = metaTxt.match(/Replies:\s*([\d.,]+)/i);
|
||||
var viM = metaTxt.match(/Views:\s*([\d.,]+)/i);
|
||||
var metaEl = metaRow([reM ? reM[1] + ' Antworten' : '',
|
||||
viM ? viM[1] + ' Views' : '']);
|
||||
if (metaEl) el.appendChild(metaEl);
|
||||
|
||||
var pb = bodyRow.querySelector('.postbody');
|
||||
if (pb) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'rm-body';
|
||||
wrap.appendChild(pb);
|
||||
el.appendChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
var jump = document.createElement('a');
|
||||
jump.className = 'rm-hit-go';
|
||||
jump.href = (subjectA || topicA).href;
|
||||
jump.textContent = 'Zum Beitrag ↓';
|
||||
el.appendChild(jump);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function buildList() {
|
||||
if (buildSearchForm()) return;
|
||||
if (buildSearchResults()) return;
|
||||
|
||||
// Dokumentreihenfolge entscheidet: in einer Forenzeile steht der Forenname
|
||||
// vor dem Link auf den letzten Beitrag. Ohne diese Reihenfolge faellt die
|
||||
// Zeile als Thema an, der Forenname verschwindet und nur die Beschreibung
|
||||
@@ -1113,7 +1411,7 @@
|
||||
inp.placeholder = 'In diesem Thema suchen';
|
||||
}
|
||||
var submit = form.querySelector('input[type="submit"]');
|
||||
if (submit) submit.value = 'Suchen';
|
||||
if (submit) { submit.value = 'Suchen'; inputToButton(submit); }
|
||||
|
||||
var bar = document.createElement('div');
|
||||
bar.className = 'rm-search-bar';
|
||||
@@ -1334,6 +1632,8 @@
|
||||
if (!td.children.length && !txt(td)) td.classList.add('rm-hidden');
|
||||
});
|
||||
|
||||
inputToButton(form.querySelector('input[type="submit"]'));
|
||||
|
||||
root.appendChild(form);
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
@@ -1387,7 +1687,7 @@
|
||||
if (DEBUG) {
|
||||
var d = document.createElement('div');
|
||||
d.className = 'rm-dbg';
|
||||
d.textContent = 'RIN Mobile v3.9\n' + log.join('\n');
|
||||
d.textContent = 'RIN Mobile v3.13\n' + log.join('\n');
|
||||
document.body.insertBefore(d, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user