// ==UserScript==
// @name Fixdit for Reddit Redesign
// @namespace http://tampermonkey.net/
// @version 0.6.8
// @description UX enhancements for Reddit's 2018 redesign: filters, collapse child comments, subreddit info...
// @author scriptpost (u/postpics)
// @match https://www.reddit.com/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @noframes
// ==/UserScript==
(function ($, undefined) {
$(function () {
if (!$('#hamburgers').length) return; // probably using old site.
const _stylesheet = String.raw`<style type="text/css" id="fixdit">.fixd_filter_msg button:hover,button.fixd_collapse:hover,button.fixd_collapse_all:hover{text-decoration:underline}#fixd_launch{box-sizing:border-box;position:fixed;top:2px;right:2px;width:24px;height:24px;z-index:100;text-align:center;border-radius:50%;border:2px solid hsla(70,0%,100%,.5);border-left-color:hsla(70,0%,0%,1);border-right-color:hsla(70,0%,0%,1);background:0 0;cursor:pointer;-moz-user-select:none;user-select:none}#fixd_launch.fixd_active{border-color:hsla(70,0%,100%,.5);border-top-color:hsla(70,0%,0%,1);border-bottom-color:hsla(70,0%,0%,1)}#fixd_launch:not(.fixd_active):hover:before{content:"Fixdit settings...";display:inline-block;padding:6px 12px;font-size:80%;color:#fff;position:absolute;right:calc(100% + 12px);white-space:nowrap;background:hsla(0,0%,0%,.8);border-radius:2px;pointer-events:none}#fixd_settings{position:fixed;top:0;right:0;opacity:0;z-index:101;padding:24px 12px;background:hsla(180,2%,90%,.95);border-radius:3px;box-shadow:0 5px 10px hsla(0,0%,0%,.25),0 0 3px hsla(0,0%,0%,.25);font-size:80%;width:300px;visibility:hidden;overflow:hidden;transition:visibility .2s,height .2s,bottom .2s,left .2s,top .2s,right .2s,opacity .2s}#fixd_settings.fixd_active{visibility:visible;opacity:1;top:5px;right:24px}#fixd_settings.fixd_expanded{bottom:5px}#fixd_settings .fixd_dialog,#fixd_settings .fixd_dialog>div{position:absolute;top:0;left:0;bottom:0;border-radius:4px;right:0}body.fixd_debug #fixd_settings:after{display:block;margin-top:4px;content:"Version " attr(data-version);text-align:right;font-size:12px;color:#7f7f7f}.fixd_description{margin:12px 0 0;line-height:1.3}#fixd_settings h2,#fixd_settings h3{font-weight:400;display:inline-block;margin-bottom:10px;vertical-align:middle}#fixd_settings h1{font-size:140%;font-weight:400;margin:0 0 .5em}#fixd_settings h2{font-size:120%}#fixd_settings h3{font-size:100%}#fixd_settings h3 span{display:block;margin-top:4px;font-size:120%}#fixd_settings .fixd_option,#fixd_settings .fixd_option_btn,#fixd_settings .fixd_option_select,#fixd_settings .fixd_switch{display:block;margin:1px -12px 0;cursor:default;-moz-user-select:none;user-select:none;background:#f3f0f0}#fixd_settings .fixd_option_select input,#fixd_settings .fixd_switch input{vertical-align:middle}#fixd_settings .fixd_option_btn{margin:1px -12px;cursor:default}#fixd_settings .fixd_option_btn:after{content:" >";color:#999;font-weight:700;float:right}#fixd_settings .fixd_option_btn:hover:after{color:#000}#fixd_settings .fixd_option_btn:hover,#fixd_settings .fixd_setting:hover{outline:hsla(0,0%,0%,.3) solid 1px}#fixd_settings .fixd_option_btn,#fixd_settings .fixd_option_select,#fixd_settings .fixd_switch{padding:10px 16px}#fixd_settings .fixd_enabled{background:#fff}#fixd_settings .fixd_switch:not(.fixd_option){font-size:120%;margin-bottom:8px;background:#f3f0f0}#fixd_settings .fixd_switch.fixd_enabled:not(.fixd_option){color:#000;background:#fff}#fixd_settings .fixd_option span{padding-left:.3em}#fixd_settings .fixd_dialog>div{padding:24px 12px 8px;background:#e4e6e6;display:flex;flex-direction:column}.fixd_dialog textarea{box-sizing:border-box;min-width:100%;max-width:100%;min-height:50px;max-height:100%;border:1px solid #fff;flex:1}.fixd_settings_buttons{text-align:right;margin:6px 0}.fixd_btn_back,.fixd_btn_save{text-transform:uppercase;border:1px solid transparent;border-radius:2px;vertical-align:middle}.fixd_btn_back{font-weight:700;color:transparent;margin-right:12px;padding:0;overflow:hidden;width:30px;height:30px;line-height:30px;margin-bottom:10px;border:1px solid #ccc;background:0 0}.fixd_btn_back:hover{border:1px solid #666}.fixd_btn_back:before{color:#000;content:"< "}.fixd_btn_save{color:#fff;padding:8px 24px;background:#0076b2;cursor:pointer}.fixd_btn_save:hover{background-color:#0087cc}button.fixd_collapse,button.fixd_collapse_all{cursor:pointer;color:inherit;display:inline-block;background:0 0;outline:0;font-size:12px;font-weight:700}a+button.fixd_collapse{margin-left:10px}button.fixd_collapse_all{margin-left:20px;color:#a6a4a4;font-size:12px;font-weight:700;text-transform:uppercase}button.fixd_collapse:before,button.fixd_collapse_all:before{content:"Hide "}button.fixd_collapse:after,button.fixd_collapse_all:after{content:" <<"}button.fixd_collapse.fixd_active:before,button.fixd_collapse_all.fixd_active:before{content:"Show "}button.fixd_collapse.fixd_active:after,button.fixd_collapse_all.fixd_active:after{content:" >"}#fixd_indicators{position:fixed;top:40px;right:0;z-index:100}.fixd_observer_indicator{box-sizing:border-box;margin:5px 0 0;width:20px;height:15px;border:1px solid hsla(0,0%,0%,.2);background:hsla(0,0%,50%,.2);transition:all .3s}.fixd_observer_indicator.fixd_active{border-color:hsla(0,0%,0%,1);background:hsla(0,0%,100%,1)}.fixd_observer_indicator.fixd_active:before,.fixd_observer_indicator:hover:before{content:attr(data-fixd-name);display:block;position:absolute;margin-top:-1px;right:100%;white-space:nowrap;color:#fff;font-size:11px;padding:3px 6px;background:hsla(0,0%,0%,.4);pointer-events:none}.fixd_observer_indicator:hover:before{background:hsla(0,0%,0%,.8)}.fixd_popup{box-sizing:border-box;position:absolute;z-index:100;padding:12px;font-size:12px;border-radius:4px;border-top:4px solid #c1cfd6;color:#1c1c1c;background-color:#fff;box-shadow:rgba(0,0,0,.2) 0 1px 3px;overflow:hidden}#fixd_popup_subreddit.fixd_subscriber{border-top-color:#0076d1}.fixd_popup>div{float:left}.fixd_popup>div:nth-of-type(2){width:200px;margin-left:12px;padding-left:12px;border-left:1px solid #edeff1}.fixd_popup .fixd_popup_subs span,.fixd_popup h2{display:block;font-size:16px;font-weight:500;line-height:20px}.fixd_filtered .Comment,.fixd_no_blank .icon-outboundLink,.fixd_popup:not(.fixd_filterable) .fixd_popup_filter,.fixd_unfiltered .fixd_filter_msg{display:none}.fixd_popup .fixd_popup_created,.fixd_popup .fixd_popup_subs{font-weight:500}.fixd_popup .fixd_popup_subs{margin-top:12px}.fixd_popup h2:before{content:"r/"}.fixd_popup .fixd_popup_subtitle{margin-bottom:.5em;color:#7f7f7f}.fixd_popup .fixd_popup_desc{color:#7f7f7f;line-height:1.2}.fixd_popup .fixd_popup_desc,.fixd_popup .fixd_popup_title{margin:0 0 .5em}.fixd_popup_filter{font-size:12px;text-transform:uppercase;padding:8px 12px;margin-top:12px;border-radius:2px;color:#fff;border:1px solid transparent;background:#0076d1;cursor:pointer}.fixd_popup_filter.fixd_active{color:#0076d1;border:1px solid;background:#fff}.fixd_popup_filter.fixd_active:before{content:"un"}.fixd_tooltip{pointer-events:none}body:not(.fixd_debug) .fixd_hidden{visibility:hidden;position:absolute}body.fixd_debug .fixd_hidden{opacity:.6}body.fixd_debug .fixd_filtered{outline:#dc143c solid 1px}body.fixd_debug .fixd_unfiltered{outline:green solid 1px}.fixd_filter_msg{font-size:12px;color:#878a8c}.fixd_filter_msg em{font-style:italic}.fixd_filter_msg button{content:"Show comment";color:inherit;background:0 0;cursor:pointer;margin:12px 0 0 12px}button.fixd_no_icon.fixd_active[data-click-id=upvote]{color:#f40!important}button.fixd_no_icon.fixd_active[data-click-id=downvote]{color:#7091ff!important}button.fixd_no_icon[data-click-id=upvote]:hover{color:#cc3600}button.fixd_no_icon[data-click-id=downvote]:hover{color:#5b75cc}button.fixd_no_icon[data-click-id=upvote]:before,button.fixd_no_icon[data-click-id=downvote]:before{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:redesignFont}button.fixd_no_icon[data-click-id=upvote]:before{content:"\F12A"}button.fixd_no_icon[data-click-id=downvote]:before{content:"\F107"}body.fixd_reduce_comment_spacing .Comment.top-level{margin-top:0}body.fixd_debug .fixd_no_blank{outline:#00f solid 1px}</style>`;
$('body').append(_stylesheet);
// TODO:
// Add notification each time settings are changed.
// Add observer to hidden comments.
// Add option to change the color of visited links.
// Observer for switching from comment permalink to all comments.
// Find more observers to add.
// strip_prefixes doesn't fire when switching view or page location.
// Firefox adds horiz scrollbar to window when static header enabled.
// CHANGELOG (latest):
// Prevent settings from flashing on page load.
// Fix issues on user profile page.
// Fix subreddit info popup not disappearing sometimes.
// Optimize overall efficiency.
// Improved overall consistency and reliability.
// Remove ability to disable internal/required features.
// Remove ability to toggle options of type 'radio'.
// Oserver for switching view.
class Feature {
constructor(data) {
this.loaded = JSON.parse(GM_getValue('features', '{}'));
if (this.loaded.hasOwnProperty(data.id)) {
data.enabled = this.loaded[data.id].enabled;
}
else {
const db_entry = this.loaded;
db_entry[data.id] = {
enabled: data.enabled,
options: {}
};
GM_setValue('features', JSON.stringify(db_entry));
this.enabled = data.enabled;
}
this.feature = data;
this.setting = Feature.add_setting(data);
this.options = {};
this.public = {};
}
static add_setting(data) {
return new Setting(data);
}
add_option(option) {
const loaded_options = this.loaded[this.feature.id].options;
let is_stored = loaded_options.hasOwnProperty(option.id);
const is_choices = option.hasOwnProperty('choices');
// Check if outdated values are in storage and reset to new values.
// TBDL: Remove this code after June 10.
if (is_stored && is_choices) {
if (option.type === 'radio') {
const is_outdated = isNaN(loaded_options[option.id].value);
if (is_outdated) {
is_stored = false;
}
}
else if (option.type === 'checkbox') {
const is_oudated = isNaN(loaded_options[option.id].value[0]);
if (is_oudated) {
is_stored = false;
}
}
}
// Remove 'enabled' from storage if it's deprecated.
// TBDL: Remove this code after June 10.
if (is_stored && !option.hasOwnProperty('enabled') &&
loaded_options[option.id].hasOwnProperty('enabled')) {
let db_entry = this.loaded;
delete db_entry[this.feature.id].options[option.id].enabled;
delete loaded_options[option.id].enabled;
GM_setValue('features', JSON.stringify(db_entry));
}
if (is_stored) {
// Modify passed object.
if (option.hasOwnProperty('enabled')) {
option.enabled = loaded_options[option.id].enabled;
}
if (loaded_options[option.id].hasOwnProperty('value')) {
option.value = loaded_options[option.id].value;
}
}
else {
// Continue without modifying.
const db_entry = this.loaded;
db_entry[this.feature.id].options[option.id] = {};
if (option.hasOwnProperty('value')) {
db_entry[this.feature.id].options[option.id].value = option.value;
}
db_entry[this.feature.id].options[option.id].enabled = option.enabled;
GM_setValue('features', JSON.stringify(db_entry));
this.loaded = db_entry;
}
this.options[option.id] = new Option(option);
}
set toggle(fn) {
this.setting.toggle = fn;
}
// arg: object
set options_toggle(functions) {
this.options_triggers = functions;
}
redraw() {
// Finds all the changed options and updates their appearance.
// Also calls any provided callback functions.
this.setting.redraw();
for (var oid in this.options) {
const new_val = this.loaded.options[oid].enabled;
const current_val = this.options[oid].enabled;
if (new_val !== current_val) {
const fid = this.feature.id;
this.options[oid].enabled = new_val;
const panel = '.fixd_options[data-id="' + fid + '"]';
const dialog = '.fixd_dialog[data-id="' + oid + '"]';
const $btn = $(panel + ' .fixd_option[data-id="' + oid + '"]');
const $switch = $(dialog + ' .fixd_switch[data-id="' + oid + '"]');
$btn.add($switch).each((idx, el) => {
if (el.classList.contains('fixd_enabled')) {
el.classList.remove('fixd_enabled');
}
else {
el.classList.add('fixd_enabled');
}
});
if (this.options_triggers &&
this.options_triggers.hasOwnProperty(oid) &&
this.setting.enabled) {
this.options_triggers[oid](new_val);
}
}
}
}
set update(data) {
this.setting.enabled = data.enabled;
this.loaded = data;
this.redraw();
}
}
class Setting {
constructor(data) {
for (let key in data) {
this[key] = data[key];
}
this.button = $('<div>', {
"text": data.label,
"class": "fixd_option_btn",
"data-click-id": "fixd_setting",
"data-id": data.id
})[0];
if (data.enabled) {
$(this.button).addClass('fixd_enabled');
}
if (!data.hidden) {
$('#fixd_settings .fixd_panel:not(.fixd_options)').append(this.button);
}
}
redraw() {
const el = $('.fixd_options .fixd_switch:not(.fixd_option)[data-id="' + this.id + '"]')[0];
if (this.enabled) {
$(el).add(this.button).addClass('fixd_enabled');
}
else {
$(el).add(this.button).removeClass('fixd_enabled');
}
if (this.toggle) {
this.toggle(this.enabled);
}
}
}
class Option {
constructor(data) {
for (let key in data) {
this[key] = data[key];
}
}
}
/**
* Creates an observer wrapper that can be extended from anywhere.
* Each time an RO is extended, it disconnects the last observer
* and adds a new one in its place with all the added functions.
* When there's a mutation and a condition is met, it loops over
* all the saved functions (from this.actions).
*/
class Reddit_Observer {
constructor(info) {
this.name = info.name;
this.target = info.target;
this.options = { childList: true };
if (info.subtree) {
this.options.subtree = true;
}
this.actions = [];
this.connect(info.target);
}
set any(callback) {
this.any_basis = callback;
}
set added(callback) {
this.added_basis = callback;
}
set removed(callback) {
this.removed_basis = callback;
}
get records() {
if (this.observer) {
return this.observer.takeRecords();
}
}
loop_actions(a, b) {
for (var i = 0; i < this.actions.length; i++) {
this.actions[i](this, a, b);
}
}
extend(fn) {
this.actions.push(fn);
if (this.target) {
this.connect();
}
}
connect(newTarget) {
if (newTarget) {
this.target = newTarget;
}
else if (!this.target) {
return console.warn("Can't connect observer because target node is undefined");
}
const self = this;
const mutation = function (mutations) {
for (let i = 0; i < mutations.length; i++) {
const a = mutations[i].addedNodes;
const r = mutations[i].removedNodes;
if (self.added_basis && a.length && a[0].nodeType === Node.ELEMENT_NODE) {
self.added_basis(self, a[0], mutations[i]);
}
else if (self.removed_basis && r.length && r[0].nodeType === Node.ELEMENT_NODE) {
self.removed_basis(self, r[0], mutations[i]);
}
else if (self.any_basis) {
self.any_basis(self, mutations[i]);
}
}
};
if (this.observer) {
this.observer.disconnect();
}
this.observer = new MutationObserver(mutation);
this.observer.observe(this.target, this.options);
}
}
// Set up pre-defined Observers:
const Body_Obs = new Reddit_Observer({
name: 'Body',
target: document.body
});
const Page_Subtree_Obs = new Reddit_Observer({
name: 'Page subtree',
target: $('#hamburgers + div')[0],
subtree: true
});
const Post_Obs = new Reddit_Observer({
name: 'Post',
target: (() => {
const p = document.getElementsByClassName('Post');
if (p.length) {
return p[0].parentNode.parentNode.parentNode;
}
})()
});
const Comment_Obs = new Reddit_Observer({
name: 'Comment',
target: (() => {
const c = document.getElementsByClassName('Comment');
if (c.length) {
return c[0].parentNode.parentNode.parentNode.parentNode;
}
})()
});
const Lb_Obs = new Reddit_Observer({
name: 'L-box',
target: document.getElementById('hamburgers').parentNode.parentNode
});
const Lb_Subtree_Obs = new Reddit_Observer({
name: 'L-box sub',
target: document.getElementById('lightbox'),
subtree: true
});
const Lb_TT_Obs = new Reddit_Observer({
name: 'L-box tooltip',
target: document.getElementById('overlayAbsoluteTooltipContent')
})
const Side_Obs = new Reddit_Observer({
name: 'Side',
target: document.getElementsByClassName('fixd--side')[0]
});
// Handle each mutation:
const added_to_body = (self, node, mutation) => {
self.loop_actions(node);
};
Body_Obs.added = added_to_body;
const added_to_page_subtree = (self, node, mutation) => {
const posts = node.getElementsByClassName('Post');
const lb = document.getElementById('lightbox');
if (posts.length) {
const list = posts[0].parentNode.parentNode.parentNode;
if (list === node || list.parentNode === node || node.parentNode === self.target) {
Post_Obs.connect(list);
Side_Obs.connect(document.getElementsByClassName('fixd--side')[0]);
self.loop_actions(node);
}
}
else if (node.classList.contains('Post')) {
self.loop_actions(node);
}
};
Page_Subtree_Obs.added = added_to_page_subtree;
const added_to_post_list = (self, node, mutation) => {
if (node.getElementsByClassName('Post')[0]) {
self.loop_actions(node);
}
};
Post_Obs.added = added_to_post_list;
const added_to_comment_list = (self, node, mutation) => {
if (node.getElementsByClassName('Comment')[0]) {
self.loop_actions(node, mutation);
}
};
Comment_Obs.added = added_to_comment_list;
const add_remove_lightbox = (self, mutation) => {
if (mutation.addedNodes.length) {
const lb = document.getElementById('lightbox');
Lb_Subtree_Obs.connect(lb);
const comments = lb.getElementsByClassName('Comment');
if (comments.length) {
// Loaded from memory.
const list = comments[0].parentNode.parentNode.parentNode.parentNode;
Comment_Obs.connect(list);
self.loop_actions(list, mutation);
}
Side_Obs.connect(lb.childNodes[0].childNodes[1]);
Lb_TT_Obs.connect(document.getElementById('overlayAbsoluteTooltipContent'));
} else {
Lb_Obs.connect();
}
};
Lb_Obs.any = add_remove_lightbox;
const added_to_lightbox = (self, node, mutation) => {
const comments = node.getElementsByClassName('Comment');
if (comments.length) {
const list = comments[0].parentNode.parentNode.parentNode.parentNode;
if (list.parentNode === node) {
Comment_Obs.connect(list);
Side_Obs.connect(self.target.childNodes[0].childNodes[1]);
self.loop_actions(node, mutation);
}
}
};
Lb_Subtree_Obs.added = added_to_lightbox;
const added_lb_tooltip = (self, node, mutation) => {
self.loop_actions(node, mutation);
};
Lb_TT_Obs.added = added_lb_tooltip;
const added_to_side = (self, node, mutation) => {
self.loop_actions(node, mutation);
};
Side_Obs.added = added_to_side;
const UT = {
format_date: {
age: date => {
// https://stackoverflow.com/a/23286781
const diff_date = new Date(new Date() - date);
let y = diff_date.toISOString().slice(0, 4) - 1970;
let m = diff_date.getMonth() + 0;
let d = diff_date.getDate();
let result;
if (y > 0) result = (y === 1) ? y + ' year' : y + ' years';
else if (m > 0) result = (m === 1) ? m + ' month' : m + ' months';
else result = (d === 1) ? d + ' day' : d + ' days';
return result;
}
},
get_post_author_link: node => {
const $context = $(node).find('div[data-click-id="body"] > div:nth-of-type(2)');
let links;
if ($context.length) {
links = $context[0].getElementsByTagName('a');
for (let i = 0; i < links.length; i++) {
const href = links[i].getAttribute('href');
if (href && href.startsWith('/user/')) {
return links[i];
}
}
}
},
is_page_type: (keyword) => {
let result;
switch (keyword) {
case 'profile':
result = !!document.getElementById('profile-nav-menu-tooltip');
break;
case 'search':
result = !!document.getElementById('search-results-sort');
break;
}
return result;
}
};
const get_reddit_data = function (kind, name) {
return new Promise(function (resolve, reject) {
let url;
const key = kind + '_' + name;
const cache = JSON.parse(GM_getValue('cache', '{}'));
let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));
if (cache[key]) {
resolve(cache[key]);
}
else if (!ratelimit.remaining || ratelimit.remaining > 150) {
const req = new XMLHttpRequest();
url = '/r/' + name + '/about.json';
req.open('GET', url);
req.onload = function () {
if (req.status === 200) {
const response = JSON.parse(this.response).data;
let json_data = cache;
ratelimit = {
used: this.getResponseHeader('x-ratelimit-used'),
remaining: this.getResponseHeader('x-ratelimit-remaining'),
reset: this.getResponseHeader('x-ratelimit-reset')
};
json_data[key] = {
name: response.display_name,
title: response.title,
subtitle: response.header_title,
desc: response.public_description,
created: response.created,
subs: response.subscribers,
subscriber: response.user_is_subscriber
};
GM_setValue('ratelimit_get', JSON.stringify(ratelimit));
GM_setValue('cache', JSON.stringify(json_data));
resolve(json_data[key]);
}
else {
reject(Error(req.statusText));
}
};
req.onerror = function () {
reject(Error("Network Error"));
};
req.send();
} else if (ratelimit.remaining) {
reject(ratelimit.remaining + ' requests remaining.');
}
})
};
/**
* // TBDL: deprecate.
* @param {string} html_tag Element to look inside.
* @param {string} query Inner text to match against.
* @param {Node} context Region to search.
*/
var getElementByText = function (html_tag, query, context) {
const result = document.evaluate(
"//" + html_tag + "[contains(., '" + query + "')]",
context, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
return result;
};
const clear_cache = (() => {
let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));
if (ratelimit.used !== undefined && ratelimit.used <= 1) {
GM_setValue('cache', '{}');
}
})();
const settings_form = function () {
const layout = $('<div>', {
"id": "fixd_settings",
'data-version': GM_info.script.version
}).append($('<div>', {
"class": "fixd_panel"
}).append($('<h1>', {
"text": "Fixdit Settings"
})))[0];
layout.addEventListener('click', handle_settings_click, false);
return layout;
};
const draw_options_panel = function (fid) {
const settings = $('#fixd_settings')[0];
const feature = FT[fid];
const setting_toggle = $('<label>', {
"html": '<input type="checkbox"> On',
"data-click-id": 'fixd_setting_toggle',
"data-id": fid,
"class": "fixd_switch"
})[0];
const panel = $('<div>', {
"class": "fixd_panel fixd_options"
})[0];
const btn_back = $('<button>', {
"data-click-id": "fixd_back",
"class": "fixd_btn_back",
"text": "Back"
})[0];
settings.appendChild(panel);
panel.dataset.id = feature.setting.id;
panel.appendChild(btn_back);
panel.appendChild($('<h2>', {
"text": feature.setting.label
})[0]);
if (!feature.setting.hidden) {
panel.appendChild(setting_toggle);
}
if (feature.setting.enabled) {
setting_toggle.querySelector('input').setAttribute('checked', 'checked');
setting_toggle.classList.add('fixd_enabled');
}
for (var oid in feature.options) {
draw_option_item(panel, feature.options[oid]);
}
};
const draw_option_item = function (panel, option) {
const label = document.createTextNode(option.label);
const row = $('<label>', {
"class": "fixd_option",
"data-id": option.id
})[0];
if (option.hidden) return;
// Will it be a checkbox or button?
if (!option.type || option.type !== 'bool') {
row.dataset.clickId = "fixd_option_btn";
row.appendChild($('<div>', {
"class": "fixd_option_btn",
"text": option.label
})[0]);
row.classList.add('fixd_option_btn');
row.innerText = option.label;
}
else {
row.classList.add('fixd_switch');
row.dataset.clickId = "fixd_option_toggle";
row.innerHTML = '<input type="checkbox"> ' + option.label;
}
if (option.enabled || option.type === 'radio') {
row.classList.add('fixd_enabled');
let checkbox = row.querySelector('input');
if (checkbox) {
checkbox.setAttribute('checked', 'checked');
}
}
panel.appendChild(row);
};
const append_option_selector = function (el, data, saved) {
for (let uid in data.choices) {
if (isNaN(uid)) {
saved = data; // Reset outdated saved values.
}
const choice = data.choices[uid];
const row = $('<label>', {
'html': '<input type="checkbox"> ' + choice[1],
'class': 'fixd_option_select'
})[0];
const input = row.querySelector('input');
input.setAttribute('name', 'fixd_option_choices');
input.setAttribute('value', uid);
if (data.type === 'radio') {
input.setAttribute('type', 'radio');
}
if (saved.value.includes(uid)) {
input.setAttribute('checked', 'checked');
row.classList.add('fixd_enabled');
}
el.appendChild(row);
};
};
const draw_option_dialog = function (oid) {
const panel = $('#fixd_settings .fixd_options')[0];
const fid = $(panel).data('id');
const saved = JSON.parse(GM_getValue('features', '{}'));
const option = FT[fid].options[oid];
const saved_option = saved[fid].options[oid];
if (option.type === 'list') {
saved_option.value.sort((a, b) => {
return a.localeCompare(b, 'en', { 'sensitivity': 'base' });
});
}
const dialog = panel.appendChild($('<div>', {
"data-id": oid,
"class": "fixd_dialog"
})[0]);
const dialog_content = dialog.appendChild($('<div>')[0]);
const header = $('<div>', {
"class": "fixd_settings_header"
})[0];
dialog_content.appendChild(header);
header.appendChild($('<button>', {
"data-click-id": "fixd_back",
"class": "fixd_btn_back",
"text": "Back"
})[0]);
header.appendChild($('<h3>', {
"html": FT[fid].setting.label + '<span>' + option.label + '</span>'
})[0]);
if (option.type !== 'radio') {
const toggle = $('<label>', {
"html": '<input type="checkbox"> On',
"data-click-id": 'fixd_option_toggle',
"data-id": option.id,
"class": "fixd_switch"
})[0];
dialog_content.appendChild(toggle);
if (saved_option.enabled) {
toggle.classList.add('fixd_enabled');
toggle.querySelector('input').setAttribute('checked', 'checked');
}
}
if (option.description) {
dialog_content.appendChild($('<div>', {
"class": "fixd_description",
"html": option.description
})[0]);
}
const buttons = $('<div>', {
"class": "fixd_settings_buttons"
})[0];
dialog_content.appendChild(buttons);
buttons.appendChild($('<button>', {
"data-click-id": "fixd_save",
"class": "fixd_btn_save",
"text": "Save changes"
})[0]);
if (option.type === 'list') {
dialog_content.appendChild($('<textarea>', {
"data-click-id": "fixd_option_value",
"text": saved_option.value.join('\n')
})[0]);
}
else if (option.hasOwnProperty('choices')) {
append_option_selector(dialog_content, option, saved_option);
}
$('#fixd_settings').addClass('fixd_expanded').append(dialog).css({});
};
const close_dialog = function () {
$('#fixd_settings').removeClass('fixd_expanded');
$('#fixd_settings .fixd_dialog').remove();
};
const handle_submit = function (ev) {
const panel = $('#fixd_settings .fixd_options')[0];
const fid = panel.dataset.id;
const oid = document.getElementById('fixd_selected_option').dataset.id;
const saved = JSON.parse(GM_getValue('features', '{}'));
const feature = FT[fid];
const option = feature.options[oid];
let db_entry = saved;
if (option.type === 'list') {
let value = $('#fixd_settings .fixd_dialog textarea').val();
value = value.replace(/[^a-zA-Z\d\n#._-]/mg, "");
db_entry[fid].options[oid].value = value.split('\n');
}
else if (option.hasOwnProperty('choices')) {
const inputs = document.getElementsByName('fixd_option_choices');
// Clear the old value before repopulating.
db_entry[fid].options[oid].value = [];
$(inputs).each((idx, el) => {
if (el.value && el.checked) {
if (option.type === 'radio') {
db_entry[fid].options[oid].value[0] = el.value;
return false;
}
else {
db_entry[fid].options[oid].value[idx] = el.value;
}
}
});
}
GM_setValue('features', JSON.stringify(db_entry));
close_dialog();
};
const handle_back = function (ev) {
if (document.getElementsByClassName('fixd_dialog').length) {
close_dialog();
}
else {
$('#fixd_settings .fixd_options').remove();
$('#fixd_settings .fixd_panel:not(.fixd_options)').show();
}
};
const close_settings = ev => {
$('#fixd_settings, #fixd_launch').removeClass('fixd_active');
close_dialog();
};
const handle_settings_click = (ev) => {
if (ev.target.dataset.clickId === 'fixd_setting') {
$('#fixd_settings .fixd_panel:not(.fixd_options)').hide();
draw_options_panel(ev.target.dataset.id);
}
if (ev.target.dataset.clickId === 'fixd_option_btn') {
const oid = ev.target.dataset.id;
let selected = document.getElementById('fixd_selected_option');
if (selected) {
selected.removeAttribute('id');
}
ev.target.id = 'fixd_selected_option';
draw_option_dialog(oid);
}
if (ev.target.dataset.clickId === 'fixd_save') {
handle_submit(ev);
}
if (ev.target.dataset.clickId === 'fixd_back') {
handle_back(ev);
}
if ($(ev.target).hasClass('fixd_switch') || $(ev.target.parentNode).hasClass('fixd_switch')) {
let fid = $('#fixd_settings .fixd_options').data('id');
let oid = ev.target.dataset.id || ev.target.parentNode.dataset.id;
if (!ev.target.dataset.id) {
// Checkbox has been clicked by user or label.
handle_settings_switch(ev, fid, oid);
}
}
if ($(ev.target).hasClass('fixd_option_select') ||
$(ev.target.parentNode).hasClass('fixd_option_select')) {
if (ev.target.tagName === 'INPUT') {
if (ev.target.getAttribute('type') === 'radio') {
let choices = document.getElementsByName('fixd_option_choices');
$(choices).parent().removeClass('fixd_enabled');
}
ev.target.parentNode.classList.toggle('fixd_enabled');
}
}
};
$('body').append(settings_form);
document.addEventListener('click', ev => {
const path = ev.composedPath();
const launcher = document.getElementById('fixd_launch');
const settings = document.getElementById('fixd_settings');
const popup = document.getElementsByClassName('fixd_popup')[0];
if (!path.includes(popup)) {
FT.subreddit_info.public.close_popup();
}
if (!path.includes(launcher) &&
!path.includes(settings)) {
close_settings();
}
if (ev.target.dataset.clickId === 'fixd_launcher') {
if ($(ev.target).hasClass('fixd_active')) {
close_settings();
}
else {
$('#fixd_launch, #fixd_settings').addClass('fixd_active');
$('#fixd_settings .fixd_options').remove();
$('#fixd_settings .fixd_panel:not(.fixd_options)').show();
}
}
});
const handle_settings_switch = function (ev, fid, oid) {
const saved = JSON.parse(GM_getValue('features', '{}'));
const feature = FT[fid];
const clicked = ev.target.dataset.clickId || ev.target.parentNode.dataset.clickId;
let db_entry = saved;
if (clicked === 'fixd_setting_toggle') {
if (saved[fid].enabled) {
db_entry[fid].enabled = false;
}
else {
db_entry[fid].enabled = true;
}
}
else {
let option = feature.options[oid];
if (saved[fid].options[oid].enabled) {
db_entry[fid].options[oid].enabled = false;
}
else {
db_entry[fid].options[oid].enabled = true;
}
}
GM_setValue('features', JSON.stringify(db_entry));
feature.update = db_entry[fid];
}
// Begin modules/features.
const OBS = {
on_add: {},
on_rem: {},
on_any: {}
};
const FT = {};
FT.ui_selectors = (() => {
var feature = new Feature({
id: "ui_selectors",
label: "Add UI Selectors",
enabled: true,
internal: true,
hidden: true
});
const tag_subreddits_boxes = (arg) => {
let context = arg;
let is_profile = UT.is_page_type('profile');
let lightbox = document.getElementById('lightbox');
$(context).find('button').each((idx, el) => {
if (['SUBSCRIBE', 'UNSUBSCRIBE'].includes(el.innerText.toUpperCase())) {
const item = el.parentNode.parentNode;
// TBDL: img/svg selectors should be more precise.
const img = item.getElementsByTagName('img');
const svg = item.getElementsByTagName('svg');
if (img.length === 1 || svg.length === 1) {
let $contents = $(item).parent().parent();
if (is_profile && !lightbox) {
$contents = $contents.parent();
}
let $h3 = $contents.find('> h3');
if ($h3.length === 1) {
let $container = $contents.parent().parent();
$container.addClass('fixd--subreddits');
return false;
}
}
}
});
context = $('.fixd--subreddits').filter(':last').next()[0];
let button_text = $(context).find('button:first').text().toUpperCase();
if (['SUBSCRIBE', 'UNSUBSCRIBE'].includes(button_text)) {
// Repeat
tag_subreddits_boxes(context);
}
};
// TBDL: reduce repitition:
var init = function () {
let $page, $content, $side, posts;
let lightbox = document.getElementById('lightbox');
let is_profile = UT.is_page_type('profile');
if (!lightbox) {
let comment_sort_pick = document.getElementById('CommentSort--SortPicker');
$page = $('#hamburgers + div').addClass('fixd--page');
posts = document.getElementsByClassName('Post');
$content = $(posts[0]).parent().parent().parent().parent().parent();
if (comment_sort_pick) {
$content = $(posts[0]).parent().parent().parent();
}
else if (is_profile) {
$content = $content.parent().parent();
}
}
else {
$content = $(lightbox).find('.Post').parent().parent();
}
$content.addClass('fixd--content');
$side = $content.find('+ div').addClass('fixd--side');
if ($side.length) {
// Side_Obs.connect($side[0].childNodes[0].childNodes[1]);
const moderators_h3 = getElementByText('h3', 'Moderators', $side[0]).iterateNext();
$(moderators_h3).parent().addClass('fixd--moderators');
tag_subreddits_boxes($side[0]);
}
};
init();
Page_Subtree_Obs.extend((self, mutation) => {
init();
});
Lb_Obs.extend((self, mutation) => {
init();
});
Lb_Subtree_Obs.extend((self, mutation) => {
init();
});
Side_Obs.extend((self, mutation) => {
init();
});
return feature;
})();
FT.ui_tweaks = (() => {
var feature = new Feature({
id: "ui_tweaks",
label: "UI Tweaks",
enabled: false
});
feature.add_option({
id: "no_prefix",
label: "No prefixes for subreddits, users",
type: "bool",
enabled: false
});
feature.add_option({
id: "no_blanks",
label: "All links can open in current tab",
type: "bool",
enabled: false
});
feature.add_option({
id: "override_vote_icons",
label: "Override custom vote icons",
enabled: false,
type: "bool"
});
feature.add_option({
id: "static_header",
label: "Make the top bar stationary/static",
enabled: false,
type: "bool",
update: 'test' // TBDL: function to be called when option is modified
});
feature.add_option({
id: "reduce_comment_spacing",
label: "Reduce comment spacing",
enabled: false,
type: 'bool'
});
const init_override_vote_icon = function (el) {
if (!el.childNodes.length) {
// Remove style override so we can search the url string for active/inactive
el.style.background = '';
const computed_style = window.getComputedStyle(el);
const bg_value = computed_style.getPropertyValue('background-image');
const class_list = ['fixd_no_icon'];
if (bg_value.includes('IconInactive')) {
class_list.push('fixd_inactive');
}
else if (bg_value.includes('IconActive')) {
class_list.push('fixd_active');
}
$(el).addClass(class_list.join(' '));
el.style.background = 'none';
}
};
const init_override_vote_icons = function (context) {
if (feature.options.override_vote_icons.enabled) {
const selector = 'button[data-click-id="upvote"], button[data-click-id="downvote"]';
$(context).find(selector).each((idx, el) => {
init_override_vote_icon(el);
});
}
};
const strip_prefixes = function (node) {
if (feature.options.no_prefix.enabled) {
let u = UT.get_post_author_link(node);
if (u) {
u.innerText = u.innerText.replace("u/", "");
}
let s = $(node).find('a[data-click-id="subreddit"]');
s.each((idx, el) => {
if (!el.children.length) {
el.innerText = el.innerText.replace("r/", "");
}
});
}
};
const remove_blanks = function (context) {
if (feature.options.no_blanks.enabled) {
const anchors = context.getElementsByTagName('a');
for (let i = 0; i < anchors.length; i++) {
if (anchors[i].getAttribute('target')) {
anchors[i].removeAttribute('target');
anchors[i].classList.add('fixd_no_blank');
}
}
}
};
const static_header = (enabled) => {
if (enabled) {
document.querySelector('body > div #header').style.position = 'absolute';
} else {
document.querySelector('body > div #header').style.position = '';
}
};
const reduce_comment_spacing = (enabled) => {
if (enabled) {
document.body.classList.add('fixd_reduce_comment_spacing');
} else {
document.body.classList.remove('fixd_reduce_comment_spacing');
}
}
feature.options_toggle = {
'static_header': static_header,
'reduce_comment_spacing': reduce_comment_spacing
};
if (feature.setting.enabled) {
static_header(feature.options.static_header.enabled);
reduce_comment_spacing(feature.options.reduce_comment_spacing.enabled);
init_override_vote_icons(document.body);
remove_blanks(document);
const posts = document.getElementsByClassName('Post');
for (let i = 0; i < posts.length; i++) {
strip_prefixes(posts[i]);
}
Body_Obs.extend((self, node) => {
remove_blanks(node);
});
Lb_Obs.extend((self, mutation) => {
init_override_vote_icons(self.target);
remove_blanks(self.target);
});
Lb_Subtree_Obs.extend((self, mutation) => {
init_override_vote_icons(document.getElementById('lightbox'));
remove_blanks(self.target);
});
Page_Subtree_Obs.extend((self, node) => {
const posts = node.getElementsByClassName('Post');
for (let i = 0; i < posts.length; i++) {
strip_prefixes(posts[i]);
}
remove_blanks(node);
init_override_vote_icons(node);
});
Post_Obs.extend((self, node) => {
strip_prefixes(node);
init_override_vote_icons(node);
remove_blanks(node);
});
Comment_Obs.extend((self, node) => {
init_override_vote_icons(node);
remove_blanks(node);
});
$(document).click(ev => {
if (['upvote', 'downvote'].includes(ev.target.dataset.clickId)) {
init_override_vote_icons(ev.target.parentNode);
}
});
}
return feature;
})();
FT.filter_content = (function () {
const feature = new Feature({
id: "filter_content",
label: "Filter Content",
enabled: true
});
feature.add_option({
id: "subreddits",
label: "Posts by subreddit",
type: "list",
enabled: true,
description: "One subreddit name per line. No commas or slashes. Ignores search results.",
value: []
});
feature.add_option({
id: "users",
label: "Posts by user",
description: "One user name per line. No commas or slashes. Ignores search results.",
type: "list",
enabled: false,
value: []
});
feature.add_option({
id: "comments",
label: "Comments by user",
description: "One user name per line. No commas or slashes.",
type: "list",
enabled: false,
value: []
});
const blocked_subs = feature.options.subreddits;
const blocked_submitters = feature.options.users;
const blocked_comments = feature.options.comments;
const regex = {
url_subreddit: /.*\/r\//i,
url_user: /.*\/user\//i
};
const is_match = function (query, list) {
let result = list.findIndex(item => query.toUpperCase() === item.toUpperCase());
if (result !== -1) {
return true;
}
};
const init_posts = function (context) {
const posts = context.getElementsByClassName('Post');
for (let i = 0; i < posts.length; i++) {
filter_post(posts[i].parentNode.parentNode);
}
};
const init_comments = function (context) {
const comments = context.getElementsByClassName('Comment');
for (let i = 0; i < comments.length; i++) {
filter_comment(comments[i].parentNode.parentNode.parentNode);
}
};
const filter_post = function (node) {
// if (document.getElementById('lightbox') || UT.is_page_type('profile')) return;
let sub_href, user_href;
if (feature.options.subreddits.enabled) {
sub_href = $(node).find('.Post a[data-click-id="subreddit"]:first').attr('href');
}
if (feature.options.users.enabled) {
let user_link = UT.get_post_author_link(node);
if (user_link) {
user_href = user_link.getAttribute('href');
}
}
if (sub_href) {
let sub_name = sub_href.replace(regex.url_subreddit, "").replace("/", "");
// TEST:
if (node.classList.contains('fixd_hidden')) {
console.error("Already filtered", node);
}
if (is_match(sub_name, blocked_subs.value)) {
node.classList.add('fixd_hidden');
}
}
if (!node.classList.contains('fixd_hidden') && user_href) {
let user_name = user_href.replace(regex.url_user, "").replace("/", "");
if (is_match(user_name, blocked_submitters.value)) {
node.classList.add('fixd_hidden');
}
}
};
const filter_comment = function (node) {
let user_link = $(node).find('.Comment > div:nth-of-type(2) > div > div > a:first').attr('href');
let is_hidden = !!node.getElementsByClassName('icon-expand').length;
if (!user_link || is_hidden) return false;
let user_name = user_link.replace(regex.url_user, "");
user_name = user_name.replace("/", "");
if (is_match(user_name, blocked_comments.value)) {
node.classList.add('fixd_filtered');
let cid;
node.querySelector('.Comment').classList.forEach(str => {
const match = /^t1_/.exec(str);
if (match) {
cid = match.input;
return false;
}
});
let c_node = document.getElementById(cid);
$(c_node).append($('<div>', {
'html': "<span>" + user_name + "</span> <em>(Fixdit filtered)</em>",
'class': "fixd_filter_msg"
}).append($('<button>', {
'text': 'Show comment',
'data-click-id': "fixd_unfilter_btn"
})));
}
};
const handle_body_click = (ev) => {
let clicked = ev.target;
if (clicked.dataset.clickId === 'fixd_unfilter_btn') {
// TBDL: reconnect comment_change observer for fixd_collapse.
let comment = $(clicked).parents('.fixd_filtered')[0];
if (comment.classList.contains('fixd_filtered')) {
comment.classList.replace('fixd_filtered', 'fixd_unfiltered');
}
else if (comment.classList.contains('fixd_unfiltered')) {
comment.classList.replace('fixd_unfiltered', 'fixd_filtered');
}
}
};
if (feature.setting.enabled) {
if (feature.options.comments.enabled) {
init_comments(document);
document.body.addEventListener('click', handle_body_click, false);
Lb_Obs.extend((self, node) => {
init_comments(node);
});
Lb_Subtree_Obs.extend((self, node) => {
init_comments(node);
});
Comment_Obs.extend((self, node) => {
filter_comment(node);
});
}
if (feature.options.subreddits.enabled || feature.options.users.enabled) {
init_posts(document); // page loaded
Page_Subtree_Obs.extend((self, node) => {
init_posts(node);
});
Post_Obs.extend((self, node) => {
filter_post(node);
});
}
}
return feature;
})();
FT.subreddit_info = (function () {
const feature = new Feature({
id: "subreddit_info",
label: "Subreddit Info Box",
enabled: true
});
feature.add_option({
id: 'delay',
label: 'Popup delay',
choices: {
1: ['short', 'Short', 200],
2: ['medium', 'Medium', 400],
3: ['long', 'Long', 700]
},
value: ['2'],
type: 'radio'
});
const delay_uid = feature.options.delay.value[0];
const delay_open = feature.options.delay.choices[delay_uid][2] || 400;
const delay_close = 100;
let tmo_open;
let tmo_close;
const get_popup = function (ev) {
feature.public.close_popup();
const box = $('<div>', {
"id": "fixd_popup_subreddit",
"class": 'fixd_popup'
}).css({ 'display': 'none' })[0];
box.addEventListener('click', ev => {
if (ev.target.classList.contains('fixd_popup_filter')) {
const saved = JSON.parse(GM_getValue('features', '{}'));
const name = $('#fixd_popup_subreddit').data('id');
let db_entry = saved;
const arr = saved.filter_content.options.subreddits.value;
if (ev.target.classList.contains('fixd_active')) {
const idx = arr.indexOf(name);
db_entry.filter_content.options.subreddits.value.splice(idx);
ev.target.classList.remove('fixd_active');
}
else {
db_entry.filter_content.options.subreddits.value.push(name);
ev.target.classList.add('fixd_active');
}
GM_setValue('features', JSON.stringify(db_entry));
}
}, false);
box.addEventListener('mouseover', ev => {
window.clearTimeout(tmo_close);
}, false);
box.addEventListener('mouseleave', ev => {
feature.public.close_popup();
}, false);
return box;
};
const add_popup = (data, ev) => {
const box = get_popup(ev);
const classes = [];
const filter_btn_classes = ["fixd_popup_filter"];
const saved = JSON.parse(GM_getValue('features', '{}'));
const filter_data = saved.filter_content.options.subreddits;
if (data.subscriber) {
classes.push('fixd_subscriber');
}
if (saved.filter_content.enabled && filter_data.enabled) {
classes.push('fixd_filterable');
if (filter_data.value.includes(data.name)) {
filter_btn_classes.push('fixd_active');
}
}
let document_width = $('html')[0].offsetWidth;
let target_offset = $(ev.target).offset().left;
let offset_left = target_offset;
if ((document_width - target_offset) < (document_width / 2)) {
offset_left -= 240;
}
$(box)
.attr('data-id', data.name)
.addClass(classes.join(' '))
.css({
top: $(ev.target).offset().top + ev.target.offsetHeight,
left: offset_left,
display: ''
})
.append($('<div>')
.append($('<h2>', {
"class": "fixd_popup_name",
"text": data.name
}))
.append($('<div>', {
"class": "fixd_popup_created",
"html": data.created
}))
.append($('<div>', {
"class": "fixd_popup_subs",
"html": '<span>' + data.subs + '</span> Subscribers'
}))
.append($('<button>', {
"class": filter_btn_classes.join(' '),
"text": 'Filter'
}))
)
.append($('<div>')
.append($('<div>', {
"class": "fixd_popup_title",
"text": data.title
}))
.append($('<div>', {
"class": "fixd_popup_subtitle",
"text": data.subtitle
}))
.append($('<div>', {
"class": "fixd_popup_desc",
"html": data.desc
}))
);
document.body.appendChild(box);
};
const init_popup = function (ev) {
let name = ev.target.getAttribute('href').split('/')[2];
get_reddit_data('t5', name).then((data) => {
const date_created = new Date(data.created * 1000);
const formatted = {
name: data.name,
title: data.title,
subtitle: data.subtitle,
created: UT.format_date.age(date_created),
subs: data.subs.toLocaleString(),
subscriber: data.subscriber,
desc: data.desc
};
add_popup(formatted, ev);
}, function (error) {
console.warn("Error retrieving subreddit info popup.", error);
});
};
feature.public.close_popup = (ev) => {
$('#fixd_popup_subreddit').remove();
};
if (feature.setting.enabled) {
let mouse_over = document.body.addEventListener('mouseover', ev => {
if (ev.target.tagName === 'A') {
let a = ev.target;
if ((a.dataset.clickId === 'subreddit' ||
$(a).parents('p, .md, .fixd--subreddits').length) &&
a.getAttribute('href').startsWith('/r/')) {
window.clearTimeout(tmo_open);
window.clearTimeout(tmo_close);
tmo_open = window.setTimeout(() => {
init_popup(ev);
}, delay_open);
let mouse_out = (ev) => {
ev.target.removeEventListener('mouseleave', mouse_out);
window.clearTimeout(tmo_open);
tmo_close = window.setTimeout(() => {
feature.public.close_popup();
}, delay_close);
};
a.addEventListener('mouseleave', mouse_out, false);
}
}
}, false);
}
return feature;
})();
FT.comments_collapse = (function () {
const feature = new Feature({
id: "comments_collapse",
label: "Collapsible Child Comments",
enabled: true
});
feature.add_option({
id: 'auto',
type: 'bool',
label: 'Automatically collapse children',
enabled: false
});
const auto_on = feature.options.auto.enabled;
const init = function (context, is_mutate) {
let lb = document.getElementById('lightbox');
if (!lb && UT.is_page_type('profile')) return;
let comments = context.getElementsByClassName('Comment');
if (comments.length) {
let comments_list = comments[0].parentNode.parentNode.parentNode.parentNode.childNodes;
const sort_picker = $('#CommentSort--SortPicker').parent()[0];
const btn_all_classList = ['fixd_collapse_all'];
if (auto_on && !is_mutate) {
btn_all_classList.push('fixd_active');
}
const $btn_all = $('<button>', {
"text": "children",
"class": btn_all_classList.join(' ')
});
if (sort_picker) {
$(sort_picker).append($btn_all);
}
for (var i = 0; i < comments_list.length; i++) {
init_comment(comments_list[i], is_mutate);
}
}
};
const init_comment = function (item, is_mutate) {
item.classList.add('fixd_comment_wrap');
const is_comment = !!item.getElementsByClassName('Comment').length;
const is_child = !item.getElementsByClassName('top-level').length;
const is_thread = !!item.getElementsByClassName('threadline').length;
const is_hidden = !!item.getElementsByClassName('icon-expand').length;
if (is_comment && !is_child) {
$(item).addClass('fixd_top-level');
const $next = $(item).next();
const next_is_child = $next.length && !$next[0].getElementsByClassName('top-level').length;
const next_is_thread = $next.length && !!$next[0].getElementsByClassName('threadline').length;
if (next_is_child && next_is_thread && !is_hidden && !$(item).find('.fixd_collapse').length) {
const btn_classList = ['fixd_collapse'];
if (auto_on && !is_mutate) {
btn_classList.push('fixd_active');
}
const $btn = $('<button>', {
"text": "children",
"class": btn_classList.join(' ')
});
let $target = $(item).find('button:last');
if (!$target.length) {
$target = $(item).find('a:last');
}
$btn.insertAfter($target);
}
}
if (auto_on && is_child && !is_mutate && !is_hidden) {
item.classList.add('fixd_hidden');
}
};
const toggle_visibility = function (clicked) {
let wrap = $(clicked).parents('.fixd_comment_wrap')[0];
let is_active = $(clicked).hasClass('fixd_active');
const check_next = function ($next) {
const next_is_child = !$next[0].getElementsByClassName('top-level').length;
const next_is_thread = !!$next[0].getElementsByClassName('threadline').length;
if (next_is_child && next_is_thread) {
if (is_active) {
$next.removeClass('fixd_hidden');
}
else {
$next.addClass('fixd_hidden');
}
if ($next.next().length) {
check_next($next.next());
}
}
};
check_next($(wrap).next());
if (is_active) {
$(clicked).removeClass('fixd_active');
} else {
$(clicked).addClass('fixd_active');
}
};
const handle_click = function (ev) {
if ($(ev.target).hasClass('fixd_collapse')) {
ev.stopImmediatePropagation();
toggle_visibility(ev.target);
}
else if ($(ev.target).hasClass('fixd_collapse_all')) {
ev.stopImmediatePropagation();
if ($(ev.target).hasClass('fixd_active')) {
$('.fixd_collapse.fixd_active').click();
}
else {
$('.fixd_collapse:not(.fixd_active)').click();
}
if ($('.fixd_collapse:not(.fixd_active)').length) {
$(ev.target).removeClass('fixd_active');
}
else {
$(ev.target).addClass('fixd_active');
}
}
};
if (feature.setting.enabled) {
init(document, false);
$(document).on('click', handle_click);
Lb_Obs.extend((self, node) => {
init(node, true);
});
Lb_Subtree_Obs.extend((self, node) => {
init(node, true);
});
Comment_Obs.extend((self, node, mutation) => {
init_comment(mutation.previousSibling, true);
});
}
return feature;
})();
FT.menu_hover = (function () {
var feature = new Feature({
id: "menu_hover",
label: "Hover to open menus",
enabled: false
});
feature.add_option({
id: 'menus',
label: "Choose menus",
enabled: true,
type: 'checkbox',
choices: {
1: ['sortpicker', 'Sort Posts', '#ListingSort--SortPicker'],
2: ['commentsort', 'Comment Sort Picker', '#CommentSort--SortPicker'],
3: ['user', 'User dropdown', '#USER_DROPDOWN_ID'],
4: ['headermoderate', 'Moderate (header)', '#Header--Moderation']
},
value: ['1', '2', '3', '4']
});
feature.add_option({
id: 'delay',
label: "Delay",
requires: "menus",
type: 'radio',
choices: {
1: ['short', 'Short', 200],
2: ['medium', 'Medium', 400],
3: ['long', 'Long', 700]
},
value: ['2']
});
const menus_val = feature.options.menus.value;
const delay_uid = feature.options.delay.value[0];
const delay_open = feature.options.delay.choices[delay_uid][2] || 400;
if (feature.setting.enabled && feature.options.menus.enabled) {
const init = function () {
for (let i = 0; i < menus_val.length; i++) {
const uid = menus_val[i];
if (uid !== null) {
add_menu_listener(feature.options.menus.choices[uid]);
}
}
};
const add_menu_listener = function (menu) {
let commit_timeout;
const name = menu[0];
let selector = menu[2];
if (name === 'sortpicker') {
selector = $(selector).parent()[0];
}
$(selector).on('mouseenter', function (ev) {
commit_timeout = window.setTimeout(() => {
ev.currentTarget.click();
}, delay_open);
});
$(selector).on('mouseleave', function (ev) {
window.clearTimeout(commit_timeout);
});
};
init();
if (menus_val.includes('2')) {
Lb_Obs.extend((self, mutation) => {
add_menu_listener(feature.options.menus.choices[2]);
});
Lb_Subtree_Obs.extend((self, mutation) => {
add_menu_listener(feature.options.menus.choices[2]);
});
}
}
return feature;
})();
FT.remindme = (function () {
const feature = new Feature({
id: "remindme",
label: "Remind Me",
enabled: false,
hidden: true
});
return feature;
})();
FT.debug = (function () {
var feature = new Feature({
id: "debug",
label: "Debug Fixdit",
enabled: false
});
const init = (enabled) => {
if (enabled) {
$('#fixd_indicators').show();
document.body.classList.add('fixd_debug');
}
else {
$('#fixd_indicators').hide();
document.body.classList.remove('fixd_debug');
}
};
feature.toggle = init;
init(feature.setting.enabled);
return feature;
})();
$('body').append('<div id="fixd_launch" data-click-id="fixd_launcher"></div>');
});
})(window.jQuery.noConflict(true));