您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
UX enhancements for Reddit's 2018 redesign: filters, collapse child comments, subreddit info...
// ==UserScript== // @name Fixdit for Reddit Redesign // @namespace http://tampermonkey.net/ // @version 0.7.4.2 // @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.3.1/jquery.slim.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_openInTab // @noframes // ==/UserScript== (function ($, undefined) { $(function () { const GetElById = document.getElementById.bind(document); const GetElsByClass = document.getElementsByClassName.bind(document); const GetElsByName = document.getElementsByName.bind(document); const GetEl = document.querySelector.bind(document); const GetEls = document.querySelectorAll.bind(document); const GetStyle = (e, p, v) => window.getComputedStyle.bind(window, [e, p]).getPropertyValue(v); if (!GetElById('SHORTCUT_FOCUSABLE_DIV')) return; // probably using old site. const _stylesheet = String.raw`<style type="text/css" id="fixdit">#fixd_settings em,.fixd_filter_msg em{font-style:italic}.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_list_area+div,.fixd_static_header .fixd_overlay_head,.fixd_static_header .fixd_overlay_head+div{top:0}#fixd_settings.fixd_expanded{bottom:5px}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{line-height:1.5;margin:-8px -12px 0;padding:8px}#fixd_settings h2,#fixd_settings h3{display:inline-block;margin-bottom:10px;vertical-align:middle}#fixd_settings strong{font-weight:700}#fixd_settings h1{font-size:140%;font-weight:400;margin:0 0 .5em}#fixd_settings h2{font-size:120%;font-weight:400}#fixd_settings h3{font-size:100%;font-weight:400}#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_dialog{position:absolute;top:0;left:0;right:0;bottom:0;padding:24px 12px 8px;border-radius:4px;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_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}.fixd_debug .fixd_hidden{opacity:.6}.fixd_debug .fixd_filtered{outline:#dc143c solid 1px}.fixd_debug .fixd_unfiltered{outline:green solid 1px}.fixd_filter_msg{font-size:12px;color:#878a8c}.fixd_filter_msg button{content:"Show comment";color:inherit;background:0 0;cursor:pointer;margin:12px 0 0 12px}.fixd_override_vote_icon>div{color:inherit!important}button.fixd_no_icon[data-click-id=upvote],button.fixd_no_icon[data-click-id=downvote]{background-image:none!important}button.fixd_no_icon[data-click-id=upvote][aria-pressed=true]{color:#f40!important}button.fixd_no_icon[data-click-id=downvote][aria-pressed=true]{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:"\F130"}button.fixd_no_icon[data-click-id=downvote]:before{content:"\F109"}body.fixd_reduce_comment_spacing .Comment.top-level{margin-top:0}.fixd_debug .fixd_no_blank{outline:#00f solid 1px}body.fixd_visited_links .Comment p a:visited,body.fixd_visited_links .Comment span>a:visited,body.fixd_visited_links .Post p a:visited,body.fixd_visited_links .Post span>a:visited{color:purple}.fixd_debug[data-fixd-view=compact] .fixd_list_area .Post>div>div:nth-of-type(2)>div>div:first-child{background-color:#c1e0fe}.fixd_expando_hitbox[data-fixd-view=compact] .fixd_list_area .Post>div>div:nth-of-type(2)>div>div:first-child{margin:0 8px}.fixd_expando_hitbox[data-fixd-view=compact] .fixd_list_area .Post>div>div:nth-of-type(2)>div>div:first-child button{margin:0}.fixd_debug #lightbox{margin:0 200px;width:auto}.fixd_debug #overlayFixedContent+div+div{background-color:rgba(0,0,0,.5)}.fixd_debug .fixd_observed:before{content:"o";position:absolute;z-index:100;padding:1px 3px;color:#fff;font-size:12px;border:2px solid hsla(0,0%,100%,.2);background:hsla(0,0%,0%,.5);pointer-events:none}.fixd_static_header header{position:absolute;z-index:40}</style>`; document.body.insertAdjacentHTML('beforeend', _stylesheet); // TODO: (current issues) // Add observer to hidden comments. // Observer for switching from comment permalink to all comments. // Use typescript. // CHANGELOG: (latest) // Apply 'static header' in overlay window. // Utils: const UT = { format_date: { age(date) { date = new Date(date).valueOf(); const difference = new Date(new Date().valueOf() - date); let y = parseInt(difference.toISOString().slice(0, 4), 10) - 1970; let m = difference.getMonth() + 0; let d = difference.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) { if (!node) return; const region = node.querySelector('div[data-click-id="body"]'); let links; if (region) { links = region.getElementsByTagName('a'); for (const link of links) { if (!link.innerText.startsWith('u/')) continue; const href = link.getAttribute('href'); if (href && href.startsWith('/user/')) { return link; } } } }, page_type() { const pages = { 'profile': () => { return !!GetElById('profile-nav-menu-tooltip'); }, 'search': () => { return !!GetElById('search-results-sort'); }, 'listing': () => { return !!GetElById('ListingSort--SortPicker'); }, 'comments': () => { return !!GetElById('CommentSort--SortPicker'); } }; const result = [...arguments].map((arg) => { return pages[arg](); }); return result.includes(true); }, node_text_includes(str, node) { const child = (node) ? node.firstChild : undefined; if (child && child.nodeValue) { return child.nodeValue.includes(str); } }, get_els_by_text(str, tag, region) { const tags = region.getElementsByTagName(tag); let result = []; for (let i = 0; i < tags.length; i++) { if (UT.node_text_includes(str, tags[i])) { result.push(tags[i]); } } return result; }, nth_parent(node, n) { if (!Number.isInteger(n) && n < 1) throw "First argument must be a positive integer"; let parent = node; if (!parent) return; for (let i = 0; i < n; i++) { parent = parent.parentNode; } return parent; }, list_has(list, query, ignore_case) { if (!query) return false; if (ignore_case) { list = list.map(i => i.toUpperCase()); query = query.toUpperCase(); } return list.includes(query); }, comment_count(region) { // TBDL: interpret abbreviated numbers. const icon = region.querySelector('.icon-comment'); if (icon && icon.nextElementSibling) { const num = parseInt(icon.nextElementSibling.innerText, 10); return !isNaN(num) ? num : 0; } else { return -1; } }, hide(node) { if (!node) return; node.style.display = 'none'; }, show(node) { if (!node) return; const initial = window.getComputedStyle(node).getPropertyValue('display'); if (initial === 'none') node.style.display = ''; } }; class Feature { constructor(data) { const loaded = JSON.parse(GM_getValue('features', '{}')); if (loaded.hasOwnProperty(data.id)) { data.enabled = loaded[data.id].enabled; if (loaded[data.id].hasOwnProperty('options')) { this.loaded_options = loaded[data.id].options; } } else { const db_entry = loaded; db_entry[data.id] = { enabled: data.enabled, options: {} }; GM_setValue('features', JSON.stringify(db_entry)); } for (const key in data) { this[key] = data[key]; } if (!data.hidden) { draw_setting(data); } this.options = {}; this.option_callbacks = {}; this.public = {}; } add_option(option) { const loaded_options = this.loaded_options; let is_stored = loaded_options.hasOwnProperty(option.id); 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 { const db_entry = JSON.parse(GM_getValue('features', '{}')); db_entry[this.id].options[option.id] = {}; if (option.hasOwnProperty('value')) { db_entry[this.id].options[option.id].value = option.value; } db_entry[this.id].options[option.id].enabled = option.enabled; GM_setValue('features', JSON.stringify(db_entry)); } this.options[option.id] = new Option(option); // Delete temporary object when we're finished with it: if (this.loaded_options.hasOwnProperty(option.id)) { delete this.loaded_options[option.id]; } } toggle() { if (this.callback) this.callback(this.enabled); const callbacks = this.option_callbacks; // Go through each option cb, sending the value of feature.enabled. for (const key in callbacks) { callbacks[key](this.enabled); } } set on_toggle(fn) { this.callback = fn; } set on_toggle_option(functions) { this.option_callbacks = functions; } update_nodes(nodes) { nodes.forEach(n => { n.classList.toggle('fixd_enabled'); }); } update(data, nodes) { this.enabled = data; this.toggle(); if (nodes) this.update_nodes(nodes); } update_option(data, oid, nodes) { this.options[oid].enabled = data; const callbacks = this.option_callbacks; if (this.enabled && callbacks.hasOwnProperty(oid)) { callbacks[oid](data); } if (nodes) this.update_nodes(nodes); } update_values(oid, data) { this.options[oid].value = data; } } class Option { constructor(data) { for (const key in data) { this[key] = data[key]; } } } class Reddit_Observer { constructor(arg) { this.name = arg.name; this.target = arg.target; if (arg.options) { this.options = arg.options; } else { this.options = { childList: true }; } this.actions = []; this.watch(arg.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(mutation, other) { for (let i = 0; i < this.actions.length; i++) { this.actions[i](this, mutation, other); } } extend(fn) { this.actions.push(fn); if (this.target) { this.watch(); } } watch(newTarget) { if (newTarget) { this.target = newTarget; } else if (!this.target) { return; } const self = this; function mutation(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); if (this.target.constructor.name !== 'NodeList') { this.observer.observe(this.target, this.options); } else { for (const target of this.target) { this.observer.observe(target, this.options); } } } } // Set up pre-defined Observers: const Body_Obs = new Reddit_Observer({ name: 'Body', target: document.body }); const View_Obs = new Reddit_Observer({ name: 'View', target: GetEls('#view--layout--FUE button'), options: { attributes: true } }); const Content_Obs = new Reddit_Observer({ name: 'Content area', target: GetEl('header').parentNode.nextElementSibling, options: { childList: true, subtree: true } }); const Post_Obs = new Reddit_Observer({ name: 'Post', target: GetEl('header').parentNode.parentNode, options: { childList: true, subtree: true } }); const Comment_Obs = new Reddit_Observer({ name: 'Comment', target: (() => { const c = GetElsByClass('Comment'); if (c.length) { return UT.nth_parent(c[0], 4); } })() }); const Lb_Obs = new Reddit_Observer({ name: 'LB', target: GetElById('SHORTCUT_FOCUSABLE_DIV').children[0] }); const Lb_Comments_Obs = new Reddit_Observer({ name: 'LB comments', target: GetElById('overlayScrollContainer'), options: { childList: true, subtree: true } }); const Lb_TT_Obs = new Reddit_Observer({ name: 'L-box tooltip', target: GetElById('overlayAbsoluteTooltipContent') }) const Side_Obs = new Reddit_Observer({ name: 'Side', target: GetElsByClass('fixd--side')[0], options: { childList: true, subtree: true } }); // Handle each mutation: function added_to_body(self, node) { self.loop_actions(node); }; Body_Obs.added = added_to_body; function changed_view(self, mutation) { if (mutation.attributeName === 'aria-pressed') { const isActive = JSON.parse(mutation.target.attributes['aria-pressed'].value); if (isActive) { document.body.dataset.fixdView = mutation.target.attributes['aria-label'].value; } } } View_Obs.any = changed_view; // TBDL: more testing needed to reduce overhead. function added_to_list_area(self, node) { if (node.classList.contains('Post')) return; // TODO: shorten statement: if (node.children[0] && node.children[0].children[0] && node.children[0].children[0].classList.contains('Post')) return; if (node.getElementsByClassName('Post').length) { Side_Obs.watch(GetEl('.fixd--side')); Post_Obs.watch(); View_Obs.watch(GetEls('#view--layout--FUE button')); self.loop_actions(node); } }; Content_Obs.added = added_to_list_area; function added_to_post_list(self, node) { if (node.querySelector('.Post') || node.classList.contains('Post')) { self.loop_actions(node); } }; Post_Obs.added = added_to_post_list; function added_to_comment_list(self, node, mutation) { if (node.getElementsByClassName('Comment')[0]) { self.loop_actions(node, mutation); } }; Comment_Obs.added = added_to_comment_list; function add_remove_lightbox(self, mutation) { if (mutation.addedNodes.length) { const node = mutation.addedNodes[0]; const lb = GetElById('overlayScrollContainer'); const com_count = UT.comment_count(node); const comments = node.getElementsByClassName('Comment'); if (com_count > -1 && comments.length > 0) { // Comments found, and watch for new comments. const list = UT.nth_parent(comments[0], 4); Comment_Obs.watch(list); Lb_Comments_Obs.loop_actions(list); // Run actions of other observer. } else if (!comments.length) { // watch for preloaded comments. Lb_Comments_Obs.watch(lb); } const side = lb.children[0].children[1]; if (side) { Side_Obs.loop_actions(side); self.loop_actions(lb); } else { // watch for side Side_Obs.watch(lb); } Lb_TT_Obs.watch(GetElById('overlayAbsoluteTooltipContent')); } else { Lb_Obs.watch(); } }; Lb_Obs.any = add_remove_lightbox; function added_preloaded_comments(self, node, mutation) { const comments = self.target.getElementsByClassName('Comment'); if (comments.length > 0) { const list = UT.nth_parent(comments[0], 4); if (list.parentNode === node) { // Freshly loaded: Comment_Obs.watch(list); self.loop_actions(list); } } }; Lb_Comments_Obs.added = added_preloaded_comments; function added_lb_tooltip(self, node, mutation) { self.loop_actions(node, mutation); }; Lb_TT_Obs.added = added_lb_tooltip; function added_to_side(self, node, mutation) { const side = self.target.children[0].children[1]; if (side.parentNode === node) { Lb_Obs.loop_actions(self.target); } }; Side_Obs.added = added_to_side; function get_reddit_data(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.'); } }) }; const clear_cache = (() => { let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}')); if (ratelimit.used !== undefined && ratelimit.used <= 1) { GM_setValue('cache', '{}'); } })(); function draw_options_panel(fid) { const feature = FT[fid]; const tpl = `<div id="fixd_options" data-id="${fid}" class="fixd_panel">\ <button class="fixd_btn_back">Back</button>\ <h2>${feature.label}</h2></div>`; const classes = ['fixd_switch', 'fixd_setting_switch']; let checked = ''; if (feature.enabled) { classes.push('fixd_enabled'); checked = 'checked'; } const toggle = `<label data-id="${fid}" class="${classes.join(' ')}"> \ <input type="checkbox" ${checked}> On</label>`; UT.hide(GetElById('fixd_settings_menu')); GetElById('fixd_settings').insertAdjacentHTML('beforeend', tpl); const panel = GetElById('fixd_options'); if (!feature.hidden) { panel.insertAdjacentHTML('beforeend', toggle); } draw_option_items(feature.options, panel); }; function draw_option_items(options, target) { for (let oid in options) { const option = options[oid]; if (option.hidden) continue; const is_bool = ['bool', undefined].includes(option.type); const is_on = !option.hasOwnProperty('enabled') || option.enabled; const classes = ['fixd_option']; if (is_bool) { classes.push('fixd_switch', 'fixd_option_switch'); } else { classes.push('fixd_option_btn'); } if (is_on) { classes.push('fixd_enabled'); } const template = `<label data-id="${option.id}" class="${classes.join(' ')}">\ ${is_bool ? `<input type="checkbox" ${is_on ? `checked` : ``}> ` : ``}${option.label}</label>`; target.insertAdjacentHTML('beforeend', template); } }; function draw_option_choices(data, saved, target) { for (let uid in data.choices) { const label = data.choices[uid][1]; const classes = ['fixd_option_select']; let type = 'radio'; let checked = ''; if (data.type !== 'radio') { type = 'checkbox'; } if (saved.value.includes(uid)) { checked = 'checked'; classes.push('fixd_enabled'); } const tpl = `<label class="${classes.join(' ')}">\ <input type="${type}" ${checked} name="fixd_option_choices" value="${uid}">\ ${label}</label>`; target.insertAdjacentHTML('beforeend', tpl); }; }; function draw_option_dialog(oid) { const panel = GetElById('fixd_options'); const fid = panel.dataset.id; const saved = JSON.parse(GM_getValue('features', '{}')); const option = FT[fid].options[oid]; const saved_option = saved[fid].options[oid]; const type = option.type; const is_on = saved_option.enabled; let list_val; if (type === 'list') { saved_option.value.sort((a, b) => { return a.localeCompare(b, 'en', { 'sensitivity': 'base' }); }); list_val = saved_option.value.join('\n'); } let toggle = ''; if (option.hasOwnProperty('enabled')) { const classes = ['fixd_switch', 'fixd_option_switch']; if (is_on) { classes.push('fixd_enabled'); } toggle = `<label class="${classes.join(' ')}" data-id="${oid}">\ <input type="checkbox" ${is_on ? `checked` : ``}> On</label>`; } const description = option.description ? option.description : ''; const tpl = `\ <div id="fixd_dialog" data-id="${oid}">\ <div class="fixd_settings_header">\ <button class="fixd_btn_back">Back</button>\ <h3>${FT[fid].label} <span>${option.label}</span></h3>\ </div>\ ${toggle}\ <div class="fixd_description">${description}</div>\ <div class="fixd_settings_buttons">\ <button class="fixd_btn_save">Save changes</button></div>\ ${type === 'list' ? `<textarea name="fixd_option_list">${list_val}</textarea>` : ``}\ </div>`; panel.insertAdjacentHTML('beforeend', tpl); const dialog = GetElById('fixd_dialog'); if (type !== 'list') { draw_option_choices(option, saved_option, dialog); } GetElById('fixd_settings').classList.add('fixd_expanded'); }; function close_dialog() { GetElById('fixd_settings').classList.remove('fixd_expanded'); $('#fixd_dialog').remove(); }; function handle_submit() { const panel = GetElById('fixd_options'); const fid = panel.dataset.id; const oid = GetElById('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 = GetElsByName('fixd_option_list')[0].value; value = value.replace(/[^a-zA-Z\d\n#._-]/mg, ""); db_entry[fid].options[oid].value = value.split('\n'); } else if (option.hasOwnProperty('choices')) { const choices = GetElsByName('fixd_option_choices'); // Clear the old value before repopulating. db_entry[fid].options[oid].value = []; function handle_choices(el, idx) { 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; } } } choices.forEach(handle_choices); } GM_setValue('features', JSON.stringify(db_entry)); feature.update_values(oid, db_entry[fid].options[oid].value); close_dialog(); }; function handle_back(ev) { if (GetElById('fixd_dialog')) { close_dialog(); } else { $('#fixd_options').remove(); UT.show(GetElById('fixd_settings_menu')); } }; function close_settings() { GetEls('#fixd_settings, #fixd_launch').forEach(e => e.classList.remove('fixd_active')); close_dialog(); }; function handle_settings_click(ev) { const clicked = ev.target; const parent = clicked.parentNode; const class_list = clicked.classList; const has_class = c => class_list ? class_list.contains(c) : false; const data_id = clicked.dataset.id; const is_input = clicked.tagName === 'INPUT'; if (has_class('fixd_setting_btn')) { draw_options_panel(data_id); } else if (has_class('fixd_option_btn')) { let selected = GetElById('fixd_selected_option'); if (selected) selected.removeAttribute('id'); clicked.id = 'fixd_selected_option'; draw_option_dialog(data_id); } else if (has_class('fixd_btn_save')) { handle_submit(ev); } else if (has_class('fixd_btn_back')) { handle_back(ev); } else if (is_input && parent.classList.contains('fixd_switch')) { handle_switch(parent); } else if (is_input && parent.classList.contains('fixd_option_select')) { if (clicked.getAttribute('type') === 'radio') { let choices = GetElsByName('fixd_option_choices'); choices.forEach(i => i.parentNode.classList.remove('fixd_enabled')); } parent.classList.toggle('fixd_enabled'); } }; const insert_settings_form = (() => { const tpl = `<div id="fixd_settings" data-version="${GM_info.script.version}"> <div id="fixd_settings_menu" class="fixd_panel"><h1>Fixdit Settings</h1></div></div>`; document.body.insertAdjacentHTML('beforeend', tpl); const box = GetElById('fixd_settings'); box.addEventListener('click', handle_settings_click, false); })(); function draw_setting(data) { const classes = ['fixd_option_btn', 'fixd_setting_btn']; if (data.enabled) { classes.push('fixd_enabled'); } const template = `<div data-id="${data.id}" class="${classes.join(' ')}">\ ${data.label}</div>`; GetElById('fixd_settings_menu').insertAdjacentHTML('beforeend', template); } document.addEventListener('click', ev => { const path = ev.composedPath(); const launcher = GetElById('fixd_launch'); const settings = GetElById('fixd_settings'); const popup = GetElsByClass('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 === launcher) { if (ev.target.classList.contains('fixd_active')) { close_settings(); } else { launcher.classList.add('fixd_active'); settings.classList.add('fixd_active'); $('#fixd_options').remove(); UT.show(GetElById('fixd_settings_menu')); } } }); function handle_switch(clicked) { const saved = JSON.parse(GM_getValue('features', '{}')); const fid = GetElById('fixd_options').dataset.id; const feature = FT[fid]; const db_entry = saved; const nodes = [clicked]; let oid; if (clicked.classList.contains('fixd_option_switch')) { oid = clicked.dataset.id; } if (oid) { if (GetElById('fixd_dialog')) { nodes.push(GetEl(`.fixd_option_btn[data-id="${oid}"]`)); } if (saved[fid].options[oid].enabled) { db_entry[fid].options[oid].enabled = false; } else { db_entry[fid].options[oid].enabled = true; } feature.update_option(db_entry[fid].options[oid].enabled, oid, nodes); } else if (fid) { nodes.push(GetEl(`.fixd_setting_btn[data-id="${fid}"]`)); if (saved[fid].enabled) { db_entry[fid].enabled = false; } else { db_entry[fid].enabled = true; } feature.update(db_entry[fid].enabled, nodes); } else { return; } GM_setValue('features', JSON.stringify(db_entry)); } // Begin modules/features. const FT = {}; FT.ui_selectors = (() => { const ftr = new Feature({ id: "ui_selectors", label: "Add UI Selectors", enabled: true, internal: true, hidden: true }); function tag_body() { const layout_switch_card = GetElById('layoutSwitch--card'); if (!layout_switch_card) return; const layout_switches_wrap = layout_switch_card.parentNode; let view_mode; for (const button of layout_switches_wrap.getElementsByTagName('button')) { const pressed = JSON.parse(button.getAttribute('aria-pressed')); if (pressed) { view_mode = button.getAttribute('aria-label'); } } document.body.dataset.fixdView = view_mode; const list_area = GetEl('header').parentNode.nextElementSibling; if (list_area) { list_area.classList.add('fixd_list_area'); } } function tag_subreddits_boxes(arg) { if (!arg) return; let region = arg; let is_profile = UT.page_type('profile'); let lightbox = GetElById('overlayScrollContainer'); for (const el of region.getElementsByTagName('button')) { if (UT.list_has(['subscribe', 'unsubscribe'], el.innerText, true)) { const item = el.parentNode.parentNode; const img = item.getElementsByTagName('img'); const svg = item.getElementsByTagName('svg'); if (img.length === 1 || svg.length === 1) { let contents = item.parentNode.parentNode.parentNode; if (is_profile && !lightbox) { contents = contents.parentNode; } const a = item.querySelector('a'); const sib = a.nextElementSibling; const p = sib && sib.tagName === 'P' ? sib : undefined; if (p && a.getAttribute('href').startsWith('/r/') && UT.node_text_includes('subscribers', p)) { let container = contents.parentNode.parentNode; container.classList.add('fixd--subreddits'); break; } } } } region = [...GetElsByClass('fixd--subreddits')].pop(); region = region ? region.nextElementSibling : undefined; if (!region) return; let button = region.querySelector('button'); let label = button ? button.innerText.toUpperCase() : undefined; if (UT.list_has(['subscribe', 'unsubscribe'], label, true)) { // Repeat tag_subreddits_boxes(region); } }; function tag_content(region, np) { const posts = document.getElementsByClassName('Post'); if (posts.length) { // Walk thru parents if np is defined, else take 'region' as content node. let content = np ? UT.nth_parent(posts[0], np) : region; content.classList.add('fixd--content'); tag_side(content); } const overlay = GetElById('overlayScrollContainer'); if (overlay) { const overlayHead = overlay.parentNode.previousSibling; if (overlayHead) { overlayHead.classList.add('fixd_overlay_head'); } } }; const tag_side = content => { const side = content.nextElementSibling; if (!side) return; side.classList.add('fixd--side'); const mods_h = UT.get_els_by_text('Moderators', 'h3', side)[0]; if (mods_h) { mods_h.parentNode.classList.add('fixd--moderators'); } tag_subreddits_boxes(side); }; // doc loaded: tag_body(); if (UT.page_type('comments')) { tag_content(document, 3); } else if (UT.page_type('profile')) { const images = GetEl('header').parentNode.nextElementSibling.getElementsByTagName('img'); for (const img of images) { if (img.src && img.src.startsWith('https://www.redditstatic.com/avatars')) { let a = img.parentNode.parentNode.querySelector('a'); if (a && a.href && a.getAttribute('href').startsWith('/user/')) { return tag_content(UT.nth_parent(img, 5), 7); } } } } else if (UT.page_type('listing')) { tag_content(document, 5); } Content_Obs.extend((self, node) => { tag_body(); tag_content(document, 5); }); Lb_Obs.extend((self, node) => { tag_content(node.children[0].children[0]); }); Side_Obs.extend((self, node) => { tag_subreddits_boxes(self.target); }); return ftr; })(); FT.ui_tweaks = (() => { const ftr = new Feature({ id: "ui_tweaks", label: "UI Tweaks", enabled: false }); ftr.add_option({ id: 'middle_click_posts', label: 'Middle mouse click behavior', description: `Choose what happens when you press the middle mouse\ button in the empty space of a post. Might not work for Firefox.`, enabled: false, type: 'radio', choices: { 1: ['do_nothing', "Do nothing (no scroll)"], 2: ['view_thread', 'Open comments in new tab'], 3: ['view_thread_fg', 'Open comments & switch to tab'] }, value: ['1'] }); ftr.add_option({ id: "no_prefix", label: "No prefixes for subreddits, users", type: "bool", enabled: false }); ftr.add_option({ id: "no_blanks", label: "All links can open in current tab", type: "bool", enabled: false }); ftr.add_option({ id: "override_vote_icons", label: "Override custom vote icons", enabled: false, type: "bool" }); ftr.add_option({ id: "static_header", label: "Make the top bar stationary/static", enabled: false, type: "bool" }); ftr.add_option({ id: "reduce_comment_spacing", label: "Reduce comment spacing", enabled: false, type: 'bool' }); ftr.add_option({ id: "visited_links", label: "Different color for visited links", enabled: true, type: 'bool' }); ftr.add_option({ id: "expando_hitbox", label: "Tighten expando hover area (in compact mode)", enabled: true, type: 'bool' }); function handle_mousedown(ev) { if (ev.button !== 1) return; const lb = GetElById('overlayScrollContainer'); const listing = UT.page_type('listing', 'search'); if (!lb && listing) { let path = ev.composedPath(); let post = (() => { // Check if I clicked inside a post or link. for (let i = 0; i < path.length; i++) { let n = path[i]; let has_class = c => n.classList ? n.classList.contains(c) : false; if (['A', 'BODY'].includes(n.tagName) || has_class('fixd--side')) break; if (has_class('Post')) { const link = n.querySelector('a[data-click-id="body"]'); if (link) { return { el: n, url: link.href }; } } } })(); if (post) { ev.stopImmediatePropagation(); ev.preventDefault(); if (ftr.options.middle_click_posts.value[0] !== '1') { let bg = false; if (ftr.options.middle_click_posts.value[0] === '2') { bg = true; } GM_openInTab(post.url, bg); } } } }; function config_middle_click(feature_on) { if (feature_on && ftr.options.middle_click_posts.enabled) { document.body.addEventListener('mousedown', handle_mousedown, false); } else { document.body.removeEventListener('mousedown', handle_mousedown); } }; function check_vote_icon(up) { const wrap = up.parentNode; const down = wrap.querySelector('button[data-click-id="downvote"]'); const score = wrap.querySelector('div'); // Customized vote buttons do not have child nodes (until we add one via CSS) if (!up.children.length) override_vote_icon(up); if (!down.children.length) override_vote_icon(down); // remove colour override from score. if (up.classList.contains('fixd_no_icon') || down.classList.contains('fixd_no_icon')) { wrap.classList.add('fixd_override_vote_icon'); } }; function init_override_vote_icons(region) { if (ftr.options.override_vote_icons.enabled) { const selector = 'button[data-click-id="upvote"]'; region.querySelectorAll(selector).forEach(check_vote_icon); } }; function override_vote_icon(el) { // Remove style override so we can search the url string for active/inactive el.classList.add('fixd_no_icon'); el.style.background = 'none'; } function strip_prefixes(node) { if (ftr.options.no_prefix.enabled) { let u = UT.get_post_author_link(node); if (u) { u.innerText = u.innerText.replace("u/", ""); } let sub_links = node.querySelectorAll('a[data-click-id="subreddit"]'); for (const a of sub_links) { if (!a.children.length) { a.innerText = a.innerText.replace("r/", ""); } }; } }; function remove_blanks(region) { if (ftr.options.no_blanks.enabled) { const anchors = region.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'); } } } }; function static_header(feature_on) { if (feature_on && ftr.options.static_header.enabled) { const header = GetEl('header'); document.body.classList.add('fixd_static_header'); } else { const header = GetEl('header'); document.body.classList.remove('fixd_static_header'); } }; function reduce_comment_spacing(feature_on) { if (feature_on && ftr.options.reduce_comment_spacing.enabled) { document.body.classList.add('fixd_reduce_comment_spacing'); } else { document.body.classList.remove('fixd_reduce_comment_spacing'); } } function visited_links(feature_on) { if (feature_on && ftr.options.visited_links.enabled) { document.body.classList.add('fixd_visited_links'); } else { document.body.classList.remove('fixd_visited_links'); } } function expando_hitbox(feature_on) { if (feature_on && ftr.options.expando_hitbox.enabled) { document.body.classList.add('fixd_expando_hitbox'); } else { document.body.classList.remove('fixd_expando_hitbox'); } } ftr.on_toggle_option = { 'static_header': static_header, 'reduce_comment_spacing': reduce_comment_spacing, 'visited_links': visited_links, 'expando_hitbox': expando_hitbox, 'middle_click_posts': config_middle_click }; if (ftr.enabled) { config_middle_click(ftr.enabled); static_header(ftr.enabled); reduce_comment_spacing(ftr.enabled); visited_links(ftr.enabled); expando_hitbox(ftr.enabled); init_override_vote_icons(document.body); remove_blanks(document); const posts = GetElsByClass('Post'); for (const post of posts) { strip_prefixes(post); remove_blanks(post); } Lb_Obs.extend((self, node) => { init_override_vote_icons(node); remove_blanks(node); }); Lb_Comments_Obs.extend((self, node) => { init_override_vote_icons(node); remove_blanks(node); }); Content_Obs.extend((self, node) => { const posts = self.target.getElementsByClassName('Post'); for (const post of posts) { strip_prefixes(post); remove_blanks(post); init_override_vote_icons(post); } }); 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.body.addEventListener('click', ev => { if (['upvote', 'downvote'].includes(ev.target.dataset.clickId)) { init_override_vote_icons(ev.target.parentNode); } }); } return ftr; })(); FT.filter_content = (function () { const ftr = new Feature({ id: "filter_content", label: "Filter Content", enabled: true }); ftr.add_option({ id: "subreddits", label: "Posts by subreddit", type: "list", enabled: true, description: "One <em>subreddit name</em> per line. No commas or slashes. Ignores search results.", value: [] }); ftr.add_option({ id: "users", label: "Posts by user", description: "One <em>user name</em> per line. No commas or slashes. Ignores search results.", type: "list", enabled: false, value: [] }); ftr.add_option({ id: "comments", label: "Comments by user", description: "One <em>user name</em> per line. No commas or slashes.", type: "list", enabled: false, value: [] }); const blocked_subs = ftr.options.subreddits.value.map(i => i.toUpperCase()); const blocked_submitters = ftr.options.users.value.map(i => i.toUpperCase()); const blocked_comments = ftr.options.comments.value.map(i => i.toUpperCase()); const regex = { url_subreddit: /.*\/r\//i, url_user: /.*\/user\//i }; function init_posts(region) { if (UT.page_type('comments', 'search', 'profile')) return; const posts = region.getElementsByClassName('Post'); for (let i = 0; i < posts.length; i++) { filter_post(posts[i].parentNode.parentNode); } }; function init_comments(region) { const comments = region.getElementsByClassName('Comment'); for (let i = 0; i < comments.length; i++) { filter_comment(UT.nth_parent(comments[i], 3)); } }; function filter_post(node) { let sub_href, user_href; if (ftr.options.subreddits.enabled) { let sub_a = node.querySelector('a[data-click-id="subreddit"]'); sub_href = sub_a ? sub_a.getAttribute('href') : undefined; } if (ftr.options.users.enabled) { let user_a = UT.get_post_author_link(node); user_href = user_a ? user_a.getAttribute('href') : undefined; } if (sub_href) { let sub_name = sub_href.replace(regex.url_subreddit, "").replace("/", ""); if (UT.list_has(blocked_subs, sub_name.toUpperCase())) { node.classList.add('fixd_hidden'); } } if (!node.classList.contains('fixd_hidden') && user_href) { let user_name = user_href.replace(regex.url_user, "").replace("/", ""); if (UT.list_has(blocked_submitters, user_name.toUpperCase())) { node.classList.add('fixd_hidden'); } } }; function filter_comment(node) { const comment_body = node.querySelector('.Comment').children[1]; if (!comment_body) return; let user_link = comment_body.children[0].querySelector('a'); user_link = user_link ? user_link.getAttribute('href') : undefined; if (!user_link) return false; let user_name = user_link.replace(regex.url_user, ""); user_name = user_name.replace("/", ""); if (UT.list_has(blocked_comments, user_name.toUpperCase())) { 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 = GetElById(cid); const tpl = `<div class="fixd_filter_msg"><span>${user_name}</span>\ <em>(Fixdit filtered)</em>\ <button data-click-id="fixd_unfilter_btn">Show comment</button></div>`; c_node.insertAdjacentHTML('beforeend', tpl); } }; function 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 (ftr.enabled) { if (ftr.options.comments.enabled) { init_comments(document); document.body.addEventListener('click', handle_body_click, false); Lb_Comments_Obs.extend((self, node) => { init_comments(node); }); Comment_Obs.extend((self, node) => { filter_comment(node); }); } if (ftr.options.subreddits.enabled || ftr.options.users.enabled) { // document ready: init_posts(document); Content_Obs.extend((self, node) => { init_posts(self.target); }); Post_Obs.extend((self, node) => { if (node.classList.contains('Post')) { node = node.parentNode.parentNode; } filter_post(node); }); } } return ftr; })(); FT.subreddit_info = (function () { const ftr = new Feature({ id: "subreddit_info", label: "Subreddit Info Box", enabled: true }); ftr.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 = ftr.options.delay.value[0]; const delay_open = ftr.options.delay.choices[delay_uid][2] || 400; const delay_close = 100; let tmo_open; let tmo_close; function get_popup() { ftr.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 = GetElById('fixd_popup_subreddit').dataset.id; let db_entry = saved; if (ev.target.classList.contains('fixd_active')) { const list = db_entry.filter_content.options.subreddits.value; const rexp = RegExp(name, 'gi'); db_entry.filter_content.options.subreddits.value = list.filter(i => !rexp.test(i)); 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 => { ftr.public.close_popup(); }, false); return box; }; function add_popup(data, ev) { const box = get_popup(); 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) { box.classList.add('fixd_subscriber'); } if (saved.filter_content.enabled && filter_data.enabled) { box.classList.add('fixd_filterable'); if (UT.list_has(filter_data.value, data.name, true)) { filter_btn_classes.push('fixd_active'); } } let document_width = GetEl('html').offsetWidth; let target_offset = $(ev.target).offset().left; let offset_left = target_offset; if ((document_width - target_offset) < (document_width / 2)) { offset_left -= 240; } const subtitle = data.subtitle ? data.subtitle : ''; const description = data.desc ? data.desc : ''; const content = `<div>\ <h2>${data.name}</h2>\ <div class="fixd_popup_created">${data.created}</div>\ <div class="fixd_popup_subs">\ <span class="fixd_popup_subs">${data.subs}</span> Subscribers\ </div>\ <button class="${filter_btn_classes.join(' ')}">Filter</button>\ </div><div>\ <div class="fixd_popup_title">${data.title}</div>\ <div class="fixd_popup_subtitle">${subtitle}</div>\ <div class="fixd_popup_desc">${description}</div></div>`; box.dataset.id = data.name; box.style.top = $(ev.target).offset().top + ev.target.offsetHeight + 'px'; box.style.left = offset_left + 'px'; box.insertAdjacentHTML('beforeend', content); document.body.appendChild(box); UT.show(box); }; function init_popup(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 }; // Stop if mouse pointer exited the target element. if (!tmo_open) return; add_popup(formatted, ev); }, function (error) { console.warn("Error retrieving subreddit info popup.", error); }); }; ftr.public.close_popup = (ev) => { const popup = GetElById('fixd_popup_subreddit'); if (popup) popup.remove(); }; if (ftr.enabled) { 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_open = null; tmo_close = window.setTimeout(() => { ftr.public.close_popup(); }, delay_close); }; a.addEventListener('mouseleave', mouse_out); } } }); } return ftr; })(); FT.comments_collapse = (function () { const ftr = new Feature({ id: "comments_collapse", label: "Collapsible Child Comments", enabled: true }); ftr.add_option({ id: 'auto', type: 'bool', label: 'Automatically collapse children', enabled: false }); const auto_on = ftr.options.auto.enabled; function init(region, is_mutate) { let lb = GetElById('overlayScrollContainer'); if (!lb && UT.page_type('profile')) return; let comments = region.getElementsByClassName('Comment'); if (comments.length) { let comments_list = UT.nth_parent(comments[0], 4).children; const sort_picker = GetElById('CommentSort--SortPicker').parentNode; const btn_all_classList = ['fixd_collapse_all']; if (auto_on && !is_mutate) { btn_all_classList.push('fixd_active'); } const btn_all = `<button class="${btn_all_classList.join(' ')}">children</button>`; if (sort_picker) { sort_picker.insertAdjacentHTML('beforeend', btn_all); } for (let i = 0; i < comments_list.length; i++) { init_comment(comments_list[i], is_mutate); } } }; function init_comment(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_hidden = !!item.getElementsByClassName('icon-expand').length; if (is_comment && !is_child) { item.classList.add('fixd_top-level'); const next = item.nextElementSibling; const next_is_child = next && !next.getElementsByClassName('top-level').length; const next_is_thread = next && !!next.getElementsByClassName('threadline').length; if (next_is_child && next_is_thread && !is_hidden && !item.querySelector('.fixd_collapse')) { const btn_classList = ['fixd_collapse']; if (auto_on && !is_mutate) { btn_classList.push('fixd_active'); } const btn = `<button class="${btn_classList.join(' ')}">children</button>`; let target = [...item.getElementsByTagName('button')].pop(); if (!target) target = [...item.getElementsByTagName('a')].pop(); if (target) target.insertAdjacentHTML('afterend', btn); } } if (auto_on && is_child && !is_mutate) { item.classList.add('fixd_hidden'); } }; function toggle_visibility(clicked) { let wrap = $(clicked).parents('.fixd_comment_wrap')[0]; let is_active = clicked.classList.contains('fixd_active'); function check_next(node) { const node_is_child = !node.getElementsByClassName('top-level').length; const node_is_thread = !!node.getElementsByClassName('threadline').length; if (node_is_child && node_is_thread) { if (is_active) { node.classList.remove('fixd_hidden'); } else { node.classList.add('fixd_hidden'); } if (node.nextElementSibling) { check_next(node.nextElementSibling); } } }; const next = wrap.nextElementSibling; if (next) check_next(next); clicked.classList.toggle('fixd_active'); }; function handle_click(ev) { if (ev.target.classList.contains('fixd_collapse')) { ev.stopImmediatePropagation(); toggle_visibility(ev.target); } else if (ev.target.classList.contains('fixd_collapse_all')) { ev.stopImmediatePropagation(); if (ev.target.classList.contains('fixd_active')) { GetEls('.fixd_collapse.fixd_active').forEach(e => e.click()); } else { GetEls('.fixd_collapse:not(.fixd_active)').forEach(e => e.click()); } if (GetEls('.fixd_collapse:not(.fixd_active)').length) { ev.target.classList.remove('fixd_active'); } else { ev.target.classList.add('fixd_active'); } } }; if (ftr.enabled) { init(document, false); document.body.addEventListener('click', handle_click, false); Lb_Obs.extend((self, node) => { init(node, true); }); Lb_Comments_Obs.extend((self, node) => { init(node, true); }); Comment_Obs.extend((self, node, mutation) => { init_comment(mutation.previousSibling, true); }); } return ftr; })(); FT.menu_hover = (function () { const ftr = new Feature({ id: "menu_hover", label: "Hover to open menus", enabled: false }); ftr.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'] }); ftr.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 = ftr.options.menus.value; const delay_uid = ftr.options.delay.value[0]; const delay_open = ftr.options.delay.choices[delay_uid][2] || 700; if (ftr.enabled && ftr.options.menus.enabled) { function init() { for (let i = 0; i < menus_val.length; i++) { const uid = menus_val[i]; if (uid !== null) { add_menu_listener(ftr.options.menus.choices[uid]); } } }; function add_menu_listener(menu) { let commit_timeout; const name = menu[0]; let el = GetEl(menu[2]); if (name === 'sortpicker') { el = el ? el.parentNode : undefined; } if (!el) return; el.addEventListener('mouseenter', (ev) => { const target = ev.currentTarget; commit_timeout = window.setTimeout(() => { target.click(); }, delay_open); }, false); el.addEventListener('mouseleave', () => { window.clearTimeout(commit_timeout); }, false); }; init(); if (menus_val.includes('2')) { Lb_Comments_Obs.extend((self, node) => { add_menu_listener(ftr.options.menus.choices[2]); }); } } return ftr; })(); FT.remindme = (function () { const ftr = new Feature({ id: "remindme", label: "Remind Me", enabled: false, hidden: true }); return ftr; })(); FT.debug = (function () { const ftr = new Feature({ id: "debug", label: "Debug Fixdit", enabled: false }); function init(enabled) { if (enabled) { document.body.classList.add('fixd_debug'); } else { document.body.classList.remove('fixd_debug'); } }; ftr.on_toggle = init; init(ftr.enabled); return ftr; })(); document.body.insertAdjacentHTML('beforeend', `<div id="fixd_launch"></div>`); }); })(window.jQuery.noConflict(true));