Fixdit for Reddit Redesign

UX enhancements for Reddit's 2018 redesign: filters, collapse child comments, subreddit info...

  1. // ==UserScript==
  2. // @name Fixdit for Reddit Redesign
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7.4.2
  5. // @description UX enhancements for Reddit's 2018 redesign: filters, collapse child comments, subreddit info...
  6. // @author scriptpost (u/postpics)
  7. // @match https://www.reddit.com/*
  8. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_listValues
  12. // @grant GM_deleteValue
  13. // @grant GM_openInTab
  14. // @noframes
  15. // ==/UserScript==
  16. (function ($, undefined) {
  17. $(function () {
  18. const GetElById = document.getElementById.bind(document);
  19. const GetElsByClass = document.getElementsByClassName.bind(document);
  20. const GetElsByName = document.getElementsByName.bind(document);
  21. const GetEl = document.querySelector.bind(document);
  22. const GetEls = document.querySelectorAll.bind(document);
  23. const GetStyle = (e, p, v) => window.getComputedStyle.bind(window, [e, p]).getPropertyValue(v);
  24.  
  25. if (!GetElById('SHORTCUT_FOCUSABLE_DIV')) return; // probably using old site.
  26.  
  27. 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>`;
  28. document.body.insertAdjacentHTML('beforeend', _stylesheet);
  29.  
  30. // TODO: (current issues)
  31. // Add observer to hidden comments.
  32. // Observer for switching from comment permalink to all comments.
  33. // Use typescript.
  34.  
  35. // CHANGELOG: (latest)
  36. // Apply 'static header' in overlay window.
  37.  
  38. // Utils:
  39. const UT = {
  40. format_date: {
  41. age(date) {
  42. date = new Date(date).valueOf();
  43. const difference = new Date(new Date().valueOf() - date);
  44. let y = parseInt(difference.toISOString().slice(0, 4), 10) - 1970;
  45. let m = difference.getMonth() + 0;
  46. let d = difference.getDate();
  47. let result;
  48. if (y > 0) result = (y === 1) ? y + ' year' : y + ' years';
  49. else if (m > 0) result = (m === 1) ? m + ' month' : m + ' months';
  50. else result = (d === 1) ? d + ' day' : d + ' days';
  51. return result;
  52. }
  53. },
  54. get_post_author_link(node) {
  55. if (!node) return;
  56. const region = node.querySelector('div[data-click-id="body"]');
  57. let links;
  58. if (region) {
  59. links = region.getElementsByTagName('a');
  60. for (const link of links) {
  61. if (!link.innerText.startsWith('u/')) continue;
  62. const href = link.getAttribute('href');
  63. if (href && href.startsWith('/user/')) {
  64. return link;
  65. }
  66. }
  67. }
  68. },
  69. page_type() {
  70. const pages = {
  71. 'profile': () => {
  72. return !!GetElById('profile-nav-menu-tooltip');
  73. },
  74. 'search': () => {
  75. return !!GetElById('search-results-sort');
  76. },
  77. 'listing': () => {
  78. return !!GetElById('ListingSort--SortPicker');
  79. },
  80. 'comments': () => {
  81. return !!GetElById('CommentSort--SortPicker');
  82. }
  83. };
  84. const result = [...arguments].map((arg) => {
  85. return pages[arg]();
  86. });
  87. return result.includes(true);
  88. },
  89. node_text_includes(str, node) {
  90. const child = (node) ? node.firstChild : undefined;
  91. if (child && child.nodeValue) {
  92. return child.nodeValue.includes(str);
  93. }
  94. },
  95. get_els_by_text(str, tag, region) {
  96. const tags = region.getElementsByTagName(tag);
  97. let result = [];
  98. for (let i = 0; i < tags.length; i++) {
  99. if (UT.node_text_includes(str, tags[i])) {
  100. result.push(tags[i]);
  101. }
  102. }
  103. return result;
  104. },
  105. nth_parent(node, n) {
  106. if (!Number.isInteger(n) && n < 1) throw "First argument must be a positive integer";
  107. let parent = node;
  108. if (!parent) return;
  109. for (let i = 0; i < n; i++) {
  110. parent = parent.parentNode;
  111. }
  112. return parent;
  113. },
  114. list_has(list, query, ignore_case) {
  115. if (!query) return false;
  116. if (ignore_case) {
  117. list = list.map(i => i.toUpperCase());
  118. query = query.toUpperCase();
  119. }
  120. return list.includes(query);
  121. },
  122. comment_count(region) {
  123. // TBDL: interpret abbreviated numbers.
  124. const icon = region.querySelector('.icon-comment');
  125. if (icon && icon.nextElementSibling) {
  126. const num = parseInt(icon.nextElementSibling.innerText, 10);
  127. return !isNaN(num) ? num : 0;
  128. } else {
  129. return -1;
  130. }
  131. },
  132. hide(node) {
  133. if (!node) return;
  134. node.style.display = 'none';
  135. },
  136. show(node) {
  137. if (!node) return;
  138. const initial = window.getComputedStyle(node).getPropertyValue('display');
  139. if (initial === 'none') node.style.display = '';
  140. }
  141. };
  142.  
  143. class Feature {
  144. constructor(data) {
  145. const loaded = JSON.parse(GM_getValue('features', '{}'));
  146.  
  147. if (loaded.hasOwnProperty(data.id)) {
  148. data.enabled = loaded[data.id].enabled;
  149. if (loaded[data.id].hasOwnProperty('options')) {
  150. this.loaded_options = loaded[data.id].options;
  151. }
  152. }
  153. else {
  154. const db_entry = loaded;
  155.  
  156. db_entry[data.id] = {
  157. enabled: data.enabled,
  158. options: {}
  159. };
  160.  
  161. GM_setValue('features', JSON.stringify(db_entry));
  162. }
  163.  
  164. for (const key in data) {
  165. this[key] = data[key];
  166. }
  167. if (!data.hidden) {
  168. draw_setting(data);
  169. }
  170. this.options = {};
  171. this.option_callbacks = {};
  172. this.public = {};
  173. }
  174.  
  175. add_option(option) {
  176. const loaded_options = this.loaded_options;
  177. let is_stored = loaded_options.hasOwnProperty(option.id);
  178. if (is_stored) {
  179. // Modify passed object.
  180. if (option.hasOwnProperty('enabled')) {
  181. option.enabled = loaded_options[option.id].enabled;
  182. }
  183. if (loaded_options[option.id].hasOwnProperty('value')) {
  184. option.value = loaded_options[option.id].value;
  185. }
  186. }
  187. else {
  188. const db_entry = JSON.parse(GM_getValue('features', '{}'));
  189. db_entry[this.id].options[option.id] = {};
  190.  
  191. if (option.hasOwnProperty('value')) {
  192. db_entry[this.id].options[option.id].value = option.value;
  193. }
  194. db_entry[this.id].options[option.id].enabled = option.enabled;
  195. GM_setValue('features', JSON.stringify(db_entry));
  196. }
  197. this.options[option.id] = new Option(option);
  198. // Delete temporary object when we're finished with it:
  199. if (this.loaded_options.hasOwnProperty(option.id)) {
  200. delete this.loaded_options[option.id];
  201. }
  202. }
  203.  
  204. toggle() {
  205. if (this.callback) this.callback(this.enabled);
  206. const callbacks = this.option_callbacks;
  207. // Go through each option cb, sending the value of feature.enabled.
  208. for (const key in callbacks) {
  209. callbacks[key](this.enabled);
  210. }
  211. }
  212.  
  213. set on_toggle(fn) {
  214. this.callback = fn;
  215. }
  216.  
  217. set on_toggle_option(functions) {
  218. this.option_callbacks = functions;
  219. }
  220.  
  221. update_nodes(nodes) {
  222. nodes.forEach(n => {
  223. n.classList.toggle('fixd_enabled');
  224. });
  225. }
  226.  
  227. update(data, nodes) {
  228. this.enabled = data;
  229. this.toggle();
  230. if (nodes) this.update_nodes(nodes);
  231. }
  232.  
  233. update_option(data, oid, nodes) {
  234. this.options[oid].enabled = data;
  235. const callbacks = this.option_callbacks;
  236. if (this.enabled && callbacks.hasOwnProperty(oid)) {
  237. callbacks[oid](data);
  238. }
  239. if (nodes) this.update_nodes(nodes);
  240. }
  241.  
  242. update_values(oid, data) {
  243. this.options[oid].value = data;
  244. }
  245. }
  246.  
  247. class Option {
  248. constructor(data) {
  249. for (const key in data) {
  250. this[key] = data[key];
  251. }
  252. }
  253. }
  254.  
  255. class Reddit_Observer {
  256. constructor(arg) {
  257. this.name = arg.name;
  258. this.target = arg.target;
  259. if (arg.options) {
  260. this.options = arg.options;
  261. } else {
  262. this.options = { childList: true };
  263. }
  264. this.actions = [];
  265. this.watch(arg.target);
  266. }
  267. set any(callback) {
  268. this.any_basis = callback;
  269. }
  270. set added(callback) {
  271. this.added_basis = callback;
  272. }
  273. set removed(callback) {
  274. this.removed_basis = callback;
  275. }
  276. get records() {
  277. if (this.observer) {
  278. return this.observer.takeRecords();
  279. }
  280. }
  281. loop_actions(mutation, other) {
  282. for (let i = 0; i < this.actions.length; i++) {
  283. this.actions[i](this, mutation, other);
  284. }
  285. }
  286. extend(fn) {
  287. this.actions.push(fn);
  288. if (this.target) {
  289. this.watch();
  290. }
  291. }
  292. watch(newTarget) {
  293. if (newTarget) {
  294. this.target = newTarget;
  295. }
  296. else if (!this.target) {
  297. return;
  298. }
  299. const self = this;
  300. function mutation(mutations) {
  301. for (let i = 0; i < mutations.length; i++) {
  302. const a = mutations[i].addedNodes;
  303. const r = mutations[i].removedNodes;
  304. if (self.added_basis && a.length && a[0].nodeType === Node.ELEMENT_NODE) {
  305. self.added_basis(self, a[0], mutations[i]);
  306. }
  307. else if (self.removed_basis && r.length && r[0].nodeType === Node.ELEMENT_NODE) {
  308. self.removed_basis(self, r[0], mutations[i]);
  309. }
  310. else if (self.any_basis) {
  311. self.any_basis(self, mutations[i]);
  312. }
  313. }
  314. };
  315. if (this.observer) {
  316. this.observer.disconnect();
  317. }
  318. this.observer = new MutationObserver(mutation);
  319. if (this.target.constructor.name !== 'NodeList') {
  320. this.observer.observe(this.target, this.options);
  321. }
  322. else {
  323. for (const target of this.target) {
  324. this.observer.observe(target, this.options);
  325. }
  326. }
  327. }
  328. }
  329. // Set up pre-defined Observers:
  330. const Body_Obs = new Reddit_Observer({
  331. name: 'Body',
  332. target: document.body
  333. });
  334. const View_Obs = new Reddit_Observer({
  335. name: 'View',
  336. target: GetEls('#view--layout--FUE button'),
  337. options: { attributes: true }
  338. });
  339. const Content_Obs = new Reddit_Observer({
  340. name: 'Content area',
  341. target: GetEl('header').parentNode.nextElementSibling,
  342. options: { childList: true, subtree: true }
  343. });
  344. const Post_Obs = new Reddit_Observer({
  345. name: 'Post',
  346. target: GetEl('header').parentNode.parentNode,
  347. options: { childList: true, subtree: true }
  348. });
  349. const Comment_Obs = new Reddit_Observer({
  350. name: 'Comment',
  351. target: (() => {
  352. const c = GetElsByClass('Comment');
  353. if (c.length) {
  354. return UT.nth_parent(c[0], 4);
  355. }
  356. })()
  357. });
  358. const Lb_Obs = new Reddit_Observer({
  359. name: 'LB',
  360. target: GetElById('SHORTCUT_FOCUSABLE_DIV').children[0]
  361. });
  362. const Lb_Comments_Obs = new Reddit_Observer({
  363. name: 'LB comments',
  364. target: GetElById('overlayScrollContainer'),
  365. options: { childList: true, subtree: true }
  366. });
  367. const Lb_TT_Obs = new Reddit_Observer({
  368. name: 'L-box tooltip',
  369. target: GetElById('overlayAbsoluteTooltipContent')
  370. })
  371. const Side_Obs = new Reddit_Observer({
  372. name: 'Side',
  373. target: GetElsByClass('fixd--side')[0],
  374. options: { childList: true, subtree: true }
  375. });
  376. // Handle each mutation:
  377. function added_to_body(self, node) {
  378. self.loop_actions(node);
  379. };
  380. Body_Obs.added = added_to_body;
  381.  
  382. function changed_view(self, mutation) {
  383. if (mutation.attributeName === 'aria-pressed') {
  384. const isActive = JSON.parse(mutation.target.attributes['aria-pressed'].value);
  385. if (isActive) {
  386. document.body.dataset.fixdView = mutation.target.attributes['aria-label'].value;
  387. }
  388. }
  389. }
  390. View_Obs.any = changed_view;
  391.  
  392. // TBDL: more testing needed to reduce overhead.
  393. function added_to_list_area(self, node) {
  394. if (node.classList.contains('Post')) return;
  395. // TODO: shorten statement:
  396. if (node.children[0] && node.children[0].children[0] && node.children[0].children[0].classList.contains('Post')) return;
  397.  
  398. if (node.getElementsByClassName('Post').length) {
  399. Side_Obs.watch(GetEl('.fixd--side'));
  400. Post_Obs.watch();
  401. View_Obs.watch(GetEls('#view--layout--FUE button'));
  402. self.loop_actions(node);
  403. }
  404. };
  405. Content_Obs.added = added_to_list_area;
  406.  
  407. function added_to_post_list(self, node) {
  408. if (node.querySelector('.Post') || node.classList.contains('Post')) {
  409. self.loop_actions(node);
  410. }
  411. };
  412. Post_Obs.added = added_to_post_list;
  413.  
  414. function added_to_comment_list(self, node, mutation) {
  415. if (node.getElementsByClassName('Comment')[0]) {
  416. self.loop_actions(node, mutation);
  417. }
  418. };
  419. Comment_Obs.added = added_to_comment_list;
  420.  
  421. function add_remove_lightbox(self, mutation) {
  422. if (mutation.addedNodes.length) {
  423. const node = mutation.addedNodes[0];
  424. const lb = GetElById('overlayScrollContainer');
  425. const com_count = UT.comment_count(node);
  426. const comments = node.getElementsByClassName('Comment');
  427. if (com_count > -1 && comments.length > 0) {
  428. // Comments found, and watch for new comments.
  429. const list = UT.nth_parent(comments[0], 4);
  430. Comment_Obs.watch(list);
  431. Lb_Comments_Obs.loop_actions(list); // Run actions of other observer.
  432. } else if (!comments.length) {
  433. // watch for preloaded comments.
  434. Lb_Comments_Obs.watch(lb);
  435. }
  436. const side = lb.children[0].children[1];
  437. if (side) {
  438. Side_Obs.loop_actions(side);
  439. self.loop_actions(lb);
  440. } else {
  441. // watch for side
  442. Side_Obs.watch(lb);
  443. }
  444. Lb_TT_Obs.watch(GetElById('overlayAbsoluteTooltipContent'));
  445. } else {
  446. Lb_Obs.watch();
  447. }
  448. };
  449. Lb_Obs.any = add_remove_lightbox;
  450.  
  451. function added_preloaded_comments(self, node, mutation) {
  452. const comments = self.target.getElementsByClassName('Comment');
  453. if (comments.length > 0) {
  454. const list = UT.nth_parent(comments[0], 4);
  455. if (list.parentNode === node) {
  456. // Freshly loaded:
  457. Comment_Obs.watch(list);
  458. self.loop_actions(list);
  459. }
  460. }
  461. };
  462. Lb_Comments_Obs.added = added_preloaded_comments;
  463.  
  464. function added_lb_tooltip(self, node, mutation) {
  465. self.loop_actions(node, mutation);
  466. };
  467. Lb_TT_Obs.added = added_lb_tooltip;
  468.  
  469. function added_to_side(self, node, mutation) {
  470. const side = self.target.children[0].children[1];
  471. if (side.parentNode === node) {
  472. Lb_Obs.loop_actions(self.target);
  473. }
  474. };
  475. Side_Obs.added = added_to_side;
  476.  
  477. function get_reddit_data(kind, name) {
  478. return new Promise(function (resolve, reject) {
  479. let url;
  480. const key = kind + '_' + name;
  481. const cache = JSON.parse(GM_getValue('cache', '{}'));
  482. let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));
  483.  
  484. if (cache[key]) {
  485. resolve(cache[key]);
  486. }
  487. else if (!ratelimit.remaining || ratelimit.remaining > 150) {
  488. const req = new XMLHttpRequest();
  489. url = '/r/' + name + '/about.json';
  490. req.open('GET', url);
  491.  
  492. req.onload = function () {
  493. if (req.status === 200) {
  494. const response = JSON.parse(this.response).data;
  495. let json_data = cache;
  496.  
  497. ratelimit = {
  498. used: this.getResponseHeader('x-ratelimit-used'),
  499. remaining: this.getResponseHeader('x-ratelimit-remaining'),
  500. reset: this.getResponseHeader('x-ratelimit-reset')
  501. };
  502.  
  503. json_data[key] = {
  504. name: response.display_name,
  505. title: response.title,
  506. subtitle: response.header_title,
  507. desc: response.public_description,
  508. created: response.created,
  509. subs: response.subscribers,
  510. subscriber: response.user_is_subscriber
  511. };
  512.  
  513. GM_setValue('ratelimit_get', JSON.stringify(ratelimit));
  514. GM_setValue('cache', JSON.stringify(json_data));
  515. resolve(json_data[key]);
  516. }
  517. else {
  518. reject(Error(req.statusText));
  519. }
  520. };
  521. req.onerror = function () {
  522. reject(Error("Network Error"));
  523. };
  524. req.send();
  525. } else if (ratelimit.remaining) {
  526. reject(ratelimit.remaining + ' requests remaining.');
  527. }
  528. })
  529. };
  530.  
  531. const clear_cache = (() => {
  532. let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));
  533. if (ratelimit.used !== undefined && ratelimit.used <= 1) {
  534. GM_setValue('cache', '{}');
  535. }
  536. })();
  537.  
  538. function draw_options_panel(fid) {
  539. const feature = FT[fid];
  540. const tpl = `<div id="fixd_options" data-id="${fid}" class="fixd_panel">\
  541. <button class="fixd_btn_back">Back</button>\
  542. <h2>${feature.label}</h2></div>`;
  543.  
  544. const classes = ['fixd_switch', 'fixd_setting_switch'];
  545. let checked = '';
  546. if (feature.enabled) {
  547. classes.push('fixd_enabled');
  548. checked = 'checked';
  549. }
  550. const toggle = `<label data-id="${fid}" class="${classes.join(' ')}"> \
  551. <input type="checkbox" ${checked}> On</label>`;
  552.  
  553. UT.hide(GetElById('fixd_settings_menu'));
  554. GetElById('fixd_settings').insertAdjacentHTML('beforeend', tpl);
  555.  
  556. const panel = GetElById('fixd_options');
  557. if (!feature.hidden) {
  558. panel.insertAdjacentHTML('beforeend', toggle);
  559. }
  560. draw_option_items(feature.options, panel);
  561. };
  562. function draw_option_items(options, target) {
  563. for (let oid in options) {
  564. const option = options[oid];
  565. if (option.hidden) continue;
  566. const is_bool = ['bool', undefined].includes(option.type);
  567. const is_on = !option.hasOwnProperty('enabled') || option.enabled;
  568. const classes = ['fixd_option'];
  569. if (is_bool) {
  570. classes.push('fixd_switch', 'fixd_option_switch');
  571. } else {
  572. classes.push('fixd_option_btn');
  573. }
  574. if (is_on) {
  575. classes.push('fixd_enabled');
  576. }
  577. const template = `<label data-id="${option.id}" class="${classes.join(' ')}">\
  578. ${is_bool ? `<input type="checkbox" ${is_on ? `checked` : ``}> ` : ``}${option.label}</label>`;
  579. target.insertAdjacentHTML('beforeend', template);
  580. }
  581. };
  582. function draw_option_choices(data, saved, target) {
  583. for (let uid in data.choices) {
  584. const label = data.choices[uid][1];
  585. const classes = ['fixd_option_select'];
  586. let type = 'radio';
  587. let checked = '';
  588. if (data.type !== 'radio') {
  589. type = 'checkbox';
  590. }
  591. if (saved.value.includes(uid)) {
  592. checked = 'checked';
  593. classes.push('fixd_enabled');
  594. }
  595. const tpl = `<label class="${classes.join(' ')}">\
  596. <input type="${type}" ${checked} name="fixd_option_choices" value="${uid}">\
  597. ${label}</label>`;
  598. target.insertAdjacentHTML('beforeend', tpl);
  599. };
  600. };
  601. function draw_option_dialog(oid) {
  602. const panel = GetElById('fixd_options');
  603. const fid = panel.dataset.id;
  604. const saved = JSON.parse(GM_getValue('features', '{}'));
  605. const option = FT[fid].options[oid];
  606. const saved_option = saved[fid].options[oid];
  607. const type = option.type;
  608. const is_on = saved_option.enabled;
  609. let list_val;
  610.  
  611. if (type === 'list') {
  612. saved_option.value.sort((a, b) => {
  613. return a.localeCompare(b, 'en', { 'sensitivity': 'base' });
  614. });
  615. list_val = saved_option.value.join('\n');
  616. }
  617.  
  618. let toggle = '';
  619. if (option.hasOwnProperty('enabled')) {
  620. const classes = ['fixd_switch', 'fixd_option_switch'];
  621. if (is_on) {
  622. classes.push('fixd_enabled');
  623. }
  624. toggle = `<label class="${classes.join(' ')}" data-id="${oid}">\
  625. <input type="checkbox" ${is_on ? `checked` : ``}> On</label>`;
  626. }
  627.  
  628. const description = option.description ? option.description : '';
  629. const tpl = `\
  630. <div id="fixd_dialog" data-id="${oid}">\
  631. <div class="fixd_settings_header">\
  632. <button class="fixd_btn_back">Back</button>\
  633. <h3>${FT[fid].label} <span>${option.label}</span></h3>\
  634. </div>\
  635. ${toggle}\
  636. <div class="fixd_description">${description}</div>\
  637. <div class="fixd_settings_buttons">\
  638. <button class="fixd_btn_save">Save changes</button></div>\
  639. ${type === 'list' ? `<textarea name="fixd_option_list">${list_val}</textarea>` : ``}\
  640. </div>`;
  641.  
  642. panel.insertAdjacentHTML('beforeend', tpl);
  643. const dialog = GetElById('fixd_dialog');
  644. if (type !== 'list') {
  645. draw_option_choices(option, saved_option, dialog);
  646. }
  647. GetElById('fixd_settings').classList.add('fixd_expanded');
  648. };
  649.  
  650. function close_dialog() {
  651. GetElById('fixd_settings').classList.remove('fixd_expanded');
  652. $('#fixd_dialog').remove();
  653. };
  654.  
  655. function handle_submit() {
  656. const panel = GetElById('fixd_options');
  657. const fid = panel.dataset.id;
  658. const oid = GetElById('fixd_selected_option').dataset.id;
  659. const saved = JSON.parse(GM_getValue('features', '{}'));
  660. const feature = FT[fid];
  661. const option = feature.options[oid];
  662. let db_entry = saved;
  663. if (option.type === 'list') {
  664. let value = GetElsByName('fixd_option_list')[0].value;
  665. value = value.replace(/[^a-zA-Z\d\n#._-]/mg, "");
  666. db_entry[fid].options[oid].value = value.split('\n');
  667. }
  668. else if (option.hasOwnProperty('choices')) {
  669. const choices = GetElsByName('fixd_option_choices');
  670. // Clear the old value before repopulating.
  671. db_entry[fid].options[oid].value = [];
  672. function handle_choices(el, idx) {
  673. if (el.value && el.checked) {
  674. if (option.type === 'radio') {
  675. db_entry[fid].options[oid].value[0] = el.value;
  676. return false;
  677. }
  678. else {
  679. db_entry[fid].options[oid].value[idx] = el.value;
  680. }
  681. }
  682. }
  683. choices.forEach(handle_choices);
  684. }
  685. GM_setValue('features', JSON.stringify(db_entry));
  686. feature.update_values(oid, db_entry[fid].options[oid].value);
  687. close_dialog();
  688. };
  689.  
  690. function handle_back(ev) {
  691. if (GetElById('fixd_dialog')) {
  692. close_dialog();
  693. } else {
  694. $('#fixd_options').remove();
  695. UT.show(GetElById('fixd_settings_menu'));
  696. }
  697. };
  698.  
  699. function close_settings() {
  700. GetEls('#fixd_settings, #fixd_launch').forEach(e => e.classList.remove('fixd_active'));
  701. close_dialog();
  702. };
  703.  
  704. function handle_settings_click(ev) {
  705. const clicked = ev.target;
  706. const parent = clicked.parentNode;
  707. const class_list = clicked.classList;
  708. const has_class = c => class_list ? class_list.contains(c) : false;
  709. const data_id = clicked.dataset.id;
  710. const is_input = clicked.tagName === 'INPUT';
  711. if (has_class('fixd_setting_btn')) {
  712. draw_options_panel(data_id);
  713. }
  714. else if (has_class('fixd_option_btn')) {
  715. let selected = GetElById('fixd_selected_option');
  716. if (selected) selected.removeAttribute('id');
  717. clicked.id = 'fixd_selected_option';
  718. draw_option_dialog(data_id);
  719. }
  720. else if (has_class('fixd_btn_save')) {
  721. handle_submit(ev);
  722. }
  723. else if (has_class('fixd_btn_back')) {
  724. handle_back(ev);
  725. }
  726. else if (is_input && parent.classList.contains('fixd_switch')) {
  727. handle_switch(parent);
  728. }
  729. else if (is_input && parent.classList.contains('fixd_option_select')) {
  730. if (clicked.getAttribute('type') === 'radio') {
  731. let choices = GetElsByName('fixd_option_choices');
  732. choices.forEach(i => i.parentNode.classList.remove('fixd_enabled'));
  733. }
  734. parent.classList.toggle('fixd_enabled');
  735. }
  736. };
  737.  
  738. const insert_settings_form = (() => {
  739. const tpl = `<div id="fixd_settings" data-version="${GM_info.script.version}">
  740. <div id="fixd_settings_menu" class="fixd_panel"><h1>Fixdit Settings</h1></div></div>`;
  741. document.body.insertAdjacentHTML('beforeend', tpl);
  742. const box = GetElById('fixd_settings');
  743. box.addEventListener('click', handle_settings_click, false);
  744. })();
  745.  
  746. function draw_setting(data) {
  747. const classes = ['fixd_option_btn', 'fixd_setting_btn'];
  748.  
  749. if (data.enabled) {
  750. classes.push('fixd_enabled');
  751. }
  752. const template = `<div data-id="${data.id}" class="${classes.join(' ')}">\
  753. ${data.label}</div>`;
  754. GetElById('fixd_settings_menu').insertAdjacentHTML('beforeend', template);
  755. }
  756.  
  757. document.addEventListener('click', ev => {
  758. const path = ev.composedPath();
  759. const launcher = GetElById('fixd_launch');
  760. const settings = GetElById('fixd_settings');
  761. const popup = GetElsByClass('fixd_popup')[0];
  762. if (!path.includes(popup)) {
  763. FT.subreddit_info.public.close_popup();
  764. }
  765. if (!path.includes(launcher) &&
  766. !path.includes(settings)) {
  767. close_settings();
  768. }
  769. if (ev.target === launcher) {
  770. if (ev.target.classList.contains('fixd_active')) {
  771. close_settings();
  772. }
  773. else {
  774. launcher.classList.add('fixd_active');
  775. settings.classList.add('fixd_active');
  776. $('#fixd_options').remove();
  777. UT.show(GetElById('fixd_settings_menu'));
  778. }
  779. }
  780. });
  781. function handle_switch(clicked) {
  782. const saved = JSON.parse(GM_getValue('features', '{}'));
  783. const fid = GetElById('fixd_options').dataset.id;
  784. const feature = FT[fid];
  785. const db_entry = saved;
  786. const nodes = [clicked];
  787.  
  788. let oid;
  789. if (clicked.classList.contains('fixd_option_switch')) {
  790. oid = clicked.dataset.id;
  791. }
  792.  
  793. if (oid) {
  794. if (GetElById('fixd_dialog')) {
  795. nodes.push(GetEl(`.fixd_option_btn[data-id="${oid}"]`));
  796. }
  797. if (saved[fid].options[oid].enabled) {
  798. db_entry[fid].options[oid].enabled = false;
  799. } else {
  800. db_entry[fid].options[oid].enabled = true;
  801. }
  802. feature.update_option(db_entry[fid].options[oid].enabled, oid, nodes);
  803. } else if (fid) {
  804. nodes.push(GetEl(`.fixd_setting_btn[data-id="${fid}"]`));
  805. if (saved[fid].enabled) {
  806. db_entry[fid].enabled = false;
  807. } else {
  808. db_entry[fid].enabled = true;
  809. }
  810. feature.update(db_entry[fid].enabled, nodes);
  811. } else {
  812. return;
  813. }
  814. GM_setValue('features', JSON.stringify(db_entry));
  815. }
  816. // Begin modules/features.
  817. const FT = {};
  818. FT.ui_selectors = (() => {
  819. const ftr = new Feature({
  820. id: "ui_selectors",
  821. label: "Add UI Selectors",
  822. enabled: true,
  823. internal: true,
  824. hidden: true
  825. });
  826. function tag_body() {
  827. const layout_switch_card = GetElById('layoutSwitch--card');
  828.  
  829. if (!layout_switch_card) return;
  830.  
  831. const layout_switches_wrap = layout_switch_card.parentNode;
  832. let view_mode;
  833.  
  834. for (const button of layout_switches_wrap.getElementsByTagName('button')) {
  835. const pressed = JSON.parse(button.getAttribute('aria-pressed'));
  836. if (pressed) {
  837. view_mode = button.getAttribute('aria-label');
  838. }
  839. }
  840.  
  841. document.body.dataset.fixdView = view_mode;
  842.  
  843. const list_area = GetEl('header').parentNode.nextElementSibling;
  844. if (list_area) {
  845. list_area.classList.add('fixd_list_area');
  846. }
  847. }
  848. function tag_subreddits_boxes(arg) {
  849. if (!arg) return;
  850. let region = arg;
  851. let is_profile = UT.page_type('profile');
  852. let lightbox = GetElById('overlayScrollContainer');
  853. for (const el of region.getElementsByTagName('button')) {
  854. if (UT.list_has(['subscribe', 'unsubscribe'], el.innerText, true)) {
  855. const item = el.parentNode.parentNode;
  856. const img = item.getElementsByTagName('img');
  857. const svg = item.getElementsByTagName('svg');
  858. if (img.length === 1 || svg.length === 1) {
  859. let contents = item.parentNode.parentNode.parentNode;
  860. if (is_profile && !lightbox) {
  861. contents = contents.parentNode;
  862. }
  863. const a = item.querySelector('a');
  864. const sib = a.nextElementSibling;
  865. const p = sib && sib.tagName === 'P' ? sib : undefined;
  866. if (p && a.getAttribute('href').startsWith('/r/') && UT.node_text_includes('subscribers', p)) {
  867. let container = contents.parentNode.parentNode;
  868. container.classList.add('fixd--subreddits');
  869. break;
  870. }
  871. }
  872. }
  873. }
  874. region = [...GetElsByClass('fixd--subreddits')].pop();
  875. region = region ? region.nextElementSibling : undefined;
  876. if (!region) return;
  877. let button = region.querySelector('button');
  878. let label = button ? button.innerText.toUpperCase() : undefined;
  879. if (UT.list_has(['subscribe', 'unsubscribe'], label, true)) {
  880. // Repeat
  881. tag_subreddits_boxes(region);
  882. }
  883. };
  884. function tag_content(region, np) {
  885. const posts = document.getElementsByClassName('Post');
  886. if (posts.length) {
  887. // Walk thru parents if np is defined, else take 'region' as content node.
  888. let content = np ? UT.nth_parent(posts[0], np) : region;
  889. content.classList.add('fixd--content');
  890. tag_side(content);
  891. }
  892.  
  893. const overlay = GetElById('overlayScrollContainer');
  894. if (overlay) {
  895. const overlayHead = overlay.parentNode.previousSibling;
  896. if (overlayHead) {
  897. overlayHead.classList.add('fixd_overlay_head');
  898. }
  899. }
  900. };
  901. const tag_side = content => {
  902. const side = content.nextElementSibling;
  903. if (!side) return;
  904. side.classList.add('fixd--side');
  905. const mods_h = UT.get_els_by_text('Moderators', 'h3', side)[0];
  906. if (mods_h) {
  907. mods_h.parentNode.classList.add('fixd--moderators');
  908. }
  909. tag_subreddits_boxes(side);
  910. };
  911.  
  912. // doc loaded:
  913. tag_body();
  914.  
  915. if (UT.page_type('comments')) {
  916. tag_content(document, 3);
  917. }
  918.  
  919. else if (UT.page_type('profile')) {
  920. const images = GetEl('header').parentNode.nextElementSibling.getElementsByTagName('img');
  921. for (const img of images) {
  922. if (img.src && img.src.startsWith('https://www.redditstatic.com/avatars')) {
  923. let a = img.parentNode.parentNode.querySelector('a');
  924. if (a && a.href && a.getAttribute('href').startsWith('/user/')) {
  925. return tag_content(UT.nth_parent(img, 5), 7);
  926. }
  927. }
  928. }
  929. }
  930.  
  931. else if (UT.page_type('listing')) {
  932. tag_content(document, 5);
  933. }
  934.  
  935. Content_Obs.extend((self, node) => {
  936. tag_body();
  937. tag_content(document, 5);
  938. });
  939. Lb_Obs.extend((self, node) => {
  940. tag_content(node.children[0].children[0]);
  941. });
  942. Side_Obs.extend((self, node) => {
  943. tag_subreddits_boxes(self.target);
  944. });
  945. return ftr;
  946. })();
  947. FT.ui_tweaks = (() => {
  948. const ftr = new Feature({
  949. id: "ui_tweaks",
  950. label: "UI Tweaks",
  951. enabled: false
  952. });
  953. ftr.add_option({
  954. id: 'middle_click_posts',
  955. label: 'Middle mouse click behavior',
  956. description: `Choose what happens when you press the middle mouse\
  957. button in the empty space of a post. Might not work for Firefox.`,
  958. enabled: false,
  959. type: 'radio',
  960. choices: {
  961. 1: ['do_nothing', "Do nothing (no scroll)"],
  962. 2: ['view_thread', 'Open comments in new tab'],
  963. 3: ['view_thread_fg', 'Open comments & switch to tab']
  964. },
  965. value: ['1']
  966. });
  967. ftr.add_option({
  968. id: "no_prefix",
  969. label: "No prefixes for subreddits, users",
  970. type: "bool",
  971. enabled: false
  972. });
  973. ftr.add_option({
  974. id: "no_blanks",
  975. label: "All links can open in current tab",
  976. type: "bool",
  977. enabled: false
  978. });
  979. ftr.add_option({
  980. id: "override_vote_icons",
  981. label: "Override custom vote icons",
  982. enabled: false,
  983. type: "bool"
  984. });
  985. ftr.add_option({
  986. id: "static_header",
  987. label: "Make the top bar stationary/static",
  988. enabled: false,
  989. type: "bool"
  990. });
  991. ftr.add_option({
  992. id: "reduce_comment_spacing",
  993. label: "Reduce comment spacing",
  994. enabled: false,
  995. type: 'bool'
  996. });
  997. ftr.add_option({
  998. id: "visited_links",
  999. label: "Different color for visited links",
  1000. enabled: true,
  1001. type: 'bool'
  1002. });
  1003. ftr.add_option({
  1004. id: "expando_hitbox",
  1005. label: "Tighten expando hover area (in compact mode)",
  1006. enabled: true,
  1007. type: 'bool'
  1008. });
  1009.  
  1010. function handle_mousedown(ev) {
  1011. if (ev.button !== 1) return;
  1012. const lb = GetElById('overlayScrollContainer');
  1013. const listing = UT.page_type('listing', 'search');
  1014. if (!lb && listing) {
  1015. let path = ev.composedPath();
  1016. let post = (() => {
  1017. // Check if I clicked inside a post or link.
  1018. for (let i = 0; i < path.length; i++) {
  1019. let n = path[i];
  1020. let has_class = c => n.classList ? n.classList.contains(c) : false;
  1021. if (['A', 'BODY'].includes(n.tagName) || has_class('fixd--side')) break;
  1022. if (has_class('Post')) {
  1023. const link = n.querySelector('a[data-click-id="body"]');
  1024. if (link) {
  1025. return { el: n, url: link.href };
  1026. }
  1027. }
  1028. }
  1029. })();
  1030. if (post) {
  1031. ev.stopImmediatePropagation();
  1032. ev.preventDefault();
  1033. if (ftr.options.middle_click_posts.value[0] !== '1') {
  1034. let bg = false;
  1035. if (ftr.options.middle_click_posts.value[0] === '2') {
  1036. bg = true;
  1037. }
  1038. GM_openInTab(post.url, bg);
  1039. }
  1040. }
  1041. }
  1042. };
  1043.  
  1044. function config_middle_click(feature_on) {
  1045. if (feature_on && ftr.options.middle_click_posts.enabled) {
  1046. document.body.addEventListener('mousedown', handle_mousedown, false);
  1047. } else {
  1048. document.body.removeEventListener('mousedown', handle_mousedown);
  1049. }
  1050. };
  1051.  
  1052. function check_vote_icon(up) {
  1053. const wrap = up.parentNode;
  1054. const down = wrap.querySelector('button[data-click-id="downvote"]');
  1055. const score = wrap.querySelector('div');
  1056.  
  1057. // Customized vote buttons do not have child nodes (until we add one via CSS)
  1058. if (!up.children.length) override_vote_icon(up);
  1059. if (!down.children.length) override_vote_icon(down);
  1060.  
  1061. // remove colour override from score.
  1062. if (up.classList.contains('fixd_no_icon') || down.classList.contains('fixd_no_icon')) {
  1063. wrap.classList.add('fixd_override_vote_icon');
  1064. }
  1065. };
  1066.  
  1067. function init_override_vote_icons(region) {
  1068. if (ftr.options.override_vote_icons.enabled) {
  1069. const selector = 'button[data-click-id="upvote"]';
  1070. region.querySelectorAll(selector).forEach(check_vote_icon);
  1071. }
  1072. };
  1073.  
  1074. function override_vote_icon(el) {
  1075. // Remove style override so we can search the url string for active/inactive
  1076. el.classList.add('fixd_no_icon');
  1077. el.style.background = 'none';
  1078. }
  1079.  
  1080. function strip_prefixes(node) {
  1081. if (ftr.options.no_prefix.enabled) {
  1082. let u = UT.get_post_author_link(node);
  1083. if (u) {
  1084. u.innerText = u.innerText.replace("u/", "");
  1085. }
  1086. let sub_links = node.querySelectorAll('a[data-click-id="subreddit"]');
  1087. for (const a of sub_links) {
  1088. if (!a.children.length) {
  1089. a.innerText = a.innerText.replace("r/", "");
  1090. }
  1091. };
  1092. }
  1093. };
  1094. function remove_blanks(region) {
  1095. if (ftr.options.no_blanks.enabled) {
  1096. const anchors = region.getElementsByTagName('a');
  1097. for (let i = 0; i < anchors.length; i++) {
  1098. if (anchors[i].getAttribute('target')) {
  1099. anchors[i].removeAttribute('target');
  1100. anchors[i].classList.add('fixd_no_blank');
  1101. }
  1102. }
  1103. }
  1104. };
  1105. function static_header(feature_on) {
  1106. if (feature_on && ftr.options.static_header.enabled) {
  1107. const header = GetEl('header');
  1108. document.body.classList.add('fixd_static_header');
  1109. } else {
  1110. const header = GetEl('header');
  1111. document.body.classList.remove('fixd_static_header');
  1112. }
  1113. };
  1114.  
  1115. function reduce_comment_spacing(feature_on) {
  1116. if (feature_on && ftr.options.reduce_comment_spacing.enabled) {
  1117. document.body.classList.add('fixd_reduce_comment_spacing');
  1118. } else {
  1119. document.body.classList.remove('fixd_reduce_comment_spacing');
  1120. }
  1121. }
  1122.  
  1123. function visited_links(feature_on) {
  1124. if (feature_on && ftr.options.visited_links.enabled) {
  1125. document.body.classList.add('fixd_visited_links');
  1126. } else {
  1127. document.body.classList.remove('fixd_visited_links');
  1128. }
  1129. }
  1130.  
  1131. function expando_hitbox(feature_on) {
  1132. if (feature_on && ftr.options.expando_hitbox.enabled) {
  1133. document.body.classList.add('fixd_expando_hitbox');
  1134. } else {
  1135. document.body.classList.remove('fixd_expando_hitbox');
  1136. }
  1137. }
  1138.  
  1139. ftr.on_toggle_option = {
  1140. 'static_header': static_header,
  1141. 'reduce_comment_spacing': reduce_comment_spacing,
  1142. 'visited_links': visited_links,
  1143. 'expando_hitbox': expando_hitbox,
  1144. 'middle_click_posts': config_middle_click
  1145. };
  1146.  
  1147. if (ftr.enabled) {
  1148. config_middle_click(ftr.enabled);
  1149. static_header(ftr.enabled);
  1150. reduce_comment_spacing(ftr.enabled);
  1151. visited_links(ftr.enabled);
  1152. expando_hitbox(ftr.enabled);
  1153. init_override_vote_icons(document.body);
  1154. remove_blanks(document);
  1155. const posts = GetElsByClass('Post');
  1156. for (const post of posts) {
  1157. strip_prefixes(post);
  1158. remove_blanks(post);
  1159. }
  1160. Lb_Obs.extend((self, node) => {
  1161. init_override_vote_icons(node);
  1162. remove_blanks(node);
  1163. });
  1164. Lb_Comments_Obs.extend((self, node) => {
  1165. init_override_vote_icons(node);
  1166. remove_blanks(node);
  1167. });
  1168. Content_Obs.extend((self, node) => {
  1169. const posts = self.target.getElementsByClassName('Post');
  1170. for (const post of posts) {
  1171. strip_prefixes(post);
  1172. remove_blanks(post);
  1173. init_override_vote_icons(post);
  1174. }
  1175. });
  1176. Post_Obs.extend((self, node) => {
  1177. strip_prefixes(node);
  1178. init_override_vote_icons(node);
  1179. remove_blanks(node);
  1180. });
  1181. Comment_Obs.extend((self, node) => {
  1182. init_override_vote_icons(node);
  1183. remove_blanks(node);
  1184. });
  1185. document.body.addEventListener('click', ev => {
  1186. if (['upvote', 'downvote'].includes(ev.target.dataset.clickId)) {
  1187. init_override_vote_icons(ev.target.parentNode);
  1188. }
  1189. });
  1190. }
  1191. return ftr;
  1192. })();
  1193. FT.filter_content = (function () {
  1194. const ftr = new Feature({
  1195. id: "filter_content",
  1196. label: "Filter Content",
  1197. enabled: true
  1198. });
  1199. ftr.add_option({
  1200. id: "subreddits",
  1201. label: "Posts by subreddit",
  1202. type: "list",
  1203. enabled: true,
  1204. description: "One <em>subreddit name</em> per line. No commas or slashes. Ignores search results.",
  1205. value: []
  1206. });
  1207. ftr.add_option({
  1208. id: "users",
  1209. label: "Posts by user",
  1210. description: "One <em>user name</em> per line. No commas or slashes. Ignores search results.",
  1211. type: "list",
  1212. enabled: false,
  1213. value: []
  1214. });
  1215. ftr.add_option({
  1216. id: "comments",
  1217. label: "Comments by user",
  1218. description: "One <em>user name</em> per line. No commas or slashes.",
  1219. type: "list",
  1220. enabled: false,
  1221. value: []
  1222. });
  1223. const blocked_subs = ftr.options.subreddits.value.map(i => i.toUpperCase());
  1224. const blocked_submitters = ftr.options.users.value.map(i => i.toUpperCase());
  1225. const blocked_comments = ftr.options.comments.value.map(i => i.toUpperCase());
  1226.  
  1227. const regex = {
  1228. url_subreddit: /.*\/r\//i,
  1229. url_user: /.*\/user\//i
  1230. };
  1231.  
  1232. function init_posts(region) {
  1233. if (UT.page_type('comments', 'search', 'profile')) return;
  1234. const posts = region.getElementsByClassName('Post');
  1235. for (let i = 0; i < posts.length; i++) {
  1236. filter_post(posts[i].parentNode.parentNode);
  1237. }
  1238. };
  1239.  
  1240. function init_comments(region) {
  1241. const comments = region.getElementsByClassName('Comment');
  1242. for (let i = 0; i < comments.length; i++) {
  1243. filter_comment(UT.nth_parent(comments[i], 3));
  1244. }
  1245. };
  1246.  
  1247. function filter_post(node) {
  1248. let sub_href, user_href;
  1249. if (ftr.options.subreddits.enabled) {
  1250. let sub_a = node.querySelector('a[data-click-id="subreddit"]');
  1251. sub_href = sub_a ? sub_a.getAttribute('href') : undefined;
  1252. }
  1253. if (ftr.options.users.enabled) {
  1254. let user_a = UT.get_post_author_link(node);
  1255. user_href = user_a ? user_a.getAttribute('href') : undefined;
  1256. }
  1257. if (sub_href) {
  1258. let sub_name = sub_href.replace(regex.url_subreddit, "").replace("/", "");
  1259.  
  1260. if (UT.list_has(blocked_subs, sub_name.toUpperCase())) {
  1261. node.classList.add('fixd_hidden');
  1262. }
  1263. }
  1264. if (!node.classList.contains('fixd_hidden') && user_href) {
  1265. let user_name = user_href.replace(regex.url_user, "").replace("/", "");
  1266.  
  1267. if (UT.list_has(blocked_submitters, user_name.toUpperCase())) {
  1268. node.classList.add('fixd_hidden');
  1269. }
  1270. }
  1271. };
  1272.  
  1273. function filter_comment(node) {
  1274. const comment_body = node.querySelector('.Comment').children[1];
  1275. if (!comment_body) return;
  1276.  
  1277. let user_link = comment_body.children[0].querySelector('a');
  1278. user_link = user_link ? user_link.getAttribute('href') : undefined;
  1279. if (!user_link) return false;
  1280.  
  1281. let user_name = user_link.replace(regex.url_user, "");
  1282. user_name = user_name.replace("/", "");
  1283.  
  1284. if (UT.list_has(blocked_comments, user_name.toUpperCase())) {
  1285. node.classList.add('fixd_filtered');
  1286.  
  1287. let cid;
  1288. node.querySelector('.Comment').classList.forEach(str => {
  1289. const match = /^t1_/.exec(str);
  1290. if (match) {
  1291. cid = match.input;
  1292. return false;
  1293. }
  1294. });
  1295.  
  1296. let c_node = GetElById(cid);
  1297. const tpl = `<div class="fixd_filter_msg"><span>${user_name}</span>\
  1298. <em>(Fixdit filtered)</em>\
  1299. <button data-click-id="fixd_unfilter_btn">Show comment</button></div>`;
  1300. c_node.insertAdjacentHTML('beforeend', tpl);
  1301. }
  1302. };
  1303.  
  1304. function handle_body_click(ev) {
  1305. let clicked = ev.target;
  1306. if (clicked.dataset.clickId === 'fixd_unfilter_btn') {
  1307. // TBDL: reconnect comment_change observer for fixd_collapse.
  1308. let comment = $(clicked).parents('.fixd_filtered')[0];
  1309.  
  1310. if (comment.classList.contains('fixd_filtered')) {
  1311. comment.classList.replace('fixd_filtered', 'fixd_unfiltered');
  1312. }
  1313. else if (comment.classList.contains('fixd_unfiltered')) {
  1314. comment.classList.replace('fixd_unfiltered', 'fixd_filtered');
  1315. }
  1316. }
  1317. };
  1318. if (ftr.enabled) {
  1319. if (ftr.options.comments.enabled) {
  1320. init_comments(document);
  1321. document.body.addEventListener('click', handle_body_click, false);
  1322. Lb_Comments_Obs.extend((self, node) => {
  1323. init_comments(node);
  1324. });
  1325. Comment_Obs.extend((self, node) => {
  1326. filter_comment(node);
  1327. });
  1328. }
  1329.  
  1330. if (ftr.options.subreddits.enabled || ftr.options.users.enabled) {
  1331. // document ready:
  1332. init_posts(document);
  1333.  
  1334. Content_Obs.extend((self, node) => {
  1335. init_posts(self.target);
  1336. });
  1337.  
  1338. Post_Obs.extend((self, node) => {
  1339. if (node.classList.contains('Post')) {
  1340. node = node.parentNode.parentNode;
  1341. }
  1342. filter_post(node);
  1343. });
  1344. }
  1345. }
  1346. return ftr;
  1347. })();
  1348. FT.subreddit_info = (function () {
  1349. const ftr = new Feature({
  1350. id: "subreddit_info",
  1351. label: "Subreddit Info Box",
  1352. enabled: true
  1353. });
  1354. ftr.add_option({
  1355. id: 'delay',
  1356. label: 'Popup delay',
  1357. choices: {
  1358. 1: ['short', 'Short', 200],
  1359. 2: ['medium', 'Medium', 400],
  1360. 3: ['long', 'Long', 700]
  1361. },
  1362. value: ['2'],
  1363. type: 'radio'
  1364. });
  1365. const delay_uid = ftr.options.delay.value[0];
  1366. const delay_open = ftr.options.delay.choices[delay_uid][2] || 400;
  1367. const delay_close = 100;
  1368. let tmo_open;
  1369. let tmo_close;
  1370.  
  1371. function get_popup() {
  1372. ftr.public.close_popup();
  1373. const box = $('<div>', {
  1374. "id": "fixd_popup_subreddit",
  1375. "class": 'fixd_popup'
  1376. }).css({ 'display': 'none' })[0];
  1377.  
  1378. box.addEventListener('click', ev => {
  1379. if (ev.target.classList.contains('fixd_popup_filter')) {
  1380. const saved = JSON.parse(GM_getValue('features', '{}'));
  1381. const name = GetElById('fixd_popup_subreddit').dataset.id;
  1382. let db_entry = saved;
  1383. if (ev.target.classList.contains('fixd_active')) {
  1384. const list = db_entry.filter_content.options.subreddits.value;
  1385. const rexp = RegExp(name, 'gi');
  1386. db_entry.filter_content.options.subreddits.value = list.filter(i => !rexp.test(i));
  1387. ev.target.classList.remove('fixd_active');
  1388. }
  1389. else {
  1390. db_entry.filter_content.options.subreddits.value.push(name);
  1391. ev.target.classList.add('fixd_active');
  1392. }
  1393. GM_setValue('features', JSON.stringify(db_entry));
  1394. }
  1395. }, false);
  1396.  
  1397. box.addEventListener('mouseover', ev => {
  1398. window.clearTimeout(tmo_close);
  1399. }, false);
  1400.  
  1401. box.addEventListener('mouseleave', ev => {
  1402. ftr.public.close_popup();
  1403. }, false);
  1404. return box;
  1405. };
  1406.  
  1407. function add_popup(data, ev) {
  1408. const box = get_popup();
  1409. const filter_btn_classes = ["fixd_popup_filter"];
  1410. const saved = JSON.parse(GM_getValue('features', '{}'));
  1411. const filter_data = saved.filter_content.options.subreddits;
  1412. if (data.subscriber) {
  1413. box.classList.add('fixd_subscriber');
  1414. }
  1415. if (saved.filter_content.enabled && filter_data.enabled) {
  1416. box.classList.add('fixd_filterable');
  1417. if (UT.list_has(filter_data.value, data.name, true)) {
  1418. filter_btn_classes.push('fixd_active');
  1419. }
  1420. }
  1421. let document_width = GetEl('html').offsetWidth;
  1422. let target_offset = $(ev.target).offset().left;
  1423. let offset_left = target_offset;
  1424. if ((document_width - target_offset) < (document_width / 2)) {
  1425. offset_left -= 240;
  1426. }
  1427. const subtitle = data.subtitle ? data.subtitle : '';
  1428. const description = data.desc ? data.desc : '';
  1429. const content = `<div>\
  1430. <h2>${data.name}</h2>\
  1431. <div class="fixd_popup_created">${data.created}</div>\
  1432. <div class="fixd_popup_subs">\
  1433. <span class="fixd_popup_subs">${data.subs}</span> Subscribers\
  1434. </div>\
  1435. <button class="${filter_btn_classes.join(' ')}">Filter</button>\
  1436. </div><div>\
  1437. <div class="fixd_popup_title">${data.title}</div>\
  1438. <div class="fixd_popup_subtitle">${subtitle}</div>\
  1439. <div class="fixd_popup_desc">${description}</div></div>`;
  1440. box.dataset.id = data.name;
  1441. box.style.top = $(ev.target).offset().top + ev.target.offsetHeight + 'px';
  1442. box.style.left = offset_left + 'px';
  1443. box.insertAdjacentHTML('beforeend', content);
  1444. document.body.appendChild(box);
  1445. UT.show(box);
  1446. };
  1447.  
  1448. function init_popup(ev) {
  1449. let name = ev.target.getAttribute('href').split('/')[2];
  1450. get_reddit_data('t5', name).then((data) => {
  1451. const date_created = new Date(data.created * 1000);
  1452. const formatted = {
  1453. name: data.name,
  1454. title: data.title,
  1455. subtitle: data.subtitle,
  1456. created: UT.format_date.age(date_created),
  1457. subs: data.subs.toLocaleString(),
  1458. subscriber: data.subscriber,
  1459. desc: data.desc
  1460. };
  1461. // Stop if mouse pointer exited the target element.
  1462. if (!tmo_open) return;
  1463.  
  1464. add_popup(formatted, ev);
  1465. }, function (error) {
  1466. console.warn("Error retrieving subreddit info popup.", error);
  1467. });
  1468. };
  1469.  
  1470. ftr.public.close_popup = (ev) => {
  1471. const popup = GetElById('fixd_popup_subreddit');
  1472. if (popup) popup.remove();
  1473. };
  1474.  
  1475. if (ftr.enabled) {
  1476. document.body.addEventListener('mouseover', ev => {
  1477. if (ev.target.tagName === 'A') {
  1478. let a = ev.target;
  1479. if ((a.dataset.clickId === 'subreddit' ||
  1480. $(a).parents('p, .md, .fixd--subreddits').length) &&
  1481. a.getAttribute('href').startsWith('/r/')) {
  1482.  
  1483. window.clearTimeout(tmo_open);
  1484. window.clearTimeout(tmo_close);
  1485.  
  1486. tmo_open = window.setTimeout(() => {
  1487. init_popup(ev);
  1488. }, delay_open);
  1489.  
  1490. let mouse_out = (ev) => {
  1491. ev.target.removeEventListener('mouseleave', mouse_out);
  1492. window.clearTimeout(tmo_open);
  1493. tmo_open = null;
  1494.  
  1495. tmo_close = window.setTimeout(() => {
  1496. ftr.public.close_popup();
  1497. }, delay_close);
  1498. };
  1499. a.addEventListener('mouseleave', mouse_out);
  1500. }
  1501. }
  1502. });
  1503. }
  1504. return ftr;
  1505. })();
  1506. FT.comments_collapse = (function () {
  1507. const ftr = new Feature({
  1508. id: "comments_collapse",
  1509. label: "Collapsible Child Comments",
  1510. enabled: true
  1511. });
  1512. ftr.add_option({
  1513. id: 'auto',
  1514. type: 'bool',
  1515. label: 'Automatically collapse children',
  1516. enabled: false
  1517. });
  1518.  
  1519. const auto_on = ftr.options.auto.enabled;
  1520.  
  1521. function init(region, is_mutate) {
  1522. let lb = GetElById('overlayScrollContainer');
  1523. if (!lb && UT.page_type('profile')) return;
  1524. let comments = region.getElementsByClassName('Comment');
  1525. if (comments.length) {
  1526. let comments_list = UT.nth_parent(comments[0], 4).children;
  1527. const sort_picker = GetElById('CommentSort--SortPicker').parentNode;
  1528. const btn_all_classList = ['fixd_collapse_all'];
  1529. if (auto_on && !is_mutate) {
  1530. btn_all_classList.push('fixd_active');
  1531. }
  1532. const btn_all = `<button class="${btn_all_classList.join(' ')}">children</button>`;
  1533. if (sort_picker) {
  1534. sort_picker.insertAdjacentHTML('beforeend', btn_all);
  1535. }
  1536. for (let i = 0; i < comments_list.length; i++) {
  1537. init_comment(comments_list[i], is_mutate);
  1538. }
  1539. }
  1540. };
  1541.  
  1542. function init_comment(item, is_mutate) {
  1543. item.classList.add('fixd_comment_wrap');
  1544. const is_comment = !!item.getElementsByClassName('Comment').length;
  1545. const is_child = !item.getElementsByClassName('top-level').length;
  1546. const is_hidden = !!item.getElementsByClassName('icon-expand').length;
  1547.  
  1548. if (is_comment && !is_child) {
  1549. item.classList.add('fixd_top-level');
  1550. const next = item.nextElementSibling;
  1551. const next_is_child = next && !next.getElementsByClassName('top-level').length;
  1552. const next_is_thread = next && !!next.getElementsByClassName('threadline').length;
  1553.  
  1554. if (next_is_child && next_is_thread && !is_hidden && !item.querySelector('.fixd_collapse')) {
  1555. const btn_classList = ['fixd_collapse'];
  1556.  
  1557. if (auto_on && !is_mutate) {
  1558. btn_classList.push('fixd_active');
  1559. }
  1560. const btn = `<button class="${btn_classList.join(' ')}">children</button>`;
  1561. let target = [...item.getElementsByTagName('button')].pop();
  1562. if (!target) target = [...item.getElementsByTagName('a')].pop();
  1563. if (target) target.insertAdjacentHTML('afterend', btn);
  1564. }
  1565. }
  1566. if (auto_on && is_child && !is_mutate) {
  1567. item.classList.add('fixd_hidden');
  1568. }
  1569. };
  1570.  
  1571. function toggle_visibility(clicked) {
  1572. let wrap = $(clicked).parents('.fixd_comment_wrap')[0];
  1573. let is_active = clicked.classList.contains('fixd_active');
  1574.  
  1575. function check_next(node) {
  1576. const node_is_child = !node.getElementsByClassName('top-level').length;
  1577. const node_is_thread = !!node.getElementsByClassName('threadline').length;
  1578. if (node_is_child && node_is_thread) {
  1579. if (is_active) {
  1580. node.classList.remove('fixd_hidden');
  1581. } else {
  1582. node.classList.add('fixd_hidden');
  1583. }
  1584. if (node.nextElementSibling) {
  1585. check_next(node.nextElementSibling);
  1586. }
  1587. }
  1588. };
  1589. const next = wrap.nextElementSibling;
  1590. if (next) check_next(next);
  1591. clicked.classList.toggle('fixd_active');
  1592. };
  1593.  
  1594. function handle_click(ev) {
  1595. if (ev.target.classList.contains('fixd_collapse')) {
  1596. ev.stopImmediatePropagation();
  1597. toggle_visibility(ev.target);
  1598. }
  1599. else if (ev.target.classList.contains('fixd_collapse_all')) {
  1600. ev.stopImmediatePropagation();
  1601. if (ev.target.classList.contains('fixd_active')) {
  1602. GetEls('.fixd_collapse.fixd_active').forEach(e => e.click());
  1603. }
  1604. else {
  1605. GetEls('.fixd_collapse:not(.fixd_active)').forEach(e => e.click());
  1606. }
  1607. if (GetEls('.fixd_collapse:not(.fixd_active)').length) {
  1608. ev.target.classList.remove('fixd_active');
  1609. }
  1610. else {
  1611. ev.target.classList.add('fixd_active');
  1612. }
  1613. }
  1614. };
  1615.  
  1616. if (ftr.enabled) {
  1617. init(document, false);
  1618. document.body.addEventListener('click', handle_click, false);
  1619. Lb_Obs.extend((self, node) => {
  1620. init(node, true);
  1621. });
  1622. Lb_Comments_Obs.extend((self, node) => {
  1623. init(node, true);
  1624. });
  1625. Comment_Obs.extend((self, node, mutation) => {
  1626. init_comment(mutation.previousSibling, true);
  1627. });
  1628. }
  1629. return ftr;
  1630. })();
  1631. FT.menu_hover = (function () {
  1632. const ftr = new Feature({
  1633. id: "menu_hover",
  1634. label: "Hover to open menus",
  1635. enabled: false
  1636. });
  1637. ftr.add_option({
  1638. id: 'menus',
  1639. label: "Choose menus",
  1640. enabled: true,
  1641. type: 'checkbox',
  1642. choices: {
  1643. 1: ['sortpicker', 'Sort Posts', '#ListingSort--SortPicker'],
  1644. 2: ['commentsort', 'Comment Sort Picker', '#CommentSort--SortPicker'],
  1645. 3: ['user', 'User dropdown', '#USER_DROPDOWN_ID'],
  1646. 4: ['headermoderate', 'Moderate (header)', '#Header--Moderation']
  1647. },
  1648. value: ['1', '2', '3', '4']
  1649. });
  1650. ftr.add_option({
  1651. id: 'delay',
  1652. label: "Delay",
  1653. requires: "menus",
  1654. type: 'radio',
  1655. choices: {
  1656. 1: ['short', 'Short', 200],
  1657. 2: ['medium', 'Medium', 400],
  1658. 3: ['long', 'Long', 700]
  1659. },
  1660. value: ['2']
  1661. });
  1662. const menus_val = ftr.options.menus.value;
  1663. const delay_uid = ftr.options.delay.value[0];
  1664. const delay_open = ftr.options.delay.choices[delay_uid][2] || 700;
  1665.  
  1666. if (ftr.enabled && ftr.options.menus.enabled) {
  1667. function init() {
  1668. for (let i = 0; i < menus_val.length; i++) {
  1669. const uid = menus_val[i];
  1670. if (uid !== null) {
  1671. add_menu_listener(ftr.options.menus.choices[uid]);
  1672. }
  1673. }
  1674. };
  1675. function add_menu_listener(menu) {
  1676. let commit_timeout;
  1677. const name = menu[0];
  1678. let el = GetEl(menu[2]);
  1679. if (name === 'sortpicker') {
  1680. el = el ? el.parentNode : undefined;
  1681. }
  1682. if (!el) return;
  1683. el.addEventListener('mouseenter', (ev) => {
  1684. const target = ev.currentTarget;
  1685. commit_timeout = window.setTimeout(() => {
  1686. target.click();
  1687. }, delay_open);
  1688. }, false);
  1689. el.addEventListener('mouseleave', () => {
  1690. window.clearTimeout(commit_timeout);
  1691. }, false);
  1692. };
  1693. init();
  1694. if (menus_val.includes('2')) {
  1695. Lb_Comments_Obs.extend((self, node) => {
  1696. add_menu_listener(ftr.options.menus.choices[2]);
  1697. });
  1698. }
  1699. }
  1700. return ftr;
  1701. })();
  1702. FT.remindme = (function () {
  1703. const ftr = new Feature({
  1704. id: "remindme",
  1705. label: "Remind Me",
  1706. enabled: false,
  1707. hidden: true
  1708. });
  1709. return ftr;
  1710. })();
  1711. FT.debug = (function () {
  1712. const ftr = new Feature({
  1713. id: "debug",
  1714. label: "Debug Fixdit",
  1715. enabled: false
  1716. });
  1717. function init(enabled) {
  1718. if (enabled) {
  1719. document.body.classList.add('fixd_debug');
  1720. } else {
  1721. document.body.classList.remove('fixd_debug');
  1722. }
  1723. };
  1724. ftr.on_toggle = init;
  1725. init(ftr.enabled);
  1726. return ftr;
  1727. })();
  1728.  
  1729. document.body.insertAdjacentHTML('beforeend', `<div id="fixd_launch"></div>`);
  1730. });
  1731. })(window.jQuery.noConflict(true));