blob: 538e576fd8020196c9aed25f244f78b77c48c1e1 [file] [log] [blame]
avm9996311707032021-02-05 19:11:25 +01001var mutationObserver, intersectionObserver, intersectionOptions, options,
2 authuser;
avm99963cbea3142019-03-28 00:48:15 +01003
avm99963f5923962020-12-07 16:44:37 +01004function removeChildNodes(node) {
5 while (node.firstChild) {
6 node.removeChild(node.firstChild);
avm99963af7860e2019-06-04 03:33:26 +02007 }
avm99963f5923962020-12-07 16:44:37 +01008}
avm99963af7860e2019-06-04 03:33:26 +02009
avm9996390cc2e32021-02-05 18:14:16 +010010function getNParent(node, n) {
11 if (n <= 0) return node;
12 if (!('parentNode' in node)) return null;
13 return getNParent(node.parentNode, n - 1);
14}
15
avm99963f5923962020-12-07 16:44:37 +010016function createExtBadge() {
17 var badge = document.createElement('div');
18 badge.classList.add('TWPT-badge');
19 badge.setAttribute(
20 'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
21 chrome.i18n.getMessage('appName')
22 ]));
23
24 var badgeI = document.createElement('i');
25 badgeI.classList.add('material-icon-i', 'material-icons-extended');
26 badgeI.textContent = 'repeat';
27
28 badge.append(badgeI);
29 return badge;
avm99963af7860e2019-06-04 03:33:26 +020030}
31
avm99963943b8492020-08-31 23:40:43 +020032function addProfileHistoryLink(node, type, query) {
33 var urlpart = encodeURIComponent('query=' + query);
avm99963a2945b62020-11-27 00:32:02 +010034 var authuserpart =
35 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
avm99963943b8492020-08-31 23:40:43 +020036 var container = document.createElement('div');
37 container.style.margin = '3px 0';
38
39 var link = document.createElement('a');
40 link.setAttribute(
avm99963a2945b62020-11-27 00:32:02 +010041 'href',
42 'https://support.google.com/s/community/search/' + urlpart +
43 authuserpart);
avm99963943b8492020-08-31 23:40:43 +020044 link.innerText = chrome.i18n.getMessage('inject_previousposts_' + type);
45
46 container.appendChild(link);
avm9996306167752020-09-08 00:50:36 +020047 node.appendChild(container);
avm99963943b8492020-08-31 23:40:43 +020048}
49
avm999638e0c1002020-12-03 16:54:20 +010050function applyDragAndDropFix(node) {
51 console.debug('Adding link drag&drop fix to ', node);
52 node.addEventListener('drop', e => {
53 if (e.dataTransfer.types.includes('text/uri-list')) {
54 e.stopImmediatePropagation();
55 console.debug('Stopping link drop event propagation.');
56 }
57 }, true);
58}
59
avm99963f5923962020-12-07 16:44:37 +010060function nodeIsReadToggleBtn(node) {
61 return ('tagName' in node) && node.tagName == 'MATERIAL-BUTTON' &&
62 node.getAttribute('debugid') !== null &&
63 (node.getAttribute('debugid') == 'mark-read-button' ||
64 node.getAttribute('debugid') == 'mark-unread-button') &&
65 ('parentNode' in node) && node.parentNode !== null &&
66 ('parentNode' in node.parentNode) &&
67 node.parentNode.querySelector('[debugid="batchlock"]') === null &&
68 node.parentNode.parentNode !== null &&
69 ('tagName' in node.parentNode.parentNode) &&
70 node.parentNode.parentNode.tagName == 'EC-BULK-ACTIONS';
71}
72
avm9996328fddc62021-02-05 20:33:48 +010073function injectDarkModeButton(rightControl) {
74 var darkThemeSwitch = document.createElement('material-button');
75 darkThemeSwitch.classList.add('TWPT-dark-theme', 'TWPT-btn--with-badge');
76 darkThemeSwitch.setAttribute('button', '');
77 darkThemeSwitch.setAttribute(
78 'title', chrome.i18n.getMessage('inject_ccdarktheme_helper'));
79
80 darkThemeSwitch.addEventListener('click', e => {
81 chrome.storage.sync.get(null, currentOptions => {
82 currentOptions.ccdarktheme_switch_status =
83 !options.ccdarktheme_switch_status;
84 chrome.storage.sync.set(currentOptions, _ => {
85 location.reload();
86 });
87 });
88 });
89
90 var switchContent = document.createElement('div');
91 switchContent.classList.add('content');
92
93 var icon = document.createElement('material-icon');
94
95 var i = document.createElement('i');
96 i.classList.add('material-icon-i', 'material-icons-extended');
97 i.textContent = 'brightness_4';
98
99 icon.appendChild(i);
100 switchContent.appendChild(icon);
101 darkThemeSwitch.appendChild(switchContent);
102
103 var badgeContent = createExtBadge();
104
105 darkThemeSwitch.appendChild(badgeContent);
106
107 rightControl.style.width =
108 (parseInt(window.getComputedStyle(rightControl).width) + 58) + 'px';
109 rightControl.insertAdjacentElement('afterbegin', darkThemeSwitch);
110}
111
avm99963f5923962020-12-07 16:44:37 +0100112function addBatchLockBtn(readToggle) {
113 var clone = readToggle.cloneNode(true);
114 clone.setAttribute('debugid', 'batchlock');
115 clone.classList.add('TWPT-btn--with-badge');
116 clone.setAttribute('title', chrome.i18n.getMessage('inject_lockbtn'));
117 clone.querySelector('material-icon').setAttribute('icon', 'lock');
118 clone.querySelector('i.material-icon-i').textContent = 'lock';
119
120 var badge = createExtBadge();
121 clone.append(badge);
122
123 clone.addEventListener('click', function() {
124 var modal = document.querySelector('.pane[pane-id="default-1"]');
125
126 var dialog = document.createElement('material-dialog');
127 dialog.setAttribute('role', 'dialog');
128 dialog.setAttribute('aria-modal', 'true');
129 dialog.classList.add('TWPT-dialog');
130
131 var header = document.createElement('header');
132 header.setAttribute('role', 'presentation');
133 header.classList.add('TWPT-dialog-header');
134
135 var title = document.createElement('div');
136 title.classList.add('TWPT-dialog-header--title', 'title');
137 title.textContent = chrome.i18n.getMessage('inject_lockbtn');
138
139 header.append(title);
140
141 var main = document.createElement('main');
142 main.setAttribute('role', 'presentation');
143 main.classList.add('TWPT-dialog-main');
144
145 var p = document.createElement('p');
146 p.textContent = chrome.i18n.getMessage('inject_lockdialog_desc');
147
148 main.append(p);
149
150 dialog.append(header, main);
151
152 var footers = [['lock', 'unlock', 'cancel'], ['reload', 'close']];
153
154 for (var i = 0; i < footers.length; ++i) {
155 var footer = document.createElement('footer');
156 footer.setAttribute('role', 'presentation');
157 footer.classList.add('TWPT-dialog-footer');
158 footer.setAttribute('data-footer-id', i);
159
160 if (i > 0) footer.classList.add('is-hidden');
161
162 footers[i].forEach(action => {
163 var btn = document.createElement('material-button');
164 btn.setAttribute('role', 'button');
165 btn.classList.add('TWPT-dialog-footer-btn');
166 if (i == 1) btn.classList.add('is-disabled');
167
168 switch (action) {
169 case 'lock':
170 case 'unlock':
171 btn.addEventListener('click', _ => {
172 if (btn.classList.contains('is-disabled')) return;
173 var message = {
174 action,
175 prefix: 'TWPT-batchlock',
176 };
177 window.postMessage(message, '*');
178 });
179 break;
180
181 case 'cancel':
182 case 'close':
183 btn.addEventListener('click', _ => {
184 if (btn.classList.contains('is-disabled')) return;
185 modal.classList.remove('visible');
186 modal.style.display = 'none';
187 removeChildNodes(modal);
188 });
189 break;
190
191 case 'reload':
192 btn.addEventListener('click', _ => {
193 if (btn.classList.contains('is-disabled')) return;
194 window.location.reload()
195 });
196 break;
197 }
198
199 var content = document.createElement('div');
200 content.classList.add('content', 'TWPT-dialog-footer-btn--content');
201 content.textContent =
202 chrome.i18n.getMessage('inject_lockdialog_btn_' + action);
203
204 btn.append(content);
205 footer.append(btn);
206 });
207
208 var clear = document.createElement('div');
209 clear.style.clear = 'both';
210
211 footer.append(clear);
212 dialog.append(footer);
213 }
214
215 removeChildNodes(modal);
216 modal.append(dialog);
217 modal.classList.add('visible', 'modal');
218 modal.style.display = 'flex';
219 });
220 readToggle.parentNode.insertBefore(
221 clone, (readToggle.nextSibling || readToggle));
222}
223
avm9996390cc2e32021-02-05 18:14:16 +0100224function injectPreviousPostsLinks(nameElement) {
225 var mainCardContent = getNParent(nameElement, 3);
226 if (mainCardContent === null) {
227 console.error(
228 '[previousposts] Couldn\'t find |.main-card-content| element.');
229 return;
avm99963490114d2021-02-05 16:12:20 +0100230 }
avm9996390cc2e32021-02-05 18:14:16 +0100231
232 var forumId = location.href.split('/forum/')[1].split('/')[0] || '0';
233
234 var name = escapeUsername(nameElement.textContent);
235 var query1 = encodeURIComponent(
236 '(creator:"' + name + '" | replier:"' + name + '") forum:' + forumId);
237 var query2 = encodeURIComponent(
238 '(creator:"' + name + '" | replier:"' + name + '") forum:any');
239
240 var container = document.createElement('div');
241 container.classList.add('TWPT-previous-posts');
242
243 var badge = createExtBadge();
244 container.appendChild(badge);
245
246 var linkContainer = document.createElement('div');
247 linkContainer.classList.add('TWPT-previous-posts--links');
248
249 addProfileHistoryLink(linkContainer, 'forum', query1);
250 addProfileHistoryLink(linkContainer, 'all', query2);
251
252 container.appendChild(linkContainer);
253
254 mainCardContent.appendChild(container);
avm99963490114d2021-02-05 16:12:20 +0100255}
256
257const watchedNodesSelectors = [
avm9996328fddc62021-02-05 20:33:48 +0100258 // App container (used to set up the intersection observer and inject the dark
259 // mode button)
avm9996311707032021-02-05 19:11:25 +0100260 'ec-app',
261
avm99963490114d2021-02-05 16:12:20 +0100262 // Load more bar (for the "load more"/"load all" buttons)
263 '.load-more-bar',
264
avm9996390cc2e32021-02-05 18:14:16 +0100265 // Username span inside ec-user (user profile view)
266 'ec-user .main-card .header > .name > span',
avm99963490114d2021-02-05 16:12:20 +0100267
268 // Rich text editor
269 'ec-movable-dialog',
270 'ec-rich-text-editor',
271
272 // Read/unread bulk action in the list of thread, for the batch lock feature
273 'ec-bulk-actions material-button[debugid="mark-read-button"]',
274 'ec-bulk-actions material-button[debugid="mark-unread-button"]',
275];
276
277function handleCandidateNode(node) {
278 if (typeof node.classList !== 'undefined') {
avm9996328fddc62021-02-05 20:33:48 +0100279 if (('tagName' in node) && node.tagName == 'EC-APP') {
280 // Set up the intersectionObserver
281 if (typeof intersectionObserver === 'undefined') {
282 var scrollableContent = node.querySelector('.scrollable-content');
283 if (scrollableContent !== null) {
284 intersectionOptions = {
285 root: scrollableContent,
286 rootMargin: '0px',
287 threshold: 1.0,
288 };
avm9996311707032021-02-05 19:11:25 +0100289
avm9996328fddc62021-02-05 20:33:48 +0100290 intersectionObserver = new IntersectionObserver(
291 intersectionCallback, intersectionOptions);
292 }
293 }
294
295 // Inject the dark mode button
296 if (options.ccdarktheme && options.ccdarktheme_mode == 'switch') {
297 var rightControl = node.querySelector('header .right-control');
298 if (rightControl !== null) injectDarkModeButton(rightControl);
avm99963d24e2db2021-02-05 20:03:55 +0100299 }
avm99963490114d2021-02-05 16:12:20 +0100300 }
301
avm9996311707032021-02-05 19:11:25 +0100302 // Start the intersectionObserver for the "load more"/"load all" buttons
303 // inside a thread
304 if ((options.thread || options.threadall) &&
305 node.classList.contains('load-more-bar')) {
306 if (typeof intersectionObserver !== 'undefined') {
307 if (options.thread)
308 intersectionObserver.observe(node.querySelector('.load-more-button'));
309 if (options.threadall)
310 intersectionObserver.observe(node.querySelector('.load-all-button'));
311 } else {
312 console.warn(
313 '[infinitescroll] ' +
314 'The intersectionObserver is not ready yet.');
315 }
avm99963490114d2021-02-05 16:12:20 +0100316 }
317
318 // Show the "previous posts" links
319 // Here we're selecting the 'ec-user > div' element (unique child)
avm9996390cc2e32021-02-05 18:14:16 +0100320 if (options.history &&
321 node.matches('ec-user .main-card .header > .name > span')) {
avm99963490114d2021-02-05 16:12:20 +0100322 injectPreviousPostsLinks(node);
323 }
324
325 // Fix the drag&drop issue with the rich text editor
326 //
327 // We target both tags because in different contexts different
328 // elements containing the text editor get added to the DOM structure.
329 // Sometimes it's a EC-MOVABLE-DIALOG which already contains the
330 // EC-RICH-TEXT-EDITOR, and sometimes it's the EC-RICH-TEXT-EDITOR
331 // directly.
332 if (options.ccdragndropfix && ('tagName' in node) &&
333 (node.tagName == 'EC-MOVABLE-DIALOG' ||
334 node.tagName == 'EC-RICH-TEXT-EDITOR')) {
335 applyDragAndDropFix(node);
336 }
337
338 // Inject the batch lock button in the thread list
339 if (options.batchlock && nodeIsReadToggleBtn(node)) {
340 addBatchLockBtn(node);
341 }
342 }
343}
344
avm99963847ee632019-03-27 00:57:44 +0100345function mutationCallback(mutationList, observer) {
346 mutationList.forEach((mutation) => {
avm99963b69eb3d2020-08-20 02:03:44 +0200347 if (mutation.type == 'childList') {
348 mutation.addedNodes.forEach(function(node) {
avm99963490114d2021-02-05 16:12:20 +0100349 handleCandidateNode(node);
avm99963847ee632019-03-27 00:57:44 +0100350 });
351 }
352 });
353}
354
avm99963adf90862020-04-12 13:27:45 +0200355function intersectionCallback(entries, observer) {
avm99963847ee632019-03-27 00:57:44 +0100356 entries.forEach(entry => {
357 if (entry.isIntersecting) {
358 entry.target.click();
359 }
360 });
361};
362
363var observerOptions = {
364 childList: true,
avm99963b69eb3d2020-08-20 02:03:44 +0200365 subtree: true,
avm99963129fb502020-08-28 05:18:53 +0200366};
avm99963847ee632019-03-27 00:57:44 +0100367
avm99963129fb502020-08-28 05:18:53 +0200368chrome.storage.sync.get(null, function(items) {
369 options = items;
avm99963cbea3142019-03-28 00:48:15 +0100370
avm99963a2945b62020-11-27 00:32:02 +0100371 var startup =
372 JSON.parse(document.querySelector('html').getAttribute('data-startup'));
373 authuser = startup[2][1] || '0';
374
avm99963490114d2021-02-05 16:12:20 +0100375 // Before starting the mutation Observer, check whether we missed any
avm9996311707032021-02-05 19:11:25 +0100376 // mutations by manually checking whether some watched nodes already
377 // exist.
avm99963490114d2021-02-05 16:12:20 +0100378 var cssSelectors = watchedNodesSelectors.join(',');
379 document.querySelectorAll(cssSelectors)
380 .forEach(node => handleCandidateNode(node));
381
avm99963129fb502020-08-28 05:18:53 +0200382 mutationObserver = new MutationObserver(mutationCallback);
avm99963e4cac402020-12-03 16:10:58 +0100383 mutationObserver.observe(document.body, observerOptions);
avm99963cbea3142019-03-28 00:48:15 +0100384
avm99963129fb502020-08-28 05:18:53 +0200385 if (options.fixedtoolbar) {
386 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200387 'ec-bulk-actions{position: sticky; top: 0; background: var(--TWPT-primary-background, #fff); z-index: 96;}');
avm99963129fb502020-08-28 05:18:53 +0200388 }
avm99963ae6a26d2020-04-12 14:03:51 +0200389
avm99963129fb502020-08-28 05:18:53 +0200390 if (options.increasecontrast) {
avm999630bc113a2020-09-07 13:02:11 +0200391 injectStyles(
avm99963a2a06442020-11-25 21:11:10 +0100392 '.thread-summary.read:not(.checked){background: var(--TWPT-thread-read-background, #ecedee)!important;}');
avm99963129fb502020-08-28 05:18:53 +0200393 }
avm999630f9503f2020-07-27 13:56:52 +0200394
avm99963129fb502020-08-28 05:18:53 +0200395 if (options.stickysidebarheaders) {
396 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200397 'material-drawer .main-header{background: var(--TWPT-drawer-background, #fff)!important; position: sticky; top: 0; z-index: 1;}');
398 }
399
avm99963129942f2020-09-08 02:07:18 +0200400 if (options.ccforcehidedrawer) {
401 var drawer = document.querySelector('material-drawer');
402 if (drawer !== null && drawer.classList.contains('mat-drawer-expanded')) {
403 document.querySelector('.material-drawer-button').click();
404 }
405 }
avm99963f5923962020-12-07 16:44:37 +0100406
407 if (options.batchlock) {
408 injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
409 }
avm99963129fb502020-08-28 05:18:53 +0200410});