// ==UserScript==
// @name FastTech Forum Enhancements (BETA)
// @namespace ftil
// @description Ignore users on the FastTech forums, and more
// @include https://*.fasttech.com/*
// @version 2.2.1
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB+klEQVRYw+2XPUscQRjHF+J7YWWhKDtLwJyIWmhnoU16r/YDREgRsLAysi+ije55OzPnzKRR1Car3gwXsAiBiI0fILWtqB8i57OeC3eKuDt3egr3wI+FLWb+PO9jGM02QngOY8Z02NrazlieyliOZDqYrswZmPIlEFEmNCWEnQoh2qe+iHbkyFPkqnJaTEct3XkBF0QWROxhwg+SAJcvB0HQGXsR2TtdyFHLyJUHtajLymXy4sH/PeTJ7IuHFzzz+17AblPy630IYIwNQ/z/6ZDPb4/VLQAOGk9fAbyMMd9oiAdqBBDuQX0vPAelfKZhIagWkC8UJl49B1oCWgKgp/diLLIRQbDf20gB5qqcjnq+aZcmnxZA2VwsIC2U0n5kh/3RJTpYbnEuCsFh6kZUGcdXm0L0DdilPtORVzrjGEJzaPi+3323YBB+DQff1ECqoNVfdkbIj0+xF63vKoOc4hnE/CYRrrqOFpKhxZ/dRsuabrkcH8CU+TrkYZR/9H4NQxL6WrhyXW8cVybneRiGHaN22AEHnestpXKlRgAm7A9k+HEC1mAj7om9CKXYAxvuGgg5TgIssEfIlvMv3ooTWUtAvQKQXRp5/CBJiFMU9XsgDD9AAp5ozIH/wFeYBWwwfmyCDep4cdb+22a56lviR6mnCuaq+vwmGuEte5DlWQaVMykAAAAASUVORK5CYII=
// ==/UserScript==
'use strict';
// Delay function calls until their preconditions have been met.
var block = (function() {
var counters = { };
return {
get: function(name) { return counters[name].val !== 0 },
block: function(name) {
if (counters[name] === undefined)
counters[name] = { val: 1, watches: [] };
else if (counters[name].val !== 0)
counters[name].val++;
},
unblock: function(name) {
var c = counters[name];
if (c.val !== 0)
c.val--;
if (c.val === 0) {
var w = c.watches;
while (w.length !== 0)
w.shift()();
}
},
watch: function(name, func) {
var c = counters[name];
c.watches.push(func);
if (c.val === 0 && c.watches.length === 1)
this.unblock(name);
},
};
})();
/* Userscript settings:
* This is just terrible. GM_[sg]etValue is used to initialize settings since
* it's shared across subdomains, while localStorage is used to notify other
* tabs of new settings.
*/
var settings = (function() {
var settings = { };
var watches = { };
var prefix = 'ignore_list:';
var prere = new RegExp('^' + prefix + '(.+)');
// Unblock after execution of the script
block.block('ready');
function inner_set(name, value) {
// Ugh.
if (JSON.stringify(settings[name].value) === JSON.stringify(value))
return false;
settings[name].value = value;
if (name in watches) {
var w = watches[name];
for (var i = 0; i < w.length; i++)
w[i](name, value);
}
return true;
}
function migrate(name) {
var v = localStorage.getItem(name);
if (v !== null) {
GM_setValue(name, v);
localStorage.removeItem(name);
}
}
window.addEventListener('storage', function(e) {
var set = e.key.match(prere);
if (set !== null && set[1] in settings)
inner_set(set[1], JSON.parse(e.newValue));
});
function get_sync(name) {
migrate(name);
var val = GM_getValue(name, null);
if (val !== null)
return JSON.parse(val);
else
return settings[name].def;
}
function wrap_get(name) {
if (settings[name].value === undefined)
settings[name].value = get_sync(name);
return settings[name].value;
}
function request() {
for (var k in settings) {
if (settings[k] === undefined)
settings[k] = get_sync(k);
}
block.unblock('ready');
}
return {
get: wrap_get,
get_sync: get_sync,
set: function(name, value) {
if (inner_set(name, value)) {
var s = JSON.stringify(value);
localStorage.setItem(prefix + name, s);
GM_setValue(name, s);
}
},
all: function() { return settings },
register: function(name, desc, def, dep) {
settings[name] = {
desc: desc,
def: def,
dep: dep,
};
},
watch: function(name, func) {
if (!(name in watches))
watches[name] = [];
watches[name].push(func);
func(name, wrap_get(name));
},
update: request,
};
})();
// Sanitize external values that may be fed into an innerHTML
var strip_tags = (function() {
var div = document.createElement('div');
return function(t) {
div.innerHTML = t;
return div.textContent;
}
})();
// Case-insensitive indexOf (needed in case of case errors in user input)
function ci_indexof(a, n) {
var l = n.toLowerCase();
for (var i = 0; i < a.length; i++) {
if (a[i].toLowerCase() === l)
return i;
}
return -1;
}
// Case-insensitive attribute lookup
function ci_lookup(a, n) {
var l = n.toLowerCase();
for (var k in a) {
if (k.toLowerCase() === l)
return k;
}
}
// Assorted generic handlers for user input.
var handlers = (function() {
return {
// A click on elm toggles the ignored state of user
ignore_toggle: function(elm, user) {
elm.addEventListener('click', function() { ignore_list.set(user) });
},
// A click on elm toggles the state of setting; setting changes update elm
checkbox: function(elm, setting) {
elm.addEventListener('change',
function() { settings.set(setting, this.checked) });
settings.watch(setting, function(name, val) {
elm.checked = val;
});
},
// Handle enter keypress on manage_menu's text box
addbox_enter: function(elm) {
elm.addEventListener('keydown', function(k) {
if (k.which !== 13)
return true;
k.stopPropagation();
k.preventDefault();
ignore_list.set(strip_tags(this.value), true);
this.value = '';
return false;
});
},
// Handle click on manage_menu's add button
addbox_click: function(elm, tgt) {
elm.addEventListener('click', function() {
ignore_list.set(strip_tags(tgt.value), true);
tgt.value = '';
});
},
top_click: function(elm) {
elm.addEventListener('click', function() { scrollTo(0, 0) });
},
hide_toggle: function(elm, post) {
elm.addEventListener('click', function() { unhide_posts.set(post) });
},
};
})();
// Manage the state of the ignore list
var ignore_list = (function() {
// Use two stages so that filters can handle updates before being called
var watches = { 'early': [], 'late': [] };
var il = [];
function fire_watches(user, state, idx) {
['early', 'late'].forEach(function(stage) {
var w = watches[stage];
for (var i = 0; i < w.length; i++)
w[i](user, state, idx);
});
}
// In order to support userscripts, we need to rebuild the list on every set
function inner_set(user, state, idx) {
if (settings.get_sync)
il = settings.get_sync('ignorelist');
if (state)
il.push(user);
else
il.splice(idx, 1);
settings.set('ignorelist', il);
fire_watches(user, state, idx);
}
var connect = function() {
// Watch for ignorelist changes, and fire incremental watch events
settings.watch('ignorelist', function(name, val) {
if (il.length === val.length)
return;
if (il.length === 0) {
for (var i = 0; i < val.length; i++)
il.push(val[i]);
fire_watches(il, true);
return;
}
var i = 0, j = 0;
while (true) {
if (il[i] === undefined && val[j] === undefined)
break;
if (il[i] !== val[j]) {
var user, state, idx;
if (il[i] === undefined) {
// New entry @ end of list: user added
user = val[j];
il.push(user);
state = true;
idx = i;
} else {
// Missing entry inside list: user removed
user = il[i];
il.splice(i, 1);
state = false;
idx = i;
}
fire_watches(user, state, idx);
continue;
}
i++;
j++;
}
});
connect = function() { };
};
return {
// Get ignored state of user (true == ignored)
get: function(user) { return (ci_indexof(il, user) !== -1) },
// Set ignored state of user (undefined == toggle)
set: function(user, state) {
var idx = ci_indexof(il, user);
if (state === undefined)
state = (idx === -1);
if ((state && (idx === -1)) || (!state && (idx !== -1)))
inner_set(user, state, idx);
},
/* Watch the ignorelist for changes (stage == ('early'|'late')).
* func may be called with a single user and a new state, or an array of
* users (with implicit ignored state for all)
*/
watch: function(stage, func) {
connect();
watches[stage].push(func);
if (il !== [])
func(il, true, -1);
},
update: function() {
settings.register('ignorelist', null, [], null);
},
};
})();
/* Core post filtering logic
* post vars are expected to be in the format
* { user: string, content_elm: element, post_elms: [element] }
*/
var post_filter = (function() {
var posts;
var watches = [];
var filters;
settings.register('hide_ignored', 'Hide posts by ignored users', true, null);
block.block('filter');
function update() {
if (posts === undefined)
return;
for (var i = 0; i < posts.length; i++) {
var post = posts[i];
var hide = false;
for (var j = 0; j < filters.length; j++) {
var tmp = filters[j](post);
if (tmp === true) {
hide = true;
} else if (tmp === false) {
hide = false;
break;
}
}
var display = hide ? 'none' : '';
for (var j = 0; j < post.post_elms.length; j++)
post.post_elms[j].style.display = display;
for (var j = 0; j < watches.length; j++)
watches[j](post, hide);
}
block.unblock('filter');
}
function builtin() {
filters = [];
filters.unshift((function() {
settings.watch('hide_ignored', post_filter.update);
ignore_list.watch('late', post_filter.update);
return function(post) {
if (settings.get('hide_ignored') && ignore_list.get(post.user))
return true;
}
})());
unhide_posts = (function() {
var posts = [];
filters.unshift(function(post) {
if (posts.indexOf(post) !== -1)
return false;
});
return {
// Set the unhide state of a post (true == unhide, undefined == toggle)
set: function(post, state) {
var idx = posts.indexOf(post);
if (state === undefined)
state = (idx === -1);
if (state && idx === -1)
posts.push(post);
else if (!state && idx !== -1)
posts.splice(idx, 1);
else
return;
post_filter.update();
}
};
})();
}
return {
// Register a list of posts to be filtered
register: function(list) {
posts = list;
update();
},
/* Add a new filter. Filters return true (should hide), false (must not
* hide), or undefined (no judgement). Does not trigger update().
*/
filter: function(func) {
if (filters === undefined)
builtin();
filters.push(func);
},
// Run all filters again
update: update,
/* Call func with changes to the hidden status of each post (true ==
* hidden). Does not trigger update().
*/
watch: function(func) { watches.push(func) },
};
})();
var unhide_posts;
// Generic buttons to toggle ignored state of a user
var ignore_buttons = (function() {
var posts;
var inner_ign = '';
var inner_unign = '';
function update_btns(users, state) {
if (users instanceof Array) {
for (var i = 0; i < posts.length; i++) {
posts[i].ignbtn.innerHTML =
ci_indexof(users, posts[i].user) !== -1 ? inner_unign : inner_ign;
}
} else {
var ul = users.toLowerCase();
for (var i = 0; i < posts.length; i++) {
if (!ul.localeCompare(posts[i].user.toLowerCase()))
posts[i].ignbtn.innerHTML = state ? inner_unign : inner_ign;
}
}
}
return {
// Set the (HTML) format of the (un)ignore buttons
set: function(ign, unign) {
inner_ign = ign;
inner_unign = unign;
},
// Register posts to have their ignore buttons handled
register: function(p) {
posts = p;
for (var i = 0; i < p.length; i++)
handlers.ignore_toggle(p[i].ignbtn, p[i].user);
ignore_list.watch('late', update_btns);
},
};
})();
// Handle RES-style tagging of users
var user_tags = (function() {
var posts;
function update_badges(n, val) {
for (var i = 0; i < posts.length; i++) {
var post = posts[i];
var k = ci_lookup(val, post.user);
post.badge.innerHTML = k !== undefined ? val[k] : post.saved_badge;
}
}
function do_prompt(e, user) {
e.preventDefault();
e.stopPropagation();
var tmp =
(settings.get_sync ? settings.get_sync : settings.get)('user_tags');
var def = '';
if (tmp[user] !== undefined)
def = tmp[user];
var tag = prompt('Tag for ' + user + ' or blank for default:', def);
if (tag === null)
return;
tag = strip_tags(tag);
// Ugh.
var tags = { };
for (var u in tmp)
tags[u] = tmp[u];
if (tags[user] !== undefined && tag === '')
delete tags[user];
else if (tag !== '')
tags[user] = tag;
settings.set('user_tags', tags);
}
function gen_prompt(user) {
return function(e) { do_prompt(e, user); }
}
return {
// Register posts to have their badges handled
register: function(p) {
posts = p;
for (var i = 0; i < p.length; i++) {
var post = p[i];
post.saved_badge = post.badge.innerHTML;
post.badge.addEventListener('click', gen_prompt(post.user));
}
settings.watch('user_tags', update_badges);
},
update: function() {
settings.register('user_tags', null, { }, null);
},
};
})();
// Track the last viewed page and post count for threads
var last_viewed = (function() {
var db;
var store = 'thread_data';
// Accumulate commands and fire them once idb is open
var cmds = [];
function run_cmds() {
if (db === undefined || db === null) {
open_db();
return;
}
var cmd;
while (cmd = cmds.shift()) {
if (cmd.cmd === 'set') {
delete cmd.cmd;
var os = db.transaction(store, 'readwrite').objectStore(store);
os.get(cmd.id).onsuccess = (function(os, cmd) {
return function(e) {
var t = e.target.result;
if (t === undefined) {
t = cmd;
} else {
t.lvt = cmd.lvt;
t.lvp = cmd.lvp;
if (cmd.lpn > t.lpn) {
t.lpn = cmd.lpn;
t.lp = cmd.lp;
}
}
os.put(t);
}
})(os, cmd);
} else if (cmd.cmd === 'get') {
var os = db.transaction(store, 'readonly').objectStore(store);
os.get(cmd.id).onsuccess = (function(cmd) {
return function(e) {
var t = e.target.result;
if (t === undefined)
return;
cmd.f(cmd.e, t.lvp, t.lpn, t.lp);
}
})(cmd);
}
}
}
// Open idb and maybe remove old threads
function open_db() {
if (db === undefined)
db = null;
else
return;
var req = indexedDB.open('ignore_list', 2);
req.onupgradeneeded = function(e) {
var tmp = e.target.result;
if (!tmp.objectStoreNames.contains(store)) {
var os = tmp.createObjectStore(store, { keyPath: 'id' });
os.createIndex('lvt', 'lvt');
}
};
req.onsuccess = function(e) {
db = e.target.result;
run_cmds();
/* Periodically scrub the database (FIXME this is awful & may run in
* multiple tabs simultaneously)
*/
settings.register('lv_scrub_time', null, 0, null);
settings.watch('lv_scrub_time', function(n, v) {
var now = Date.now();
if (v + 24 * 60 * 60 * 1000 > now)
return;
var tr = db.transaction(store, 'readwrite');
var os = tr.objectStore(store);
os.index('lvt')
.openCursor(IDBKeyRange.upperBound(now - 60 * 24 * 60 * 60 * 1000))
.onsuccess = function(e) {
var c = e.target.result;
if (c) {
os.delete(c.primaryKey);
c.continue();
} else {
settings.set('lv_scrub_time', now);
}
};
});
};
}
return {
/* Record a visit to thread_id, on page last_viewed_page, with up to
* last_post_nr (of id last_post_id) visible. Should be called on every
* (thread) pageview.
*/
set: function(thread_id, last_viewed_page, last_post_nr, last_post_id) {
cmds.push({
cmd: 'set',
id: thread_id,
lvt: Date.now(),
lvp: last_viewed_page,
lpn: last_post_nr,
lp: last_post_id,
});
run_cmds();
},
// Call func with the view history of thread_id (and pass extra as well)
get: function(thread_id, func, extra) {
cmds.push({ cmd: 'get', id: thread_id, f: func, e: extra });
run_cmds();
},
update: open_db,
};
})();
// Generic settings menu
var settings_menu = (function() {
return {
/* Attach a settings menu to elm. Menus is a list of submenus, in the form
* [ { title: 'str' | undefined, vars: [ "name" ] } ]
*/
register: function(elm, menus) {
var s = settings.all();
var d = document.createElement('div');
var html = '';
var vs = [];
for (var i = 0; i < menus.length; i++) {
var menu = menus[i];
if (i !== 0)
html += '<hr></hr>';
if (menu.title !== undefined)
html += '<b>' + menu.title + '</b>';
for (var j = 0; j < menu.vars.length; j++) {
var v = menu.vars[j];
vs.push(v);
html += '<div><input type="checkbox" id="ilcb_' + v + '></input>' +
'<label for="ilcb_' + v + '>' + s[v].desc + '</label></div>';
}
}
d.innerHTML = html;
var e = d.firstElementChild;
var depre = /(!)?(.*)/;
function h(e, v) {
handlers.checkbox(e, v);
var d = s[v].dep;
if (d) {
var ds = s[v].dep.match(depre);
var i = !ds[1];
d = ds[2];
settings.watch(d, function(n, v) { e.disabled = v ^ i });
}
}
while (e !== null) {
if (e.tagName === 'DIV')
h(e.firstElementChild, vs.shift());
e = e.nextElementSibling;
}
elm.appendChild(d);
}
};
})();
// Display a nice menu for managing the ignorelist
var manage_menu = (function() {
var menu_list;
var menu_elms = [];
var remfmt = '';
function create_elm(user) {
var tr = document.createElement('tr');
tr.innerHTML = '<td style="padding: 0px">' + user + '</td>' +
'<td align="right" style="padding: 0px"><a username=' + user + '>' +
remfmt + '</a></td>';
var a = tr.lastElementChild.firstElementChild;
a.href = 'javascript:void(0)';
handlers.ignore_toggle(a, user);
return { user: user.toLowerCase(), elm: tr };
}
function find_elm(user) {
var l = user.toLowerCase();
for (var i = 0; i < menu_elms.length; i++) {
if (menu_elms[i].user.localeCompare(l) === 0)
return i;
}
}
function comp_elm(a, b) { return a.user.localeCompare(b.user) }
function update(user, state) {
if (menu_list === undefined)
return;
if (user instanceof Array) {
var frag = document.createDocumentFragment();
menu_elms = user.map(create_elm).sort(comp_elm);
for (var i = 0; i < menu_elms.length; i++)
frag.appendChild(menu_elms[i].elm);
menu_list.appendChild(frag);
} else {
if (state) {
menu_elms.push(create_elm(user));
menu_elms.sort(comp_elm);
var idx = find_elm(user);
if (idx + 1 < menu_elms.length)
menu_list.insertBefore(menu_elms[idx].elm, menu_elms[idx + 1].elm);
else
menu_list.appendChild(menu_elms[idx].elm);
} else {
var idx = find_elm(user);
menu_elms[idx].elm.remove();
menu_elms.splice(idx, 1);
}
}
}
return {
// Set the format of the remove/unignore button
set: function(fmt) { remfmt = fmt },
// Attach management elements to elm
register: function(elm) {
menu_list = document.createElement('table');
menu_list.innerHTML = '<tbody><tr><td colspan="2" style="padding: 0px">' +
'<input type="text" placeholder="Add user..." width="auto">' +
'</input> <div class="il_addbtn">Add</div></td></tr></tbody>';
var td = menu_list.firstElementChild.firstElementChild.firstElementChild;
handlers.addbox_enter(td.firstElementChild);
handlers.addbox_click(td.lastElementChild, td.firstElementChild);
ignore_list.watch('late', update);
elm.appendChild(menu_list);
},
};
})();
// Where are we?
// In the interest of sanity, ftl.thread is true for all thread-like pages
// (i.e. any page with one or more posts); ftl.threadlist is true for all
// threadlist-like pages (i.e. any page with a list of threads). The
// formatting for all such pages is similar enough to treat them equivalently.
var ftl = {
// Mobile pages
mobile: '^https?://m\\.',
// All forum pages
forums: '^https?://(m|www)\\.fasttech\\.com/forums',
// Thread-like pages (i.e. those with posts)
thread: '^https?://(m|www)\\.fasttech\\.com/forums/[^/]+/t/[0-9]+/.',
// Permalink pages
perma: '^https?://(m|www)\\.fasttech\\.com/forums' +
'/[^/]+/t/[0-9]+/[^/]+\\?[0-9]+$',
// Threadlist-like pages (i.e. those with thread links)
threadlist: '^https?://(m|www)\\.fasttech\\.com/forums' +
'(/[^/]+(/page/[0-9]+)?(/search\\?.*)?)?$',
// Lists of threads for a single author
author: '^https?://(m|www)\\.fasttech\\.com/forums/author/',
// Lists of threads for a single product
product: '^https?://(m|www)\\.fasttech\\.com/p(roducts?)?/',
// New thread page for a product
pnthread: '^https?://(m|www)\\.fasttech\\.com/forums/[0-9]+/post',
// Forum settings page
settings: '^https?://www\\.fasttech\\.com/forums/settings$',
};
for (var k in ftl)
ftl[k] = new RegExp(ftl[k]).test(location.href);
// ftl.editor matches all pages with a post editor
ftl.editor = (ftl.thread && !ftl.perma) ||
(ftl.threadlist && !ftl.settings) ||
ftl.pnthread;
// ftl.threadlist matches all pages listing threads
ftl.threadlist = ftl.threadlist || ftl.author || ftl.product || ftl.settings;
// ftl.forums matches all pages with forum elements (threads or posts)
ftl.forums = ftl.forums || ftl.product;
if (ftl.thread || ftl.settings) {
// Menu configuration
var full_menus = [
{ title: 'General', vars: ['track_viewed', 'track_posts', 'always_unread',
'hide_threads', 'quote_quotes', 'quote_imgs'] },
{ title: 'Thread Menu', vars: ['inthread_menu', 'inthread_manage'] },
{ title: 'Post Hiding', vars: ['placeholders', 'hide_ignored',
'hide_quotes', 'hide_deleted'] },
];
var thread_menu;
if (true) {
thread_menu = [
{ title: 'General', vars: ['quote_quotes', 'quote_imgs'] },
{ title: 'Post Hiding',
vars: ['hide_ignored', 'hide_quotes', 'hide_deleted'] },
];
} else {
thread_menu = full_menus;
}
manage_menu.set('<img alt="Remove" ' +
'src="https://www.fasttech.com/images/minus-button.png"></img></a></td>');
settings.register('inthread_menu', 'Show menu in thread toolbar', true, null);
settings.register('inthread_manage', 'Manage ignored users from the menu',
false, 'inthread_menu');
settings.register('placeholders', 'Show placeholders for hidden posts', true,
null);
settings.register('hide_quotes', 'Hide posts quoting ignored users', false,
'hide_ignored');
settings.register('hide_deleted', 'Hide deleted posts', true, null);
settings.register('quote_quotes', 'Include quotes in quotes', true, null);
settings.register('quote_imgs', 'Include images in quotes', true, null);
}
if (ftl.threadlist) {
settings.register('track_viewed', 'Link to last viewed page', false, null);
settings.register('track_posts', 'Link to unread posts', true, null);
settings.register('always_unread', 'Always show unread posts button', false,
'track_posts');
settings.register('hide_threads', 'Hide threads by ignored users', false,
null);
}
if (ftl.editor) {
settings.register('agree_tou', null, false, null);
}
block.block('ready');
if (ftl.forums) {
ignore_list.update();
last_viewed.update();
if (ftl.thread)
user_tags.update();
settings.update();
block.block('late');
block.watch('ready',
//function() { block.watch('ready', function() { block.unblock('late') });
function() { window.setTimeout(function() { block.unblock('late') }, 0);
});
}
document.addEventListener('readystatechange', function() {
if (document.readyState === 'interactive') block.unblock('ready');
});
if (ftl.thread || ftl.editor) {
/* Fix up display of certain post elements:
* - Fix align tags (which should have been 100% divs to begin with)
* - Avoid breaking SKU links across lines
*/
var style = document.createElement('style');
style.innerHTML =
'.PostContent span[style*="text-align:"] {display:block;width:100%}' +
'a.SKUAutoLink {display=inline-block}';
block.watch('ready', function() { document.body.appendChild(style) });
}
// RE to pull a thread id from a URL
if (ftl.forums)
var thread_id_re = new RegExp('/forums/[^/]+/t/([0-9]+)/[^?]*$');
if (ftl.thread) {
// Add a filter for posts quoting ignored users
post_filter.filter((function() {
var quote_res = [];
function new_qre(user) {
quote_res.push(
new RegExp('\\b' + user + ' wrote(</a>)?:?[\r\n]*<br>', 'i'));
}
function update(user, state, idx) {
if (user instanceof Array) {
quote_res = [];
for (var i = 0; i < user.length; i++)
new_qre(user[i]);
} else {
if (state)
new_qre(user);
else
quote_res.splice(idx, 1);
}
}
ignore_list.watch('early', update);
settings.watch('hide_quotes', post_filter.update);
return function(post) {
if (settings.get('hide_ignored') && settings.get('hide_quotes')) {
for (var i = 0; i < quote_res.length; i++) {
if (quote_res[i].test(post.content_elm.innerHTML))
return true;
}
}
}
})());
// Add a filter for posts that the admins have deleted
post_filter.filter((function() {
var deleted_re = /post deleted/i;
var edited_re = /^[ \r\n]*$/;
settings.watch('hide_deleted', post_filter.update);
return function(post) {
if (settings.get('hide_deleted')) {
var wb = post.content_elm.getElementsByClassName('WarningBox')[0];
if ((wb !== undefined && deleted_re.test(wb.innerHTML)) ||
edited_re.test(post.content_elm.innerHTML))
return true;
}
}
})());
// Get all post bodies, desktop and mobile
var posts;
block.watch('ready', function() {
posts = [];
var elms = document.getElementsByClassName('PostContent');
for (var i = 0; i < elms.length; i++) {
var pc = elms[i];
var user = pc.getAttribute('data-username');
if (user !== null)
posts.push({ user: user, content_elm: pc });
}
});
// Create a function to create placeholders as needed
var gen_watch_filter = function(ph_func) {
return function(post, state) {
var placeholders = settings.get('placeholders');
if (state && placeholders) {
if (!post.placeholder)
post.placeholder = ph_func(post);
post.placeholder.style.display = '';
} else {
if (post.placeholder !== undefined)
post.placeholder.style.display = 'none';
}
}
};
// Get this thread's ID
var thread_id = location.href.match(thread_id_re);
if (thread_id !== null && thread_id[1] !== undefined)
thread_id = parseInt(thread_id[1]);
else
thread_id = undefined;
}
// Get the current and last pages from a pager element, desktop and mobile
function parse_pager(elm) {
var cur, last;
if (elm === undefined || elm === null) {
cur = last = 1;
} else if (!ftl.mobile) {
var sel = elm.getElementsByClassName('PageLink_Selected')[0];
if (sel !== undefined)
cur = parseInt(sel.innerHTML);
last = parseInt(elm.lastElementChild.previousElementSibling.innerHTML);
} else {
var sel = elm.getElementsByClassName('active')[0];
if (sel !== undefined)
cur = parseInt(sel.firstElementChild.innerHTML);
last = parseInt(elm.lastElementChild.firstElementChild.innerHTML);
}
return {
cur: cur,
last: last
};
}
if (!ftl.mobile) {
/* Jump-to-page boxes are broken site-wide. Fix them.
* The existing JumpToPage box could be fixed by changing "function (e)" to
* "function (event)" in the event listener, but instead, I'm doing all this.
*/
var fix_jtp = function() {
var jtpbox = document.getElementById('JumpToPage');
if (jtpbox !== null) {
var lastpg = parse_pager(jtpbox.parentElement).last;
var as = jtpbox.parentElement.getElementsByTagName('a');
if (as[0] !== undefined) {
var base = as[0].href.match(new RegExp('(.*/)[0-9]+($|[?#/].*)'));
if (base !== null) {
jtpbox.addEventListener('keypress', function(k) {
if (k.which === 13) {
k.preventDefault();
k.stopPropagation();
var pg = parseInt(this.value);
if (pg > 0 && pg < lastpg)
location.href = base[1] + pg + base[2];
}
return false;
});
}
}
}
};
block.watch(ftl.forums ? 'late' : 'ready', fix_jtp);
}
if (ftl.threadlist) {
// Translate a post number into a page number, handling FT's off-by-one quirk
var postnr_to_pagenr = function(post, maxpage) {
return Math.floor(Math.min(maxpage, post / 10 + 1));
};
// Create a link to a specific page of a thread
var gen_page_link = function(base, page, hash) {
var ret = base;
if (page !== 1)
ret += '/' + page;
if (hash)
ret += '#' + hash;
return ret;
};
}
// Add a "Top of page" button at the bottom of the page, aligned with the pager
if ((ftl.thread && !ftl.perma) || (ftl.threadlist && !ftl.settings)) {
var add_top_btn;
if (!ftl.mobile) {
add_top_btn = function() {
var elm = document.getElementsByClassName('ForumThread TightTable')[0];
if (elm)
elm = elm.nextElementSibling;
if (!elm)
return;
elm.className = 'Pager';
var tbtn = document.createElement('span');
tbtn.className = 'ControlArrows';
tbtn.setAttribute('style', 'float: left; min-width: 150px; ' +
'max-width: 250px; text-align: center');
tbtn.innerHTML = '<a>Top of page</a>';
tbtn.firstElementChild.href = 'javascript:void(0)';
handlers.top_click(tbtn);
var pager = elm.firstElementChild;
elm.appendChild(tbtn);
if (pager) {
var pspan = document.createElement('span');
pspan.appendChild(pager);
elm.appendChild(pspan);
} else {
elm.parentElement
.insertBefore(document.createElement('br'), elm.nextElementSibling);
}
};
} else {
add_top_btn = function() {
var elm = document.getElementsByClassName('ListRow')[0];
if (elm)
elm = elm.parentElement.nextElementSibling;
if (!elm)
return;
var tbtn = document.createElement('button');
tbtn.className = 'btn btn-default';
tbtn.innerHTML = 'Top';
handlers.top_click(tbtn);
if (elm.firstElementChild) {
tbtn.style.marginTop = '-5px';
elm = elm.firstElementChild;
var td = document.createElement('td');
td.style.textAlign = 'right';
td.appendChild(tbtn);
elm.style.width = '100%';
elm.firstElementChild.firstElementChild.appendChild(td);
} else {
var d = elm.appendChild(document.createElement('div'));
d.setAttribute('style', 'width: 100%; text-align: right; ' +
'margin-bottom: 8px');
d.appendChild(tbtn);
}
};
}
block.watch('ready', add_top_btn);
}
if (ftl.editor) {
var bbeeditor; // element to scroll to
var bbetext; // textarea
var bbeexpand; // expand button (mobile)
var bbepanel; // parent of editor
if (!ftl.mobile) {
var fix_editor = function() {
if (bbetext === undefined)
bbetext = document.getElementsByClassName('mbbc-editor')[0];
if (bbetext !== undefined) {
// Save & restore the editor text
if (history.state && history.state.saved_post)
bbetext.value = history.state.saved_post;
window.addEventListener('unload', function() {
var state = history.state;
state.saved_post = bbetext.value;
history.replaceState(state, '');
});
// Remove the broken size=[67] buttons
var size_popup = document
.getElementsByClassName('mbbc-size-popup')[0];
if (size_popup !== undefined) {
size_popup.lastElementChild.remove();
size_popup.lastElementChild.remove();
}
// Fix the editor width
bbetext.style.boxSizing = 'border-box';
bbetext.style.width = '100%';
// Add a YouTube button
var url_btn = document.getElementsByClassName('mbbc-url')[0];
if (url_btn !== undefined) {
var yt_btn = document.createElement('li');
yt_btn.className = 'mbbc-youtube';
yt_btn.addEventListener('click', function() {
var sp = bbetext.selectionStart;
var ep = bbetext.selectionEnd;
var sel;
if (ep < sp) {
sp = ep;
ep = bbetext.selectionStart;
}
if (ep !== sp) {
sel = bbetext.value.slice(sp, ep);
} else {
sel = prompt('Video URL:', '');
if (sel === null)
return;
var vid = sel
.match(new RegExp('[/?=#]([-\\w]{11})([^-\\w]|$)'));
if (vid !== null)
sel = 'https://www.youtube.com/watch?v=' + vid[1];
}
if (sel === undefined)
return;
bbetext.value = bbetext.value.slice(0, sp) +
'[youtube]' + sel + '[/youtube]' +
bbetext.value.slice(ep);
bbetext.selectionStart = bbetext.selectionEnd = sp + 9 +
(sel === '' ? 0 : 10 + sel.length);
bbetext.focus();
});
url_btn.parentElement.insertBefore(yt_btn, url_btn);
}
}
};
block.watch('late', function() {
// Persist the terms of use checkbox
var toubox = document.getElementById('AgreeTerms');
if (toubox !== null)
handlers.checkbox(toubox, 'agree_tou');
bbeeditor = document.getElementById('bbEditor');
if (bbeeditor !== null) {
bbetext = bbeeditor.getElementsByClassName('mbbc-editor')[0];
if (bbetext !== undefined)
fix_editor();
else
new MutationObserver(function(es, mo) {
mo.disconnect();
fix_editor();
}).observe(bbeeditor, { childList: true });
}
});
} else {
block.watch('ready', function() {
bbetext = document.getElementById('RawContent');
if (bbetext !== null) {
bbepanel = bbetext.parentElement.parentElement.parentElement;
bbeeditor = bbepanel.parentElement;
bbeexpand = bbepanel.previousElementSibling.firstElementChild
.firstElementChild;
}
});
}
}
if (ftl.thread && !ftl.perma) {
// Re-implement quotes in a way that sort of works
var fix_quote_btns = (function() {
function wrap_tag(tag, val, str) {
return '[' + tag + (val ? '=' + val : '') + ']' + str + '[/' + tag + ']';
}
function inner_quote(elm, range, depth) {
var tmp = '';
for (var e = elm.firstChild; e !== null; e = e.nextSibling) {
var sp, ep;
if (range !== undefined) {
if (range.startContainer.compareDocumentPosition(e) === 2)
continue;
if (range.endContainer.compareDocumentPosition(e) === 4)
break;
if (range.startContainer === e)
sp = range.startOffset;
if (range.endContainer === e)
ep = range.endOffset;
}
switch (e.tagName) {
case 'BR':
tmp += '\n';
break;
case 'STRONG':
tmp += wrap_tag('b', '', inner_quote(e, range, depth));
break;
case 'EM':
tmp += wrap_tag('i', '', inner_quote(e, range, depth));
break;
case 'HR':
tmp += '[hr]';
break;
case 'P':
if (e.innerHTML !== '')
tmp += '\n' + inner_quote(e, range, depth);
break;
case 'SPAN':
var s = e.style;
var t = null;
var v = '';
if (s.textDecoration === 'underline') {
t = 'u';
} else if (s.textDecoration === 'line-through') {
t = 's';
} else if (s.fontSize !== '') {
t = 'size';
v = parseInt(s.fontSize);
} else if (s.color !== '') {
t = 'color';
v = s.color;
} else if (s.textAlign !== '') {
t = 'align';
v = s.textAlign;
} else {
console.log('Unknown span!');
}
tmp += wrap_tag(t, v, inner_quote(e, range, depth));
break;
case 'DIV':
if (e.className === 'ForumQuote') {
if (tmp[tmp.length - 1] !== '\n')
tmp += '\n';
if (depth === 0 && settings.get('quote_quotes'))
tmp += wrap_tag('quote', '',
inner_quote(e.firstElementChild, range, depth + 1).trim());
else
tmp += '(snip)\n';
} else {
console.log('Unknown div!');
}
break;
case 'IFRAME':
var m = e.src.match(/youtube[^\/]*.com.*\/embed\/([^?]+)/);
if (m !== null && m[1] !== undefined) {
/* A YouTube tag inside a quote breaks the parsing of the quote.
tmp += wrap_tag('youtube', '',
'https://www.youtube.com/watch?v=' + m[1])
*/
tmp += wrap_tag('url',
'https://www.youtube.com/watch?v=' + m[1], 'YouTube video');
} else {
console.warn('Unknown iframe!');
}
break;
case 'IMG':
if (settings.get('quote_imgs')) {
tmp += wrap_tag('img', '', e.src);
} else if (e.parentElement.tagName !== 'A') {
tmp += wrap_tag('url', e.src, 'Image');
} else {
tmp += 'Image';
}
break;
case 'A':
if (e.className !== 'SKUAutoLink') {
tmp += wrap_tag('url', e.href, inner_quote(e, range, depth));
} else {
tmp += e.textContent;
}
break;
case undefined:
// Filter out newlines and empty tags
tmp += e.wholeText.slice(sp ? sp : 0, ep)
.replace(/[\r\n]+/mg, '')
.replace(/\[([^\]=]*)(=[^\]]*)?\]\[\/\1\]/mg, '');
break;
default:
console.warn('Unhandled tag ' + e.tagName);
}
}
return tmp;
}
function quote(user, elm) {
if (bbetext === null) {
console.error('bbEditor text box not found!');
return;
}
var post_id;
var pid = elm.id.match(/^POST([0-9]+)$/);
if (pid !== null)
post_id = pid[1];
var sel = getSelection();
if (sel.rangeCount > 0) {
sel = sel.getRangeAt(0);
if (sel.collapsed || !sel.intersectsNode(elm))
sel = undefined;
} else {
sel = undefined;
}
if (bbetext.value.length > 0 &&
bbetext.value[bbetext.value.length - 1] !== '\n')
bbetext.value += '\n';
if (thread_id !== undefined && post_id !== undefined)
bbetext.value += wrap_tag('url', '/forums/-/t/' + thread_id + '?' +
post_id, user + ' wrote') + ':\n';
else
bbetext.value += user + ' wrote:\n';
bbetext.value += wrap_tag('quote', '', inner_quote(elm, sel, 0)
.trim().replace(/[\r\n]{3,}/mg, '\n\n')) + '\n';
if (bbeeditor)
bbeeditor.scrollIntoView();
if (bbeexpand && bbepanel && bbepanel.className &&
!/\bin\b/.test(bbepanel.className))
expand.click();
}
return function(elms, func) {
for (var i = 0; i < elms.length; i++) {
var elm = elms[i];
var newbtn = func(elm.content_elm);
newbtn.addEventListener('click', (function(elm) {
return (function() { quote(elm.user, elm.content_elm) })})(elm));
}
};
})();
if (!ftl.mobile) {
var replace_quotebtn = function(elm) {
// Ugh.
var oldbtn = elm.parentElement.parentElement.parentElement
.previousElementSibling.firstElementChild.lastElementChild
.firstElementChild;
var newbtn = document.createElement('a');
newbtn.innerHTML = 'quote/reply';
newbtn.href = 'javascript:void(0)';
oldbtn.parentElement.insertBefore(newbtn, oldbtn);
oldbtn.remove();
return newbtn;
};
block.watch('late', function() {
fix_quote_btns(posts, replace_quotebtn);
});
} else { // mobile
// Fix up YT iframe size: CSS doesn't work well here.
var fr_resize = (function() {
var frs;
function handler() {
for (var i = 0; i < frs.length; i++) {
var fr = frs[i];
fr.elm.style.height = Math.ceil(fr.elm.clientWidth * fr.ar) + 'px';
}
}
return function(fr) {
if (frs === undefined) {
frs = [];
window.addEventListener('resize', handler);
}
var ar = fr.clientHeight / fr.clientWidth;
fr.style.maxWidth = fr.clientWidth + 'px';
fr.style.width = '100%';
fr.style.height = Math.ceil(fr.clientWidth * ar) + 'px';
frs.push({elm: fr, ar: ar});
};
})();
var replace_quotebtn_m = function(elm) {
// Ugh.
var oldbtn = elm.parentElement.firstElementChild.lastElementChild
.lastElementChild.firstElementChild;
var newbtn = document.createElement('a');
newbtn.innerHTML = oldbtn.innerHTML;
newbtn.href = 'javascript:void(0)';
newbtn.tabindex = '-1';
newbtn.role = 'menuitem';
oldbtn.parentElement.insertBefore(newbtn, oldbtn);
oldbtn.remove();
return newbtn;
};
block.watch('ready', function() {
var frs = document.getElementsByTagName('iframe');
for (var i = 0; i < frs.length; i++)
fr_resize(frs[i]);
});
block.watch('late', function() {
fix_quote_btns(posts, replace_quotebtn_m);
// Add a link to forum settings to allow ignorelist management
// Ugh. There's no semi-robust way to select this.
var li = document.querySelector('ul.nav:nth-child(1)>li:nth-child(3)' +
'>ul:nth-child(2)>li:nth-child(8)');
if (li !== null) {
var new_li = document.createElement('li');
new_li.innerHTML = '<a title="Forum Settings" ' +
'href="https://www.fasttech.com/forums/settings">' +
'Forum Settings</a>';
li.parentElement.insertBefore(new_li, li);
}
});
}
}
if (ftl.thread && !ftl.perma) {
var scroll_to_hash = function() {
function scroll(elm) {
if (elm) {
var sd = elm.style.display;
elm.style.display = '';
elm.scrollIntoView();
elm.style.display = sd;
}
}
if (location.hash === '#unread' || location.hash === '#last')
last_viewed.get(thread_id, function(hash, lvp, lpn, lp) {
block.watch('filter', function() {
var i;
var pid = 'POST' + lp;
for (i = posts.length - 1; i >= 0; i--) {
if (posts[i].content_elm.id === pid)
break;
}
if (i < posts.length - 1)
scroll(posts[i + 1].post_elms[0]);
else if (hash === '#last')
scroll(posts[posts.length - 1].post_elms[0]);
});
}, location.hash);
else if (location.hash !== '')
block.watch('filter', function() {
scroll(document.getElementById(location.hash));
});
// Drop the hash so we won't run again
history.replaceState(history.state, '', location.pathname);
};
}
if (ftl.threadlist) {
if (!ftl.mobile) {
var add_last_viewed =
function(elm, last_viewed_page, last_post_nr, last_post_id) {
var row = elm.parentElement;
var pelm = row.parentElement.getElementsByClassName('Pager')[0];
var p = parse_pager(pelm);
var skip_margin = false;
if (pelm === undefined) {
skip_margin = true;
if (!ftl.settings) {
pelm = row.lastElementChild;
pelm.style.display = '';
pelm = pelm.appendChild(document.createElement('div'));
pelm.className = 'Pager';
} else {
pelm = row.parentElement.appendChild(document.createElement('div'));
pelm.className = 'Pager';
}
}
if (settings.get('track_viewed')) {
var lv = document.createElement('a');
lv.href = gen_page_link(elm.href, last_viewed_page);
lv.innerHTML = 'Last viewed';
lv.className = 'FormButton blue';
lv.style.color = '#fff';
if (!skip_margin)
lv.style.marginLeft = '5px';
pelm.appendChild(lv);
skip_margin = false;
}
var tr = row.parentElement.parentElement;
var ts = tr.getElementsByClassName('ThreadStats')[0];
var pc;
if (ts !== undefined)
pc = (pc = ts.innerHTML.match(/Replies: ([0-9]+)/)) ?
parseInt(pc[1]) : undefined;
var unread = pc > 0 && pc >= last_post_nr;
if ((unread || settings.get('always_unread')) &&
settings.get('track_posts')) {
var np = document.createElement('a');
np.style.marginLeft = '5px';
if (unread) {
np.innerHTML = 'Unread posts';
np.className = 'FormButton red';
np.style.color = '#fff';
np.href = gen_page_link(elm.href,
postnr_to_pagenr(last_post_nr, p.last), 'unread');
} else {
np.innerHTML = 'No unread posts';
np.className = 'FormButton white';
np.href = gen_page_link(elm.href,
postnr_to_pagenr(last_post_nr, p.last), 'last');
}
if (!skip_margin)
np.style.marginLeft = '5px';
pelm.appendChild(np);
skip_margin = false;
}
};
var mangle_list = function() {
var header = document.getElementsByClassName('ForumHeader')[0];
var filter = !ftl.settings && !ftl.author && settings.get('hide_threads');
var sku_re = new RegExp('fasttechcdn.com/[0-9]+/([0-9]+)/.*');
if (header !== undefined) {
var ch = header.parentElement.children;
for (var i = 1; i < ch.length; i += 2) {
var tr = ch[i];
var tl = tr.getElementsByClassName('ThreadLink')[0];
var tid;
if (tl !== undefined &&
(settings.get('track_posts') || settings.get('track_viewed'))) {
tid = parseInt(tl.href.match(thread_id_re)[1]);
if (!isNaN(tid))
last_viewed.get(tid, add_last_viewed, tl);
}
var img = tr.firstElementChild.firstElementChild;
if (img && img.tagName === 'IMG') {
var sku = img.src.match(sku_re);
if (sku !== null) {
var p = img.parentElement;
var a = document.createElement('a');
a.href = '/products/' + sku[1];
a.appendChild(img);
p.appendChild(a);
}
}
if (filter) {
var u = tl.parentElement.textContent
.match(/started\W+by\W+(\w+)/);
if (u && u[1] && ignore_list.get(u[1])) {
tr.style.display = 'none';
if (tr.previousElementSibling !== header)
tr.previousElementSibling.style.display = 'none';
}
}
}
}
};
block.watch('ready', mangle_list);
} else { // mobile
var add_last_viewed =
function(elm, last_viewed_page, last_post_nr, last_post_id) {
var rh = elm.parentElement;
var tr = rh.parentElement;
var p = parse_pager(tr.getElementsByClassName('pagination')[0]);
var d = document.createElement('div');
d.style.marginLeft = '-5px';
if (settings.get('track_viewed')) {
var lv = document.createElement('span');
lv.innerHTML = '<a href="' +
gen_page_link(elm.href, last_viewed_page) + '">Last viewed</a>';
lv.className = 'btn btn-default';
lv.style.margin = '5px';
d.appendChild(lv);
}
var pc = (pc = tr.textContent.match(/R(eplies)?: ([0-9]+)/)) ?
parseInt(pc[2]) : undefined;
var unread = pc > 0 && pc >= last_post_nr;
if ((unread || settings.get('always_unread')) &&
settings.get('track_posts')) {
var np = document.createElement('span');
np.className = 'btn btn-default';
np.style.margin = '5px';
var npa = document.createElement('a');
if (unread) {
npa.innerHTML = 'Unread posts';
npa.href = gen_page_link(elm.href,
postnr_to_pagenr(last_post_nr, p.last), 'unread');
npa.style.color = '#C64148';
} else {
npa.innerHTML = 'No unread posts';
npa.style.color = '#888';
np.href = gen_page_link(elm.href,
postnr_to_pagenr(last_post_nr, p.last), 'last');
}
np.appendChild(npa);
d.appendChild(np);
}
rh.appendChild(d);
};
var mangle_list = function() {
var filter = settings.get('hide_threads');
var elms = document.getElementsByClassName('ListRow');
for (i = 0; i < elms.length; i++) {
var elm = elms[i];
if (elm === undefined)
continue;
var rh = elm.getElementsByClassName('RowHeading')[0];
if (rh === undefined)
continue;
var l = rh.firstElementChild;
if (l !== undefined &&
(settings.get('track_posts') || settings.get('track_viewed'))) {
var tid = parseInt(l.href.match(thread_id_re)[1]);
if (!isNaN(tid))
last_viewed.get(tid, add_last_viewed, l);
}
if (filter) {
var u = tl.parentElement.textContent
.match(/started\W+by\W+(\w+)/);
if (u && u[1] && ignore_list.get(u[1]))
elm.style.display = 'none';
}
}
};
block.watch('ready', function() {
if (ftl.product) {
// Wait for thread list to load before mangling
var div = document.getElementById('forum');
if (div !== null)
new MutationObserver(function(es, mo) {
mo.disconnect();
mangle_list();
}).observe(div, { childList: true });
} else {
mangle_list();
}
});
}
}
if (!ftl.mobile && ftl.thread) {
// Re-implement FT's PopoutMenu, only nicer
var PM2 = (function() {
var saved_class, saved_par, saved_ch, timeout;
function add_listener(elem, type, val, par, ch) {
elem.addEventListener(type, function() { act(val, par, ch) });
}
function add_listeners(elem, par, ch) {
add_listener(elem, 'mouseover', 1, par, ch);
add_listener(elem, 'mouseout', 0, par, ch);
}
function show() {
saved_par.className += ' focused';
saved_ch.style.display = 'inline';
// Hack: this is the easiest way to fix up the element locations.
saved_ch.style.left = saved_ch.style.top = '0px';
var pb = saved_par.getBoundingClientRect();
var cb = saved_ch.getBoundingClientRect();
var pad = parseInt(saved_ch.style.padding);
pad = pad ? pad : 0;
saved_ch.style.left = Math.min(pb.left - cb.left,
document.body.clientWidth - cb.width + pad) + 'px';
saved_ch.style.top = (pb.top - cb.top + 35) + 'px';
saved_ch.style.width = (cb.width - pad * 2) + 'px';
}
function hide() {
if (!saved_class) return;
saved_par.className = saved_class;
saved_ch.style.display = 'none';
saved_class = undefined;
}
function act(val, par, ch) {
if (val) {
clearTimeout(timeout);
if (saved_par != par)
hide();
saved_par = par;
saved_ch = ch;
if (saved_class === undefined) {
saved_class = par.className;
show();
}
} else {
timeout = setTimeout(function() { hide() }, 300);
}
}
// Hack: we can't remove anonymous event listeners, so duplicate elements
function dupe(id, raw) {
var elm = document.getElementById(id);
var nelm = document.createElement(elm.tagName);
nelm.align = elm.align;
if (raw) {
nelm.innerHTML = elm.innerHTML;
} else {
nelm.setAttribute('style', 'position: absolute; height: auto; ' +
'background-color: white; padding: 20px');
while (elm.childNodes[0])
nelm.appendChild(elm.childNodes[0]);
}
nelm.id = elm.id + '2';
nelm.className = elm.className;
elm.parentElement.insertBefore(nelm, elm);
elm.remove();
return nelm;
}
return function(par) {
var ch = dupe(par + 'Popout', false);
par = dupe(par, true);
add_listeners(par, par, ch);
add_listeners(ch, par, ch);
}
})();
var have_itmenu = false;
var have_itmanage = false;
var update_itmanage = function(n, itm) {
if (!have_itmenu)
return;
var div = document.getElementById('IgnorelistPopout2');
if (div === null)
return;
if (!have_itmanage && itm) {
var frag = document.createDocumentFragment();
var hr = document.createElement('hr');
hr.className = 'itmanage';
frag.appendChild(hr);
var l = document.createElement('div');
l.innerHTML = '<b>Ignored Users</b>';
l.className = 'itmanage';
l.style.marginTop = '10px';
manage_menu.register(l);
l.getElementsByClassName('il_addbtn')[0].className = 'FormButton blue';
frag.appendChild(l);
div.appendChild(frag);
have_itmanage = true;
} else if (have_itmanage) {
var elms = div.getElementsByClassName('itmanage');
for (var i = 0; i < elms.length; i++)
elms[i].style.display = itm ? '' : 'none';
}
};
var update_itmenu = function(n, itm) {
if (!have_itmenu && itm) {
var b = document.getElementsByClassName('ThreadCommandBar')[0];
var sp = document.createElement('span');
sp.id = 'Ignorelist';
sp.className = 'ThreadCommand';
sp.innerHTML = 'Settings';
b.appendChild(sp);
block.watch('late', function() {
var div = document.createElement('div');
div.id = 'IgnorelistPopout';
div.className = 'PopoutPanel';
div.align = 'left';
settings_menu.register(div, thread_menu);
b.appendChild(div);
PM2('Ignorelist');
PM2('RateThread');
PM2('ForumTools');
have_itmenu = true;
if (settings.get('inthread_manage') !== undefined)
update_itmanage(undefined, settings.get('inthread_manage'));
});
} else if (have_itmenu) {
var elm = document.getElementById('Ignorelist2');
if (elm !== null)
elm.style.display = itm ? '' : 'none';
}
};
post_filter.watch(gen_watch_filter(function(post) {
var tbl = post.post_elms[0].parentElement;
var ph = post.placeholder = document.createElement('tr');
ph.innerHTML = '<td colspan="2" class="ForumHeader" ' +
'style="padding: 10px; text-align: center"><a>Post by ' +
strip_tags(post.user) + ' hidden. Click to show.</a></td>';
ph.firstElementChild.firstElementChild.href = 'javascript:void(0)';
handlers.hide_toggle(ph.firstElementChild.firstElementChild, post);
tbl.insertBefore(ph, post.post_elms[0]);
return ph;
}));
settings.watch('placeholders', post_filter.update);
block.watch('ready', function() {
// Wire posts & ignorebuttons up
ignore_buttons.set('+ ignore', '+ unignore');
for (var i = 0; i < posts.length; i++) {
var post = posts[i];
var tl = post.content_elm.parentElement.parentElement.parentElement;
post.post_elms = [tl.previousElementSibling, tl];
var head = tl.previousElementSibling.firstElementChild.lastElementChild;
var new_a = document.createElement('a');
new_a.href = 'javascript:void(0)';
head.appendChild(new_a);
post.ignbtn = new_a;
post.badge = tl.getElementsByClassName('Badges')[0];
}
post_filter.register(posts);
ignore_buttons.register(posts);
user_tags.register(posts);
settings.watch('inthread_manage', update_itmanage);
settings.watch('inthread_menu', update_itmenu);
});
block.watch('late', function() {
var p = parse_pager(document.getElementsByClassName('Pager')[1]);
var lpn = (p.cur - 1) * 10 + posts.length;
var lp = parseInt(posts[posts.length - 1].content_elm.id.slice(4));
last_viewed.set(thread_id, p.cur, lpn, lp);
});
if (!ftl.perma && thread_id !== undefined) {
if (location.hash !== '')
scroll_to_hash();
}
}
if (ftl.mobile && ftl.thread) {
post_filter.watch(gen_watch_filter(function(post) {
var tbl = post.post_elms[0].parentElement;
var ph = post.placeholder = document.createElement('div');
ph.className = 'col-xs-12 ListRow';
ph.style.paddingBottom = '5px';
ph.style.textAlign = 'center';
ph.innerHTML = '<a>Post by ' + strip_tags(post.user) +
' hidden. Click to show.</a>';
ph.firstElementChild.href = 'javascript:void(0)';
handlers.hide_toggle(ph.firstElementChild, post);
tbl.insertBefore(ph, post.post_elms[0]);
return ph;
}));
settings.watch('placeholders', post_filter.update);
ignore_buttons.set(' Ignore', ' Unignore');
var have_itmenu = false;
var have_itmanage = false;
var update_itmanage = function(n, itm) {
if (!have_itmenu)
return;
var menu = document.getElementById('hideSettings');
if (menu === null)
return;
if (!have_itmanage && itm) {
var frag = document.createDocumentFragment();
var inner = menu.getElementsByClassName('panel-body')[0];
var hr = document.createElement('hr');
hr.className = 'itmanage';
frag.appendChild(hr);
var l = document.createElement('div');
l.className = 'itmanage';
l.innerHTML = '<b>Ignored Users</b>';
l.style.marginTop = '10px';
manage_menu.register(l);
l.getElementsByClassName('il_addbtn')[0].remove();
frag.appendChild(l);
inner.appendChild(frag);
have_itmanage = true;
} else if (have_itmanage) {
var elms = menu.getElementsByClassName('itmanage');
for (var i = 0; i < elms.length; i++)
elms[i].style.display = itm ? '' : 'none';
}
};
var update_itmenu = function(n, itm) {
if (!have_itmenu && itm) {
var div = document.createElement('div');
div.id = 'itmenu';
div.className = 'panel panel-default';
div.innerHTML = '<div class="panel-heading"><h5 class="panel-title">' +
'<a href="#hideSettings" data-toggle="collapse">Settings ' +
'<span class="caret"></span></a></h5>' +
'<div id="hideSettings" class="panel-collapse collapse">' +
'<div class="panel-body"></div></div>';
block.watch('late', function() {
var inner = div.getElementsByClassName('panel-body')[0];
settings_menu.register(inner, thread_menu);
document.getElementsByClassName('body-content')[0].appendChild(div);
have_itmenu = true;
if (settings.get('inthread_manage') !== undefined)
update_itmanage(undefined, settings.get('inthread_manage'));
});
} else if (have_itmenu) {
document.getElementById('itmenu').style.display = itm ? '' : 'none';
}
};
block.watch('ready', function() {
for (var i = 0; i < posts.length; i++) {
var post = posts[i];
var pe = post.content_elm.parentElement;
post.post_elms = [pe];
var menu = pe.firstElementChild.lastElementChild;
var li = document.createElement('li');
li.setAttribute('role', 'presentation');
li.innerHTML = '<a tabindex="-1" role="menuitem">' +
'<span class="glyphicon glyphicon-plus red"></span><span></span></a>';
li.firstElementChild.href = 'javascript:void(0)';
var rp = menu.lastElementChild;
if (!rp.firstElementChild.lastChild.data.startsWith(' Report'))
rp = rp.previousElementSibling;
menu.insertBefore(li, rp);
post.ignbtn = li.firstElementChild.lastElementChild;
var badge = menu.firstElementChild.firstElementChild;
var m = badge.innerHTML.match(/(.*)\((.*)\)/);
badge.innerHTML = m[1] + '(<span>' + m[2] + '</span>)';
post.badge = badge.lastElementChild;
}
post_filter.register(posts);
ignore_buttons.register(posts);
user_tags.register(posts);
settings.watch('inthread_manage', update_itmanage);
settings.watch('inthread_menu', update_itmenu);
});
block.watch('late', function() {
var p = parse_pager(document.getElementsByClassName('pagination')[0]);
var lpn = (p.cur - 1) * 10 + posts.length;
var lp = parseInt(posts[posts.length - 1].content_elm.id.slice(4));
last_viewed.set(thread_id, p.cur, lpn, lp);
});
if (!ftl.perma && thread_id !== undefined) {
if (location.hash !== '')
scroll_to_hash();
}
}
if (ftl.settings) {
block.watch('ready', function() {
var panel = document.getElementsByClassName('PageContentPanel')[0];
var div = document.createElement('div');
div.style.marginTop = '5px';
var tbl = document.createElement('table');
tbl.style.width = 'auto';
tbl.innerHTML = '<tbody><tr><td></td><td width="15"></td><td></td></tr>' +
'<tr><td></td><td></td><td></td></tr></tbody>';
div.appendChild(tbl);
var cells = tbl.getElementsByTagName('td');
cells[0].className = 'MediumLabel Bold EndOfInlineSection';
cells[0].style.padding = '5px';
cells[0].innerHTML = 'Ignored Users';
cells[2].className = 'MediumLabel Bold EndOfInlineSection';
cells[2].style.padding = '5px';
cells[2].innerHTML = 'FastTech Forum Enhancements Settings';
cells[5].style.padding = '0px';
var menu = document.createElement('div');
menu.className = 'BGShadow';
menu.style.padding = '10px';
settings_menu.register(menu, full_menus);
cells[5].appendChild(menu);
cells[3].style.padding = '0px';
menu = document.createElement('div');
menu.className = 'BGShadow';
menu.style.padding = '10px';
manage_menu.register(menu);
menu.getElementsByClassName('il_addbtn')[0].className = 'FormButton blue';
cells[3].appendChild(menu);
panel.appendChild(div);
});
}