Fixed more layout bugs
[lw2-viewer.git] / www / head.js
blob0212d8efc396da6cb5059382468d5af09b5aea23
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/);
15         
16         classNames.forEach(className => {
17                 if (!this.hasClass(className))
18                         elementClassNames.push(className);
19         });
20         
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();
30         });
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);
39         else
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)) {
52             case '#':
53                 // Handle ID-based selectors
54                 let element = document.getElementById(selector.substr(1));
55                 return element ? [ element ] : [ ];
56             case '.':
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));
62             default:
63                 // Handle tag-based selectors
64                 return [].slice.call(context.getElementsByTagName(selector));
65         }
66     }
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 /***********************************/
85 GW.widthOptions = [
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;
105     return true;
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;
135                         }
137                         currentNodeInTree = currentNodeInTree[1][indexOfMatchingChild];
138                 });
139         });
141         return tree;
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"
146         ];
147         
148         function selectorFromExclusionTreeNode(node, path = [ ]) {
149                 let [ value, children ] = node;
151                 let newPath = path.clone();
152                 newPath.push(value);
154                 if (!children) {
155                         return value;
156                 } else if (children.length == 0) {
157                         return `${newPath.join(" > ")} > *, ${newPath.join(" > ")}::before, ${newPath.join(" > ")}::after`;
158                 } else {
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(", ");
160                 }
161         }
162         
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})`;
170         }
171         return filterString;
173 function applyFilters(filters) {
174         var fullStyleString = "";
175         
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)}; }`;
179         }
180         
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;
198         
199         if (zoomFactor <= minZoomFactor) {
200                 zoomFactor = minZoomFactor;
201                 queryAll(".text-size-adjust-button.decrease").forEach(function (button) {
202                         button.disabled = true;
203                 });
204         } else if (zoomFactor >= maxZoomFactor) {
205                 zoomFactor = maxZoomFactor;
206                 queryAll(".text-size-adjust-button.increase").forEach(function (button) {
207                         button.disabled = true;
208                 });
209         } else {
210                 queryAll(".text-size-adjust-button").forEach(function (button) {
211                         button.disabled = false;
212                 });
213         }
215         let textZoomStyle = query("#text-zoom");
216         textZoomStyle.innerHTML = 
217                 `.post, .comment, .comment-controls {
218                         zoom: ${zoomFactor};
219                 }`;
221         if (window.generateImagesOverlay) setTimeout(generateImagesOverlay);
223 GW.currentTextZoom = localStorage.getItem('text-zoom');
224 setTextZoom(GW.currentTextZoom);
226 /**********/
227 /* THEMES */
228 /**********/
230 GW.themeOptions = [
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']
242 /*****************/
243 /* ANTI-KIBITZER */
244 /*****************/
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; }` + 
250         "</style>");
252         if(document.location.pathname.match(new RegExp("/posts/.*/comment/"))) {
253                 query("head").insertAdjacentHTML("beforeend", "<"+"title class='fake-title'></title>");
254         }