2 // FOR TESTING ONLY, COMMENT WHEN DEPLOYING.
3 // GW.loggingEnabled = true;
5 /****************************************************/
6 /* CSS CLASS MANIPULATION (polyfill for .classList) */
7 /****************************************************/
9 Element.prototype.addClass = function(className) {
10 if (!this.hasClass(className))
11 this.className = (this.className + " " + className).trim();
13 Element.prototype.addClasses = function(classNames) {
14 let elementClassNames = this.className.trim().split(/\s/);
16 classNames.forEach(className => {
17 if (!this.hasClass(className))
18 elementClassNames.push(className);
21 this.className = elementClassNames.join(" ");
23 Element.prototype.removeClass = function(className) {
24 this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
25 if (this.className == "") this.removeAttribute("class");
27 Element.prototype.removeClasses = function(classNames) {
28 classNames.forEach(className => {
29 this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
31 if (this.className == "") this.removeAttribute("class");
33 Element.prototype.hasClass = function(className) {
34 return (new RegExp("(^|\\s+)" + className + "(\\s+|$)")).test(this.className);
36 Element.prototype.toggleClass = function(className) {
37 if (this.hasClass(className))
38 this.removeClass(className);
40 this.addClass(className);
43 /********************/
44 /* QUERYING THE DOM */
45 /********************/
47 function queryAll(selector, context) {
48 context = context || document;
49 // Redirect simple selectors to the more performant function
50 if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) {
51 switch (selector.charAt(0)) {
53 // Handle ID-based selectors
54 let element = document.getElementById(selector.substr(1));
55 return element ? [ element ] : [ ];
57 // Handle class-based selectors
58 // Query by multiple classes by converting the selector
59 // string into single spaced class names
60 var classes = selector.substr(1).replace(/\./g, ' ');
61 return [].slice.call(context.getElementsByClassName(classes));
63 // Handle tag-based selectors
64 return [].slice.call(context.getElementsByTagName(selector));
67 // Default to `querySelectorAll`
68 return [].slice.call(context.querySelectorAll(selector));
70 function query(selector, context) {
71 let all = queryAll(selector, context);
72 return (all.length > 0) ? all[0] : null;
74 Object.prototype.queryAll = function (selector) {
75 return queryAll(selector, this);
77 Object.prototype.query = function (selector) {
78 return query(selector, this);
81 /***********************************/
82 /* CONTENT COLUMN WIDTH ADJUSTMENT */
83 /***********************************/
86 ['normal', 'Narrow (fixed-width) content column', 'N'],
87 ['wide', 'Wide (fixed-width) content column', 'W'],
88 ['fluid', 'Full-width (fluid) content column', 'F']
91 function setContentWidth(widthOption) {
92 let currentWidth = localStorage.getItem("selected-width") || 'normal';
93 let head = query('head');
94 head.removeClasses(GW.widthOptions.map(wo => 'content-width-' + wo[0]));
95 head.addClass('content-width-' + (widthOption || 'normal'));
97 setContentWidth(localStorage.getItem('selected-width'));
99 /********************************************/
100 /* APPEARANCE CUSTOMIZATION (THEME TWEAKER) */
101 /********************************************/
103 Object.prototype.isEmpty = function () {
104 for (var prop in this) if (this.hasOwnProperty(prop)) return false;
107 Object.prototype.keys = function () {
108 return Object.keys(this);
110 Array.prototype.contains = function (element) {
111 return (this.indexOf(element) !== -1);
113 Array.prototype.clone = function() {
114 return JSON.parse(JSON.stringify(this));
117 GW.themeTweaker = { };
118 GW.themeTweaker.filtersExclusionPaths = { };
119 GW.themeTweaker.defaultFiltersExclusionTree = [ "#content", [ ] ];
121 function exclusionTreeFromExclusionPaths(paths) {
122 if (!paths) return null;
124 let tree = GW.themeTweaker.defaultFiltersExclusionTree.clone();
125 paths.keys().flatMap(key => paths[key]).forEach(path => {
126 var currentNodeInTree = tree;
127 path.split(" ").slice(1).forEach(step => {
128 if (currentNodeInTree[1] == null)
129 currentNodeInTree[1] = [ ];
131 var indexOfMatchingChild = currentNodeInTree[1].findIndex(child => { return child[0] == step; });
132 if (indexOfMatchingChild == -1) {
133 currentNodeInTree[1].push([ step, [ ] ]);
134 indexOfMatchingChild = currentNodeInTree[1].length - 1;
137 currentNodeInTree = currentNodeInTree[1][indexOfMatchingChild];
143 function selectorFromExclusionTree(tree) {
144 var selectorParts = [
145 "body::before, #ui-elements-container > div:not(#theme-tweaker-ui), #theme-tweaker-ui #theme-tweak-section-sample-text .sample-text-container"
148 function selectorFromExclusionTreeNode(node, path = [ ]) {
149 let [ value, children ] = node;
151 let newPath = path.clone();
156 } else if (children.length == 0) {
157 return `${newPath.join(" > ")} > *, ${newPath.join(" > ")}::before, ${newPath.join(" > ")}::after`;
159 return `${newPath.join(" > ")} > *:not(${children.map(child => child[0]).join("):not(")}), ${newPath.join(" > ")}::before, ${newPath.join(" > ")}::after, ` + children.map(child => selectorFromExclusionTreeNode(child, newPath)).join(", ");
163 return selectorParts + ", " + selectorFromExclusionTreeNode(tree);
165 function filterStringFromFilters(filters) {
166 var filterString = "";
167 for (key of Object.keys(filters)) {
168 let value = filters[key];
169 filterString += ` ${key}(${value})`;
173 function applyFilters(filters) {
174 var fullStyleString = "";
176 if (!filters.isEmpty()) {
177 let filtersExclusionTree = exclusionTreeFromExclusionPaths(GW.themeTweaker.filtersExclusionPaths) || GW.themeTweaker.defaultFiltersExclusionTree;
178 fullStyleString = `body::before { content: ""; } body > #content::before { z-index: 0; } ${selectorFromExclusionTree(filtersExclusionTree)} { filter: ${filterStringFromFilters(filters)}; }`;
181 // Update the style tag (if it’s already been loaded).
182 (query("#theme-tweak")||{}).innerHTML = fullStyleString;
184 query("head").insertAdjacentHTML("beforeend", "<style id='theme-tweak'></style>");
185 GW.currentFilters = JSON.parse(localStorage.getItem("theme-tweaks") || "{ }");
186 applyFilters(GW.currentFilters);
188 /************************/
189 /* TEXT SIZE ADJUSTMENT */
190 /************************/
192 query("head").insertAdjacentHTML("beforeend", "<style id='text-zoom'></style>");
193 function setTextZoom(zoomFactor) {
194 if (!zoomFactor) return;
196 let minZoomFactor = 0.5;
197 let maxZoomFactor = 1.5;
199 if (zoomFactor <= minZoomFactor) {
200 zoomFactor = minZoomFactor;
201 queryAll(".text-size-adjust-button.decrease").forEach(function (button) {
202 button.disabled = true;
204 } else if (zoomFactor >= maxZoomFactor) {
205 zoomFactor = maxZoomFactor;
206 queryAll(".text-size-adjust-button.increase").forEach(function (button) {
207 button.disabled = true;
210 queryAll(".text-size-adjust-button").forEach(function (button) {
211 button.disabled = false;
215 let textZoomStyle = query("#text-zoom");
216 textZoomStyle.innerHTML =
217 `.post, .comment, .comment-controls {
221 if (window.generateImagesOverlay) setTimeout(generateImagesOverlay);
223 GW.currentTextZoom = localStorage.getItem('text-zoom');
224 setTextZoom(GW.currentTextZoom);
231 ['default', 'Default theme (dark text on light background)', 'A'],
232 ['dark', 'Dark theme (light text on dark background)', 'B'],
233 ['grey', 'Grey theme (more subdued than default theme)', 'C'],
234 ['ultramodern', 'Ultramodern theme (very hip)', 'D'],
235 ['zero', 'Theme zero (plain and simple)', 'E'],
236 ['brutalist', 'Brutalist theme (the Motherland calls!)', 'F'],
237 ['rts', 'ReadTheSequences.com theme', 'G'],
238 ['classic', 'Classic Less Wrong theme', 'H'],
239 ['less', 'Less theme (serenity now)', 'I']
246 // While everything's being loaded, hide the authors and karma values.
247 if (localStorage.getItem("antikibitzer") == "true") {
248 query("head").insertAdjacentHTML("beforeend", "<style id='antikibitzer-temp'>" +
249 `.author, .inline-author, .karma-value, .individual-thread-page > h1 { visibility: hidden; }` +
252 if(document.location.pathname.match(new RegExp("/posts/.*/comment/"))) {
253 query("head").insertAdjacentHTML("beforeend", "<"+"title class='fake-title'></title>");