catalog-test.js
1 var $ = {}; 2 3 $.id = function(id) { 4 return document.getElementById(id); 5 }; 6 7 $.cls = function(klass, root) { 8 return (root || document).getElementsByClassName(klass); 9 }; 10 11 $.tag = function(tag, root) { 12 return (root || document).getElementsByTagName(tag); 13 }; 14 15 $.extend = function(destination, source) { 16 for (var key in source) { 17 destination[key] = source[key]; 18 } 19 }; 20 21 $.on = function(n, e, h) { 22 n.addEventListener(e, h, false); 23 }; 24 25 $.off = function(n, e, h) { 26 n.removeEventListener(e, h, false); 27 }; 28 29 $.readCookie = function(name) { 30 var i, c, ca, key; 31 32 key = name + '='; 33 ca = document.cookie.split(';'); 34 35 for (i = 0; c = ca[i]; ++i) { 36 while (c.charAt(0) == ' ') { 37 c = c.substring(1, c.length); 38 } 39 if (c.indexOf(key) === 0) { 40 return decodeURIComponent(c.substring(key.length, c.length)); 41 } 42 } 43 return null; 44 }; 45 46 if (!document.documentElement.classList) { 47 $.hasClass = function(el, klass) { 48 return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; 49 }; 50 51 $.addClass = function(el, klass) { 52 el.className = (el.className === '') ? klass : el.className + ' ' + klass; 53 }; 54 55 $.removeClass = function(el, klass) { 56 el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); 57 }; 58 } 59 else { 60 $.hasClass = function(el, klass) { 61 return el.classList.contains(klass); 62 }; 63 64 $.addClass = function(el, klass) { 65 el.classList.add(klass); 66 }; 67 68 $.removeClass = function(el, klass) { 69 el.classList.remove(klass); 70 }; 71 } 72 73 $.toggleClass = function(el, klass) { 74 if ($.hasClass(el, klass)) { 75 $.removeClass(el, klass); 76 } 77 else { 78 $.addClass(el, klass); 79 } 80 }; 81 82 var UA = {}; 83 84 UA.init = function() { 85 document.head = document.head || $.tag('head')[0]; 86 87 this.hasContextMenu = 'HTMLMenuItemElement' in window; 88 89 this.hasWebStorage = (function() { 90 var kv = 'catalog'; 91 try { 92 localStorage.setItem(kv, kv); 93 localStorage.removeItem(kv); 94 return true; 95 } catch (e) { 96 return false; 97 } 98 })(); 99 100 this.hasSessionStorage = (function() { 101 var kv = 'catalog'; 102 try { 103 sessionStorage.setItem(kv, kv); 104 sessionStorage.removeItem(kv); 105 return true; 106 } catch (e) { 107 return false; 108 } 109 })(); 110 111 this.hasCORS = 'withCredentials' in new XMLHttpRequest(); 112 113 this.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); 114 }; 115 116 UA.dispatchEvent = function(name, detail) { 117 var e = document.createEvent('Event'); 118 e.initEvent(name, false, false); 119 if (detail) { 120 e.detail = detail; 121 } 122 document.dispatchEvent(e); 123 }; 124 125 var FC = function() { 126 127 var self = this, 128 129 options = { 130 orderby: 'alt', 131 large: false, 132 extended: true, 133 imgdel: '//s.4cdn.org/image/filedeleted-res.gif', 134 imgspoiler: '//s.4cdn.org/image/spoiler', 135 nofile: '//s.4cdn.org/image/nofile.png', 136 smallsize: 150, 137 tipdelay: 250, 138 filterColors: [ 139 ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], 140 ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], 141 ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] 142 ] 143 }, 144 145 capcodeMap = { 146 admin: 'Administrator', 147 mod: 'Moderator', 148 developer: 'Developer', 149 manager: 'Manager', 150 founder: 'Founder', 151 verified: 'Verified' 152 }, 153 154 keybinds = { 155 83: focusQuickfilter, // S 156 82: refreshWindow, // R 157 88: cycleOrder // X 158 }, 159 160 catalog = {}, 161 162 basicSettings = [ 'orderby', 'large', 'extended' ], 163 164 activeTheme = {}, 165 166 activeStyleGroup, 167 activeStyleSheet, 168 169 activeFilters = {}, 170 171 hasTooltip = false, 172 tooltipTimeout = null, 173 174 pinnedThreads = {}, 175 176 hiddenThreads = {}, 177 hiddenThreadsCount = 0, 178 179 filteredThreadsCount = 0, 180 181 hasThreadWatcher = false, 182 hasDropDownNav = false, 183 hasClassicNav = false, 184 hasAutoHideNav = false, 185 altCaptcha = false, 186 187 quickFilterPattern = false, 188 189 hiddenMode = false, 190 191 $threads, 192 $qfCtrl, 193 $teaserCtrl, 194 $sizeCtrl, 195 $orderCtrl, 196 $filterPalette, 197 198 ctxCmds; 199 200 if (window.devicePixelRatio >= 2) { 201 options.imgdel.replace('.', '@2x.'); 202 options.nofile.replace('.', '@2x.'); 203 } 204 205 UA.init(); 206 207 loadTheme(); 208 209 self.init = function() { 210 var extConfig, el, val, fn; 211 212 FC.hasMobileLayout = checkMobileLayout(); 213 214 applyTheme(activeTheme, true); 215 216 $threads = $.id('threads'); 217 $qfCtrl = $.id('qf-ctrl'); 218 $teaserCtrl = $.id('teaser-ctrl'); 219 $sizeCtrl = $.id('size-ctrl'); 220 $orderCtrl = $.id('order-ctrl'); 221 222 $.on($qfCtrl, 'click', toggleQuickfilter); 223 $.on($.id('filters-clear-hidden'), 'click', toggleHiddenThreads); 224 $.on($.id('filters-clear-hidden-bottom'), 'click', toggleHiddenThreads); 225 $.on($.id('qf-clear'), 'click', toggleQuickfilter); 226 $.on($.id('settingsWindowLink'), 'click', showThemeEditor); 227 $.on($.id('settingsWindowLinkBot'), 'click', showThemeEditor); 228 $.on($.id('settingsWindowLinkMobile'), 'click', showThemeEditor); 229 $.on($.id('filters-ctrl'), 'click', showFilters); 230 $.on($teaserCtrl, 'change', onTeaserChange); 231 $.on($sizeCtrl, 'change', onSizeChange); 232 $.on($orderCtrl, 'change', onOrderChange); 233 $.on($threads, 'mouseover', onThreadMouseOver); 234 $.on($threads, 'mouseout', onThreadMouseOut); 235 $.on($.id('togglePostFormLinkMobile'), 'click', togglePostFormMobile); 236 $.on(document, 'click', onClick); 237 238 loadSettings(); 239 240 bindGlobalShortcuts(); 241 242 initGlobalMessage(); 243 244 if (UA.hasContextMenu) { 245 buildContextMenu(); 246 } 247 248 window.Config = {}; 249 250 if (UA.hasWebStorage) { 251 if (extConfig = localStorage.getItem('4chan-settings')) { 252 extConfig = JSON.parse(extConfig); 253 254 window.Config = extConfig; 255 256 if (!extConfig.disableAll) { 257 CustomMenu.initCtrl(extConfig.dropDownNav, extConfig.classicNav); 258 /* 259 if (location.host === 'boards.4channel.org' && extConfig.showNWSBoards) { 260 CustomMenu.showNWSBoards(); 261 } 262 */ 263 if (extConfig.filter) { 264 ThreadWatcher.hasFilters = true; 265 } 266 267 if (extConfig.threadWatcher) { 268 hasThreadWatcher = true; 269 ThreadWatcher.init(); 270 } 271 272 if (extConfig.customMenu) { 273 CustomMenu.apply(extConfig.customMenuList); 274 } 275 276 if (extConfig.dropDownNav !== false && !FC.hasMobileLayout) { 277 hasDropDownNav = true; 278 hasClassicNav = extConfig.classicNav; 279 hasAutoHideNav = extConfig.autoHideNav; 280 showDropDownNav(); 281 } 282 283 altCaptcha = extConfig.altCaptcha; 284 } 285 } 286 else if (UA.isMobileDevice && !FC.hasMobileLayout) { 287 hasDropDownNav = true; 288 showDropDownNav(); 289 } 290 else { 291 CustomMenu.initCtrl(false, false); 292 } 293 294 if (window.css_event && activeStyleSheet === '_special') { 295 fn = window['fc_' + window.css_event + '_init']; 296 fn && fn(); 297 } 298 } 299 300 if (el = document.forms.post.flag) { 301 if ((val = $.readCookie('4chan_flag')) && (el = el.querySelector('option[value="' + val + '"]'))) { 302 el.setAttribute('selected', 'selected'); 303 } 304 } 305 306 setOrder(options.orderby, true); 307 setLarge(options.large, true); 308 setExtended(options.extended, true); 309 310 UA.dispatchEvent('4chanMainInit'); 311 }; 312 313 function showDropDownNav() { 314 var el, top, bottom; 315 316 top = $.id('boardNavDesktop'); 317 bottom = $.id('boardNavDesktopFoot'); 318 319 if (hasClassicNav) { 320 el = document.createElement('div'); 321 el.className = 'pageJump'; 322 el.innerHTML = '<a href="#bottom">▼</a>' 323 + '<a href="javascript:void(0);" id="settingsWindowLinkClassic">Settings</a>' 324 + '<a href="//www.' + $L.d(catalog.slug) + '" target="_top">Home</a></div>'; 325 326 top.appendChild(el); 327 328 $.id('settingsWindowLinkClassic') 329 .addEventListener('click', showThemeEditor, false); 330 331 $.addClass(top, 'persistentNav'); 332 } 333 else { 334 top.style.display = 'none'; 335 $.removeClass($.id('boardNavMobile'), 'mobile'); 336 } 337 338 if (hasAutoHideNav) { 339 StickyNav.init(hasClassicNav); 340 } 341 342 bottom.style.display = 'none'; 343 344 $.addClass(document.body, 'hasDropDownNav'); 345 } 346 347 function hideDropDownNav() { 348 var el, top, bottom; 349 350 top = $.id('boardNavDesktop'); 351 bottom = $.id('boardNavDesktopFoot'); 352 353 if (hasClassicNav) { 354 if (el = $.cls('pageJump', top)[0]) { 355 $.id('settingsWindowLinkClassic') 356 .removeEventListener('click', showThemeEditor, false); 357 top.removeChild(el); 358 } 359 360 $.removeClass(top, 'persistentNav'); 361 } 362 else { 363 top.style.display = ''; 364 $.addClass($.id('boardNavMobile'), 'mobile'); 365 } 366 367 if (hasAutoHideNav) { 368 StickyNav.destroy(hasClassicNav); 369 } 370 371 bottom.style.display = ''; 372 373 $.removeClass(document.body, 'hasDropDownNav'); 374 } 375 376 self.loadCatalog = function(c) { 377 var query; 378 379 catalog = c; 380 381 $.addClass(document.body, activeStyleSheet.toLowerCase().replace(/ /g, '_')); 382 383 initStyleSwitcher(); 384 loadFilters(); 385 loadStorage(); 386 387 if (UA.hasSessionStorage && !location.hash && (query = sessionStorage.getItem('4chan-catalog-search'))) { 388 if (catalog.slug != sessionStorage.getItem('4chan-catalog-search-board')) { 389 sessionStorage.removeItem('4chan-catalog-search'); 390 sessionStorage.removeItem('4chan-catalog-search-board'); 391 query = null; 392 } 393 } 394 else if (location.hash && (query = location.hash.match(/#s=(.+)/))) { 395 query = decodeURIComponent(query[1].replace(/\+/g, ' ')); 396 } 397 398 if (query) { 399 toggleQuickfilter(); 400 $.id('qf-box').value = query; 401 applyQuickfilter(); 402 } 403 else { 404 buildThreads(); 405 } 406 }; 407 408 function initGlobalMessage() { 409 var msg, btn, thisTs, oldTs; 410 411 if (!UA.hasWebStorage || FC.hasMobileLayout) { 412 return; 413 } 414 415 if ((msg = $.id('globalMessage')) && msg.textContent) { 416 msg.nextElementSibling.style.clear = 'both'; 417 418 btn = document.createElement('span'); 419 btn.id = 'toggleMsgBtn'; 420 btn.setAttribute('data-cmd', 'toggleMsg'); 421 btn.title = 'Toggle announcement'; 422 423 oldTs = localStorage.getItem('4chan-global-msg'); 424 thisTs = msg.getAttribute('data-utc'); 425 426 if (oldTs && thisTs <= oldTs) { 427 msg.style.display = 'none'; 428 btn.style.opacity = '0.5'; 429 btn.className = 'expandIcon'; 430 } 431 else { 432 btn.className = 'collapseIcon'; 433 } 434 435 $.on(btn, 'click', toggleGlobalCatalogMessage); 436 msg.parentNode.insertBefore(btn, msg); 437 } 438 } 439 440 function toggleGlobalCatalogMessage() { 441 var msg, btn; 442 443 msg = $.id('globalMessage'); 444 btn = $.id('toggleMsgBtn'); 445 if (msg.style.display == 'none') { 446 msg.style.display = ''; 447 btn.className = 'collapseIcon'; 448 btn.style.opacity = '1'; 449 localStorage.removeItem('4chan-global-msg'); 450 } 451 else { 452 msg.style.display = 'none'; 453 btn.className = 'expandIcon'; 454 btn.innerHTML = '<span class="mobile">View Important Announcement</span>'; 455 btn.style.opacity = '0.5'; 456 localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); 457 } 458 459 //StorageSync.sync('4chan-global-msg'); 460 } 461 462 function togglePostFormMobile() { 463 var el = document.getElementById('postForm'); 464 465 if (el.style.display == 'table') { 466 el.style.display = ''; 467 this.textContent = 'Start a New Thread'; 468 } 469 else { 470 el.style.display = 'table'; 471 this.textContent = 'Close Post Form'; 472 window.initRecaptcha(); 473 window.initTCaptcha(); 474 } 475 } 476 477 function getRegexSpecials() { 478 var specials = ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\' ]; 479 return new RegExp('(\\' + specials.join('|\\') + ')', 'g'); 480 } 481 482 function getThreadPage(tid) { 483 return (0 | (catalog.threads[tid].b / catalog.pagesize)) + 1; 484 } 485 486 function initStyleSwitcher() { 487 var i, selector, nodes, ss; 488 489 selector = $.id('styleSelector'); 490 nodes = selector.children; 491 492 for (i = 0; ss = nodes[i]; ++i) { 493 if (ss.value == activeStyleSheet) { 494 selector.selectedIndex = i; 495 } 496 } 497 498 $.on(selector, 'change', onStyleSheetChange); 499 } 500 501 function onStyleSheetChange() { 502 var expires; 503 504 if (this.value !== '_special') { 505 expires = new Date(); 506 507 expires.setTime(expires.getTime() + 31536000000); 508 509 document.cookie = activeStyleGroup + '=' + this.value + '; expires=' 510 + expires.toGMTString() + '; path=/; domain=' + $L.d(catalog.slug); 511 512 if (window.css_event) { 513 fn = window['fc_' + window.css_event + '_cleanup']; 514 localStorage.setItem('4chan_stop_css_event', `${window.css_event}-${window.css_event_v}`); 515 } 516 } 517 else if (window.css_event) { 518 fn = window['fc_' + window.css_event + '_init']; 519 localStorage.removeItem('4chan_stop_css_event'); 520 } 521 522 //StorageSync.sync('4chan_stop_css_event'); 523 524 refreshWindow(); 525 } 526 527 function refreshWindow(e) { 528 if (e && e.shiftKey) { 529 return; 530 } 531 location.href = location.href; 532 } 533 534 function debounce(delay, fn) { 535 var timeout; 536 537 return function() { 538 var args = arguments, context = this; 539 540 clearTimeout(timeout); 541 timeout = setTimeout(function () { 542 fn.apply(context, args); 543 }, delay); 544 }; 545 } 546 547 function focusQuickfilter() { 548 if ($.hasClass($qfCtrl, 'active')) { 549 clearQuickfilter(true); 550 } 551 else { 552 toggleQuickfilter(); 553 } 554 } 555 556 function toggleQuickfilter() { 557 var qfBox, qfcnt = $.id('qf-cnt'); 558 if ($.hasClass($qfCtrl, 'active')) { 559 clearQuickfilter(); 560 qfcnt.style.display = 'none'; 561 $.removeClass($qfCtrl, 'active'); 562 } 563 else { 564 qfcnt.style.display = 'inline'; 565 qfBox = $.id('qf-box'); 566 if (!qfcnt.hasAttribute('data-built')) { 567 qfcnt.setAttribute('data-built', '1'); 568 $.on(qfBox, 'keyup', debounce(250, applyQuickfilter)); 569 $.on(qfBox, 'keydown', function(e) { 570 if (e.keyCode == '27') { 571 toggleQuickfilter(); 572 } 573 }); 574 } 575 qfBox.focus(); 576 qfBox.value = ''; 577 $.addClass($qfCtrl, 'active'); 578 } 579 } 580 581 function applyQuickfilter() { 582 var regexEscape, qfstr; 583 584 if ((qfstr = $.id('qf-box').value) !== '') { 585 if (UA.hasSessionStorage) { 586 sessionStorage.setItem('4chan-catalog-search', qfstr); 587 sessionStorage.setItem('4chan-catalog-search-board', catalog.slug); 588 } 589 regexEscape = getRegexSpecials(); 590 $.id('search-term').textContent = $.id('search-term-bottom').textContent = qfstr; 591 $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'inline'; 592 qfstr = qfstr.replace(regexEscape, '\\$1'); 593 quickFilterPattern = new RegExp(qfstr, 'i'); 594 buildThreads(); 595 } else { 596 clearQuickfilter(); 597 } 598 } 599 600 function clearQuickfilter(focus) { 601 var qf = $.id('qf-box'); 602 $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'none'; 603 if (focus) { 604 qf.value = ''; 605 qf.focus(); 606 } 607 else { 608 if (UA.hasSessionStorage) { 609 sessionStorage.removeItem('4chan-catalog-search'); 610 } 611 quickFilterPattern = false; 612 buildThreads(); 613 } 614 } 615 616 function buildContextMenu() { 617 ctxCmds = { 618 pin: toggleThreadPin, 619 hide: toggleThreadHide, 620 report: reportThread 621 }; 622 623 $.id('ctxmenu-main').innerHTML = 624 '<menuitem label="Unpin all threads"></menuitem>'; 625 626 $.id('ctxmenu-thread').innerHTML = 627 '<menuitem label="Pin/Unpin" data-cmd="pin"></menuitem>' + 628 '<menuitem label="Hide/Unhide" data-cmd="hide"></menuitem>' + 629 '<menuitem label="Report" data-cmd="report"></menuitem>'; 630 631 $.on($.id('ctxmenu-main'), 'click', clearPinnedThreads); 632 $.on($.id('ctxmenu-thread'), 'click', onThreadContextClick); 633 } 634 635 function bindGlobalShortcuts() { 636 var el, tid; 637 if (UA.hasWebStorage) { 638 $.on($threads, 'mousedown', function(e) { 639 el = e.target; 640 if (el.className.indexOf('thumb') != -1) { 641 tid = el.getAttribute('data-id'); 642 if (e.which == 3) { 643 $threads.setAttribute('contextmenu', 'ctxmenu-thread'); 644 $.id('ctxmenu-thread').target = tid; 645 } 646 else if (e.which == 1 && e.altKey) { 647 toggleThreadPin(tid); 648 return false; 649 } 650 else if (e.which == 1 && e.shiftKey) { 651 toggleThreadHide(tid); 652 return false; 653 } 654 } 655 else if (e.which == 3) { 656 $threads.setAttribute('contextmenu', 'ctxmenu-main'); 657 } 658 }); 659 } 660 if (!activeTheme.nobinds) { 661 $.on(document, 'keyup', processKeybind); 662 } 663 } 664 665 function toggleThreadPin(tid) { 666 if (pinnedThreads[tid] >= 0) { 667 delete pinnedThreads[tid]; 668 } 669 else { 670 pinnedThreads[tid] = catalog.threads[tid].r || 0; 671 } 672 localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); 673 buildThreads(); 674 } 675 676 function toggleThreadHide(tid) { 677 if (hiddenMode) { 678 delete hiddenThreads[tid]; 679 --hiddenThreadsCount; 680 } 681 else { 682 hiddenThreads[tid] = true; 683 ++hiddenThreadsCount; 684 } 685 686 localStorage.setItem('4chan-hide-t-' + catalog.slug, JSON.stringify(hiddenThreads)); 687 688 $.id('thread-' + tid).style.display = 'none'; 689 690 setHiddenCount(hiddenThreadsCount); 691 692 if (hiddenThreadsCount === 0) { 693 setHiddenMode(false); 694 } 695 } 696 697 function setHiddenMode(state) { 698 hiddenMode = state; 699 700 $.id('filters-clear-hidden').textContent = 701 $.id('filters-clear-hidden-bottom').textContent = state ? 'Back' : 'Show'; 702 703 buildThreads(); 704 } 705 706 function setProcessedCount(type, num) { 707 var label = type + '-label', count = type + '-count'; 708 709 if (num > 0) { 710 $.id(count).textContent = $.id(count + '-bottom').textContent = num; 711 $.id(label).style.display = $.id(label + '-bottom').style.display = 'inline'; 712 } 713 else { 714 $.id(label).style.display = $.id(label + '-bottom').style.display = 'none'; 715 } 716 } 717 718 function setHiddenCount(num) { 719 setProcessedCount('hidden', num); 720 } 721 722 function setFilteredCount(num) { 723 setProcessedCount('filtered', num); 724 } 725 726 function reportThread(tid) { 727 var height, altc; 728 729 if (window.passEnabled || !window.grecaptcha) { 730 height = 175; 731 } 732 else if (altCaptcha) { 733 height = 320; 734 altc = '&altc=1'; 735 } 736 else { 737 height = 510; 738 altc = ''; 739 } 740 741 window.open( 742 'https://sys.' + $L.d(catalog.slug) + '/' + catalog.slug + 743 '/imgboard.php?mode=report&no=' + tid + altc, 744 Date.now(), 745 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=' + height 746 ); 747 } 748 749 function onThreadContextClick(e) { 750 var cmd = e.target.getAttribute('data-cmd'); 751 ctxCmds[cmd]($.id('ctxmenu-thread').target); 752 } 753 754 function processKeybind(e) { 755 var el = e.target; 756 if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { 757 return; 758 } 759 if (keybinds[e.keyCode]) { 760 keybinds[e.keyCode](e); 761 } 762 } 763 764 function toggleHiddenThreads(e) { 765 var el; 766 767 e.preventDefault(); 768 769 if (hiddenThreadsCount > 0) { 770 if ((el = $.id('filters-clear-hidden')).textContent == 'Show') { 771 setHiddenMode(true); 772 } 773 else { 774 setHiddenMode(false); 775 } 776 } 777 } 778 779 function clearPinnedThreads() { 780 pinnedThreads = {}; 781 localStorage.removeItem('4chan-pin-' + catalog.slug); 782 buildThreads(); 783 return false; 784 } 785 786 function onClick(e) { 787 var t = e.target, tid; 788 789 if ((t = e.target) == document) { 790 return; 791 } 792 793 if (tid = t.getAttribute('data-watch')) { 794 ThreadWatcher.toggle( 795 tid, 796 catalog.slug, 797 catalog.threads[tid].sub, 798 catalog.threads[tid].teaser, 799 catalog.threads[tid].lr.id 800 ); 801 } 802 else if (tid = t.getAttribute('data-hide')) { 803 e.preventDefault(); 804 toggleThreadHide(tid); 805 } 806 else if (tid = t.getAttribute('data-pin')) { 807 e.preventDefault(); 808 toggleThreadPin(tid); 809 } 810 else if (tid = t.getAttribute('data-report')) { 811 e.preventDefault(); 812 reportThread(tid); 813 } 814 else if (tid = t.getAttribute('data-post-menu')) { 815 e.preventDefault(); 816 PostMenu.open(t, tid, hasThreadWatcher, hiddenThreads[tid], pinnedThreads[tid]); 817 } 818 else if (t.hasAttribute('data-cm-edit')) { 819 e.preventDefault(); 820 CustomMenu.showEditor(true); 821 } 822 else if (t.id == 'backdrop') { 823 if (!panelHidden($.id('filters'))) { 824 if (!panelHidden($.id('filters-protip'))) { 825 closeFiltersHelp(); 826 } 827 else { 828 closeFilters(); 829 } 830 } 831 else if (!panelHidden($.id('theme'))) { 832 closeThemeEditor(); 833 } 834 } 835 else if (e.target.id == 'filter-palette') { 836 closeFilterPalette(); 837 } 838 } 839 840 function buildFilterPalette() { 841 var i, j, table, palette, rows, cols, tr, td, foot; 842 843 $filterPalette = $.id('filter-palette'); 844 845 table = $.id('filter-color-table'); 846 palette = $.tag('tbody', table)[0]; 847 rows = options.filterColors.length; 848 849 if (rows > 0) { 850 cols = options.filterColors[0].length; 851 foot = $.tag('tfoot', table)[0]; 852 for (i = foot.children.length - 1; i >= 0; i--) { 853 foot.children[i].firstElementChild.setAttribute('colspan', cols); 854 } 855 } 856 for (i = 0; i < rows; ++i) { 857 tr = document.createElement('tr'); 858 for (j = 0; j < cols; ++j) { 859 td = document.createElement('td'); 860 td.innerHTML = '<span class="button clickbox" style="background:' 861 + options.filterColors[i][j] + '"></span>'; 862 $.on(td.firstElementChild, 'click', selectFilterColor); 863 tr.appendChild(td); 864 } 865 palette.appendChild(tr); 866 } 867 } 868 869 function showFilterPalette(el) { 870 var picker, pos = el.getBoundingClientRect(); 871 872 if (!$filterPalette) { 873 buildFilterPalette(); 874 } 875 876 $.removeClass($filterPalette, 'hidden'); 877 $filterPalette.setAttribute('data-target', el.id.split('-')[2]); 878 879 picker = $filterPalette.firstElementChild; 880 picker.style.cssText = 'top:' + pos.top + 'px;left:' 881 + (pos.left - picker.clientWidth - 10) + 'px;'; 882 } 883 884 function showFiltersHelp() { 885 var el = $.id('filters-protip'); 886 el.style.top = window.pageYOffset + 50 + 'px'; 887 $.removeClass(el, 'hidden'); 888 } 889 890 function closeFiltersHelp() { 891 $.addClass($.id('filters-protip'), 'hidden'); 892 } 893 894 function onFiltersClick(e) { 895 var t = e.target; 896 897 if (t.id == 'filters-close') 898 closeFilters(); 899 else if (t.id == 'filters-add') 900 addEmptyFilter(); 901 else if (t.id == 'filters-save') { 902 saveFilters(); 903 closeFilters(); 904 } 905 else if (t.hasAttribute('data-active')) 906 toggleFilter(t, 'active'); 907 else if (t.hasAttribute('data-hide')) 908 toggleFilter(t, 'hide', 'top'); 909 else if (t.hasAttribute('data-top')) 910 toggleFilter(t, 'top', 'hide'); 911 else if ($.hasClass(t, 'filter-color')) 912 showFilterPalette(t); 913 else if (t.hasAttribute('data-target')) 914 deleteFilter(t); 915 else if (t.hasAttribute('data-up')) 916 moveFilterUp(t); 917 } 918 919 function moveFilterUp(el) { 920 var tr, prev; 921 922 tr = el.parentNode.parentNode; 923 prev = tr.previousElementSibling; 924 925 if (prev) { 926 tr.parentNode.insertBefore(tr, prev); 927 } 928 } 929 930 function onFiltersSearch(e) { 931 var i, el, nodes, cnt, str; 932 933 if (e && (e.keyCode == 27)) { 934 this.value = ''; 935 } 936 937 str = this.value.toLowerCase(); 938 939 nodes = document.getElementsByClassName('filter-pattern'); 940 941 cnt = document.getElementById('filter-list'); 942 943 cnt.style.display = 'none'; 944 945 for (i = 0; el = nodes[i]; ++i) { 946 if (el.value.toLowerCase().indexOf(str) === -1) { 947 el.parentNode.parentNode.style.display = 'none'; 948 } 949 else { 950 el.parentNode.parentNode.style.display = ''; 951 } 952 } 953 954 cnt.style.display = ''; 955 } 956 957 function showFilters() { 958 var i, filtersPanel, rawFilters, filterList, filterId, el; 959 960 filtersPanel = $.id('filters'); 961 962 if (!filtersPanel) { 963 filtersPanel = FC.panelHTML.build('filters', 'panel hidden'); 964 FC.panelHTML.build('filters-protip', 'panel hidden'); 965 FC.panelHTML.build('filter-palette', 'hidden'); 966 } 967 968 if (!filtersPanel.hasAttribute('data-built')) { 969 $.on(filtersPanel, 'click', onFiltersClick); 970 971 $.on($.id('filter-palette-close'), 'click', closeFilterPalette); 972 $.on($.id('filter-palette-clear'), 'click', clearFilterColor); 973 974 $.on($.id('filters-help-open'), 'click', showFiltersHelp); 975 $.on($.id('filters-help-close'), 'click', closeFiltersHelp); 976 977 $.on($.id('filter-rgb'), 'keyup', filterSetCustomColor); 978 $.on($.id('filter-rgb-ok'), 'click', selectFilterColor); 979 980 $.on($.id('filters-search'), 'keyup', onFiltersSearch); 981 982 filtersPanel.setAttribute('data-built', '1'); 983 } 984 else { 985 $.id('filters-search').value = ''; 986 } 987 988 rawFilters = localStorage.getItem('catalog-filters'); 989 filterId = 0; 990 991 if (rawFilters) { 992 filterList = $.id('filter-list'); 993 rawFilters = JSON.parse(rawFilters); 994 for (i in rawFilters) { 995 filterList.appendChild(buildFilter(rawFilters[i], filterId)); 996 ++filterId; 997 } 998 updateFilterHitCount(); 999 } 1000 1001 filtersPanel.style.top = window.pageYOffset + 60 + 'px'; 1002 1003 $.removeClass(filtersPanel, 'hidden'); 1004 1005 if (el = $.cls('filter-active', filtersPanel)[0]) { 1006 el.focus(); 1007 } 1008 1009 toggleBackdrop(); 1010 } 1011 1012 function closeFilters() { 1013 var i, filterList, nodes; 1014 1015 $.id('filters-msg').style.display = 'none'; 1016 $.addClass($.id('filters'), 'hidden'); 1017 1018 filterList = $.id('filter-list'); 1019 nodes = $.tag('tr', filterList); 1020 for (i = nodes.length - 1; i >= 0; i--) { 1021 filterList.removeChild(nodes[i]); 1022 } 1023 1024 closeFilterPalette(); 1025 toggleBackdrop(); 1026 } 1027 1028 function closeFilterPalette() { 1029 if ($filterPalette && !$.hasClass($filterPalette, 'hidden')) { 1030 $.addClass($filterPalette, 'hidden'); 1031 } 1032 } 1033 1034 // Loads patterns from the localStorage and builds regexps 1035 function loadFilters() { 1036 if (!UA.hasWebStorage) return; 1037 1038 activeFilters = {}; 1039 1040 var rawFilters = localStorage.getItem('catalog-filters'); 1041 if (!rawFilters) return; 1042 1043 rawFilters = JSON.parse(rawFilters); 1044 1045 var 1046 rf, fid, v, w, wordcount, 1047 wordSepS, wordSepE, 1048 regexType = /^\/(.*)\/(i?)$/, 1049 regexOrNorm = /\s*\|+\s*/g, 1050 regexWc = /\\\*/g, replWc = '[^\\s]*', 1051 regexEscape = getRegexSpecials(), 1052 match, inner, words, rawPattern, pattern, orOp, orCluster, type; 1053 1054 wordSepS = '(?=.*\\b'; 1055 wordSepE = '\\b)'; 1056 1057 try { 1058 for (fid in rawFilters) { 1059 rf = rawFilters[fid]; 1060 if (rf.active && rf.pattern !== '') { 1061 if (rf.boards && rf.boards.split(' ').indexOf(catalog.slug) == -1) { 1062 continue; 1063 } 1064 rawPattern = rf.pattern; 1065 if (rawPattern.charAt(0) == '#') { 1066 type = (rawPattern.charAt(1) == '#') ? 2 : 1; 1067 pattern = new RegExp(rawPattern.slice(type).replace(regexEscape, '\\$1')); 1068 } 1069 else { 1070 type = 0; 1071 if (match = rawPattern.match(regexType)) { 1072 pattern = new RegExp(match[1], match[2]); 1073 } 1074 else if (rawPattern.charAt(0) == '"' && rawPattern.charAt(rawPattern.length - 1) == '"') { 1075 pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); 1076 } 1077 else { 1078 words = rawPattern.replace(regexOrNorm, '|').split(' '); 1079 pattern = ''; 1080 wordcount = words.length; 1081 for (w = 0; w < wordcount; ++w) { 1082 if (words[w].indexOf('|') != -1) { 1083 orOp = words[w].split('|'); 1084 orCluster = []; 1085 for (v = orOp.length - 1; v >= 0; v--) { 1086 if (orOp[v] !== '') { 1087 orCluster.push(orOp[v].replace(regexEscape, '\\$1')); 1088 } 1089 } 1090 inner = orCluster.join('|').replace(regexWc, replWc); 1091 pattern += wordSepS + '(' + inner + ')' + wordSepE; 1092 } 1093 else { 1094 inner = words[w].replace(regexEscape, '\\$1').replace(regexWc, replWc); 1095 pattern += wordSepS + inner + wordSepE; 1096 } 1097 } 1098 pattern = new RegExp('^' + pattern, 'i'); 1099 } 1100 } 1101 //console.log('Resulting regex: ' + pattern); 1102 activeFilters[fid] = { 1103 type: type, 1104 pattern: pattern, 1105 boards: rf.boards, 1106 fid: fid, 1107 hidden: rf.hidden, 1108 color: rf.color, 1109 top: rf.top, 1110 hits: 0 1111 }; 1112 } 1113 } 1114 } 1115 catch (err) { 1116 alert('There was an error processing one of the filters: ' 1117 + err + ' in: ' + rf.pattern); 1118 } 1119 } 1120 1121 function saveFilters() { 1122 var i, j, f, rawFilters, filterList, msg, rows, color; 1123 1124 rawFilters = {}; 1125 filterList = $.id('filter-list'); 1126 rows = filterList.children; 1127 1128 for (i = 0; j = rows[i]; ++i) { 1129 f = { 1130 active: $.cls('filter-active', j)[0].checked ? 1 : 0, 1131 pattern: $.cls('filter-pattern', j)[0].value, 1132 boards: $.cls('filter-boards', j)[0].value, 1133 hidden: $.cls('filter-hide', j)[0].checked ? 1 : 0, 1134 top: $.cls('filter-top', j)[0].checked ? 1 : 0 1135 }; 1136 color = $.cls('filter-color', j)[0]; 1137 if (!color.hasAttribute('data-nocolor')) { 1138 f.color = color.style.backgroundColor; 1139 } 1140 rawFilters[i] = f; 1141 } 1142 1143 if (rawFilters[0]) { 1144 localStorage.setItem('catalog-filters', JSON.stringify(rawFilters)); 1145 } 1146 else { 1147 localStorage.removeItem('catalog-filters'); 1148 } 1149 1150 msg = $.id('filters-msg'); 1151 msg.innerHTML = 'Done'; 1152 msg.className = 'msg-ok'; 1153 msg.style.display = 'inline'; 1154 setTimeout(function() { msg.style.display = 'none'; }, 2000); 1155 1156 loadFilters(); 1157 buildThreads(); 1158 updateFilterHitCount(); 1159 } 1160 1161 function filterSetCustomColor() { 1162 var filterRgbOk; 1163 1164 filterRgbOk = $.id('filter-rgb-ok'); 1165 1166 filterRgbOk.style.backgroundColor = this.value; 1167 } 1168 1169 function buildFilter(filter, id) { 1170 var td, tr, span, input; 1171 1172 tr = document.createElement('tr'); 1173 tr.id = 'filter-' + id; 1174 1175 // Move up 1176 td = document.createElement('td'); 1177 span = document.createElement('span'); 1178 span.setAttribute('data-up', id); 1179 span.className = 'pointer'; 1180 span.innerHTML = '↑'; 1181 td.appendChild(span); 1182 tr.appendChild(td); 1183 1184 // On 1185 td = document.createElement('td'); 1186 input = document.createElement('input'); 1187 input.type = 'checkbox'; 1188 input.checked = !!filter.active; 1189 input.className = 'filter-active'; 1190 td.appendChild(input); 1191 tr.appendChild(td); 1192 1193 // Pattern 1194 td = document.createElement('td'); 1195 input = document.createElement('input'); 1196 input.type = 'text'; 1197 input.value = filter.pattern; 1198 input.className = 'filter-pattern'; 1199 td.appendChild(input); 1200 tr.appendChild(td); 1201 1202 // Boards 1203 td = document.createElement('td'); 1204 input = document.createElement('input'); 1205 input.type = 'text'; 1206 input.value = filter.boards; 1207 input.className = 'filter-boards'; 1208 td.appendChild(input); 1209 tr.appendChild(td); 1210 1211 // Color 1212 td = document.createElement('td'); 1213 span = document.createElement('span'); 1214 span.id = 'filter-color-' + id; 1215 span.title = 'Change Color'; 1216 span.className = 'button clickbox filter-color'; 1217 if (!filter.color) { 1218 span.setAttribute('data-nocolor', '1'); 1219 span.innerHTML = '∕'; 1220 } 1221 else { 1222 span.style.background = filter.color; 1223 } 1224 td.appendChild(span); 1225 tr.appendChild(td); 1226 1227 // Hide 1228 td = document.createElement('td'); 1229 input = document.createElement('input'); 1230 input.type = 'checkbox'; 1231 input.checked = !!filter.hidden; 1232 input.className = 'filter-hide'; 1233 td.appendChild(input); 1234 tr.appendChild(td); 1235 1236 // Top 1237 td = document.createElement('td'); 1238 input = document.createElement('input'); 1239 input.type = 'checkbox'; 1240 input.checked = !!filter.top; 1241 input.className = 'filter-top'; 1242 td.appendChild(input); 1243 tr.appendChild(td); 1244 1245 // Del 1246 td = document.createElement('td'); 1247 span = document.createElement('span'); 1248 span.setAttribute('data-target', id); 1249 span.className = 'pointer'; 1250 span.innerHTML = '×'; 1251 td.appendChild(span); 1252 tr.appendChild(td); 1253 1254 // Match count 1255 td = document.createElement('td'); 1256 td.id = 'fhc-' + id; 1257 td.className = 'filter-hits'; 1258 tr.appendChild(td); 1259 1260 return tr; 1261 } 1262 1263 function selectFilterColor(clear) { 1264 var target = $.id('filter-color-' + $filterPalette.getAttribute('data-target')); 1265 if (clear === true) { 1266 target.setAttribute('data-nocolor', '1'); 1267 target.innerHTML = '∕'; 1268 target.style.background = ''; 1269 } 1270 else { 1271 target.removeAttribute('data-nocolor'); 1272 target.innerHTML = ''; 1273 target.style.background = this.style.backgroundColor; 1274 } 1275 closeFilterPalette(); 1276 } 1277 1278 function clearFilterColor() { 1279 selectFilterColor(true); 1280 } 1281 1282 function addEmptyFilter() { 1283 var filter = { 1284 active: 1, 1285 pattern: '', 1286 boards: '', 1287 color: '', 1288 hidden: 0, 1289 top: 0, 1290 hits: 0 1291 }; 1292 $.id('filter-list').appendChild(buildFilter(filter, getNextFilterId())); 1293 } 1294 1295 function getNextFilterId() { 1296 var i, j, max, rows = $.id('filter-list').children; 1297 1298 if (!rows.length) { 1299 return 0; 1300 } 1301 else { 1302 max = 0; 1303 for (i = 0; j = rows[i]; ++i) { 1304 j = +j.id.slice(7); 1305 if (j > max) { 1306 max = j; 1307 } 1308 } 1309 return max + 1; 1310 } 1311 } 1312 1313 function deleteFilter(t) { 1314 var el = $.id('filter-' + t.getAttribute('data-target')); 1315 el.parentNode.removeChild(el); 1316 } 1317 1318 function toggleFilter(el, type, xor) { 1319 var attr = 'data-' + type, xorEle; 1320 1321 if (el.getAttribute(attr) == '0') { 1322 el.setAttribute(attr, '1'); 1323 $.addClass(el, 'active'); 1324 el.innerHTML = '✔'; 1325 if (xor) { 1326 xorEle = $.cls('filter-' + xor, el.parentNode.parentNode)[0]; 1327 xorEle.setAttribute('data-' + xor, '0'); 1328 $.removeClass(xorEle, 'active'); 1329 xorEle.innerHTML = ''; 1330 } 1331 } 1332 else { 1333 el.setAttribute(attr, '0'); 1334 $.removeClass(el, 'active'); 1335 el.innerHTML = ''; 1336 } 1337 } 1338 1339 function updateFilterHitCount() { 1340 var i, j, rows = $.id('filter-list').children; 1341 for (i = 0; j = rows[i]; ++i) { 1342 $.id('fhc-' + j.id.slice(7)) 1343 .innerHTML = activeFilters[i] ? 'x' + activeFilters[i].hits : ''; 1344 } 1345 } 1346 1347 function panelHidden(el) { 1348 return el && $.hasClass(el, 'hidden'); 1349 } 1350 1351 function showThemeEditor() { 1352 var themePanel, el, theme; 1353 1354 if (!UA.hasWebStorage) { 1355 alert("Your browser doesn't support Local Storage"); 1356 return; 1357 } 1358 1359 themePanel = $.id('theme'); 1360 1361 if (!themePanel) { 1362 themePanel = FC.panelHTML.build('theme', 'panel hidden'); 1363 } 1364 1365 theme = localStorage.getItem('catalog-theme'); 1366 theme = theme ? JSON.parse(theme) : {}; 1367 1368 $.id('theme-nobinds').checked = !!theme.nobinds; 1369 $.id('theme-nospoiler').checked = !!theme.nospoiler; 1370 $.id('theme-newtab').checked = !!theme.newtab; 1371 $.id('theme-tw').checked = hasThreadWatcher; 1372 $.id('theme-ddn').checked = hasDropDownNav; 1373 1374 if (theme.css) { 1375 $.id('theme-css').value = theme.css; 1376 } 1377 1378 $.on($.id('theme-save'), 'click', saveTheme); 1379 $.on($.id('theme-close'), 'click', closeThemeEditor); 1380 1381 $.id('theme-msg').style.display = 'none'; 1382 1383 themePanel.style.top = window.pageYOffset + 60 + 'px'; 1384 $.removeClass(themePanel, 'hidden'); 1385 1386 if (el = $.tag('input', themePanel)[0]) { 1387 el.focus(); 1388 } 1389 1390 toggleBackdrop(); 1391 1392 document.dispatchEvent(new CustomEvent('4chanCatalogThemeEditorReady')); 1393 } 1394 1395 function closeThemeEditor() { 1396 $.off($.id('theme-save'), 'click', saveTheme); 1397 $.off($.id('theme-close'), 'click', closeThemeEditor); 1398 1399 $.addClass($.id('theme'), 'hidden'); 1400 toggleBackdrop(); 1401 } 1402 1403 function toggleBackdrop() { 1404 $.toggleClass($.id('backdrop'), 'hidden'); 1405 } 1406 1407 function loadTheme() { 1408 var customTheme; 1409 1410 if (UA.hasWebStorage && (customTheme = localStorage.getItem('catalog-theme'))) { 1411 activeTheme = JSON.parse(customTheme); 1412 } 1413 } 1414 1415 function applyTheme(customTheme, nocss) { 1416 if (customTheme.nobinds) { 1417 if (activeTheme.nobinds != customTheme.nobinds) { 1418 $.off(document, 'keyup', processKeybind); 1419 } 1420 } 1421 else { 1422 if (activeTheme.nobinds != customTheme.nobinds) { 1423 $.on(document, 'keyup', processKeybind); 1424 } 1425 } 1426 1427 if (customTheme.nospoiler) { 1428 $.addClass(document.body, 'reveal-img-spoilers'); 1429 } 1430 else { 1431 $.removeClass(document.body, 'reveal-img-spoilers'); 1432 } 1433 1434 if (!nocss) { 1435 self.applyCSS(customTheme); 1436 } 1437 1438 document.dispatchEvent(new CustomEvent('4chanCatalogThemeApplied')); 1439 } 1440 1441 self.applyCSS = function(customTheme, style_group, css_version) { 1442 var style, ss; 1443 1444 if (!customTheme) { 1445 customTheme = activeTheme; 1446 } 1447 1448 // Preferred stylesheet 1449 if (style_group !== undefined) { 1450 if (!(ss = $.readCookie(style_group))) { 1451 ss = style_group == 'nws_style' ? 'Yotsuba New' : 'Yotsuba B New'; 1452 } 1453 1454 activeStyleGroup = style_group; 1455 1456 if (window.css_event && localStorage.getItem('4chan_stop_css_event') !== `${window.css_event}-${window.css_event_v}`) { 1457 activeStyleSheet = '_special' 1458 ss = window.css_event; 1459 } 1460 else { 1461 activeStyleSheet = ss; 1462 } 1463 1464 style = document.createElement('link'); 1465 style.type = 'text/css'; 1466 style.id = 'base-css'; 1467 style.rel = 'stylesheet'; 1468 style.setAttribute('href', '//s.4cdn.org/css/catalog_' 1469 + ss.toLowerCase().replace(/ /g, '_') + '.' + css_version + '.css'); 1470 document.head.insertBefore(style, $.id('mobile-css')); 1471 } 1472 1473 // Custom CSS 1474 if (style = $.id('custom-css')) { 1475 document.head.removeChild(style); 1476 } 1477 1478 if (customTheme.css) { 1479 style = document.createElement('style'); 1480 style.type = 'text/css'; 1481 style.id = 'custom-css'; 1482 1483 if (style.styleSheet) { 1484 style.styleSheet.cssText = customTheme.css; 1485 } 1486 else { 1487 style.innerHTML = customTheme.css; 1488 } 1489 document.head.appendChild(style); 1490 } 1491 }; 1492 1493 // Applies and saves the theme to localStorage 1494 function saveTheme() { 1495 var i, css, tw, ddn, extConfig, customTheme = {}; 1496 1497 if ($.id('theme-nobinds').checked) { 1498 customTheme.nobinds = true; 1499 } 1500 1501 if ($.id('theme-nospoiler').checked) { 1502 customTheme.nospoiler = true; 1503 } 1504 1505 if ($.id('theme-newtab').checked) { 1506 customTheme.newtab = true; 1507 } 1508 1509 tw = $.id('theme-tw').checked; 1510 1511 ddn = $.id('theme-ddn').checked; 1512 1513 if (extConfig = localStorage.getItem('4chan-settings')) { 1514 extConfig = JSON.parse(extConfig); 1515 } 1516 else { 1517 extConfig = {}; 1518 } 1519 1520 if (tw != hasThreadWatcher) { 1521 if (tw) { 1522 ThreadWatcher.init(); 1523 extConfig.disableAll = false; 1524 } 1525 else { 1526 ThreadWatcher.unInit(); 1527 } 1528 } 1529 1530 if (ddn != hasDropDownNav) { 1531 if (ddn) { 1532 showDropDownNav(); 1533 extConfig.disableAll = false; 1534 } 1535 else { 1536 hideDropDownNav(); 1537 } 1538 } 1539 1540 extConfig.threadWatcher = tw; 1541 extConfig.dropDownNav = ddn; 1542 localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); 1543 //StorageSync.sync('4chan-settings'); 1544 1545 hasThreadWatcher = tw; 1546 hasDropDownNav = ddn; 1547 1548 if ((css = $.id('theme-css').value) !== '') { 1549 customTheme.css = css; 1550 } 1551 1552 applyTheme(customTheme); 1553 1554 localStorage.removeItem('catalog-theme'); 1555 1556 for (i in customTheme) { 1557 localStorage.setItem('catalog-theme', JSON.stringify(customTheme)); 1558 break; 1559 } 1560 1561 //StorageSync.sync('catalog-theme'); 1562 1563 activeTheme = customTheme; 1564 1565 buildThreads(); 1566 closeThemeEditor(); 1567 } 1568 1569 function loadThreadList(key) { 1570 var i, threads, mod = false, ft = 0; 1571 1572 if (threads = localStorage.getItem(key)) { 1573 ft = +Object.keys(catalog.threads).pop(); 1574 threads = JSON.parse(threads); 1575 for (i in threads) { 1576 if (!catalog.threads[i] && i < ft) { 1577 delete threads[i]; 1578 mod = true; 1579 } 1580 } 1581 for (i in threads) { 1582 if (mod) { localStorage.setItem(key, JSON.stringify(threads)); } 1583 return threads; 1584 } 1585 localStorage.removeItem(key); 1586 } 1587 return {}; 1588 } 1589 1590 function loadStorage() { 1591 if (UA.hasWebStorage) { 1592 hiddenThreads = loadThreadList('4chan-hide-t-' + catalog.slug); 1593 pinnedThreads = loadThreadList('4chan-pin-' + catalog.slug); 1594 } 1595 } 1596 1597 function loadSettings() { 1598 var settings; 1599 if (UA.hasWebStorage && (settings = localStorage.getItem('catalog-settings'))) { 1600 $.extend(options, JSON.parse(settings)); 1601 } 1602 } 1603 1604 function saveSettings() { 1605 var i, key, settings; 1606 if (!UA.hasWebStorage) { 1607 return; 1608 } 1609 settings = {}; 1610 for (i = basicSettings.length - 1; i >= 0; i--) { 1611 key = basicSettings[i]; 1612 settings[key] = options[key]; 1613 } 1614 localStorage.setItem('catalog-settings', JSON.stringify(settings)); 1615 //StorageSync.sync('catalog-settings'); 1616 } 1617 1618 function setExtended(mode, init) { 1619 var cls = ''; 1620 if (mode) { 1621 $teaserCtrl.selectedIndex = 1; 1622 cls = 'extended-'; 1623 options.extended = true; 1624 } 1625 else { 1626 $teaserCtrl.selectedIndex = 0; 1627 options.extended = false; 1628 } 1629 if (options.large) { 1630 cls += 'large'; 1631 } 1632 else { 1633 cls += 'small'; 1634 } 1635 $threads.className = cls; 1636 if (!init) { 1637 saveSettings(); 1638 } 1639 } 1640 1641 function setLarge(mode, init) { 1642 var cls = options.extended ? 'extended-' : ''; 1643 if (mode) { 1644 $sizeCtrl.selectedIndex = 1; 1645 cls += 'large'; 1646 options.large = true; 1647 } 1648 else { 1649 $sizeCtrl.selectedIndex = 0; 1650 cls += 'small'; 1651 options.large = false; 1652 } 1653 $threads.className = cls; 1654 if (!init) { 1655 saveSettings(); 1656 buildThreads(); 1657 } 1658 } 1659 1660 function setOrder(order, init) { 1661 var o = { alt: 0, absdate: 1, date: 2, r: 3 }; 1662 if (o[order] !== undefined) { 1663 $orderCtrl.selectedIndex = o[order]; 1664 options.orderby = order; 1665 } 1666 else { 1667 $orderCtrl.selectedIndex = 0; 1668 options.orderby = 'date'; 1669 } 1670 if (!init) { 1671 saveSettings(); 1672 buildThreads(); 1673 } 1674 } 1675 1676 function onTeaserChange() { 1677 setExtended($teaserCtrl.options[$teaserCtrl.selectedIndex].value == 'on'); 1678 } 1679 1680 function onOrderChange() { 1681 setOrder($orderCtrl.options[$orderCtrl.selectedIndex].value); 1682 } 1683 1684 function onSizeChange() { 1685 setLarge($sizeCtrl.options[$sizeCtrl.selectedIndex].value == 'large'); 1686 } 1687 1688 function cycleOrder() { 1689 if (options.orderby == 'date') { 1690 setOrder('alt'); 1691 } 1692 else if (options.orderby == 'alt') { 1693 setOrder('r'); 1694 } 1695 else if (options.orderby == 'r') { 1696 setOrder('absdate'); 1697 } 1698 else { 1699 setOrder('date'); 1700 } 1701 } 1702 1703 function sortThreadList(threadList) { 1704 var order = options.orderby; 1705 1706 if (order == 'date') { 1707 threadList.sort(function(a, b) { 1708 if (a.id > b.id) return -1; 1709 if (a.id < b.id) return 1; 1710 return 0; 1711 }); 1712 } 1713 else if (order == 'absdate' && !catalog.no_lr) { 1714 threadList.sort(function(a, b) { 1715 a = a.entry.lr.id; 1716 b = b.entry.lr.id; 1717 if (a > b) return -1; 1718 if (a < b) return 1; 1719 return 0; 1720 }); 1721 } 1722 else if (order == 'r') { 1723 threadList.sort(function(a, b) { 1724 var 1725 a = a.entry.r || 0, 1726 b = b.entry.r || 0; 1727 if (a > b) return -1; 1728 if (a < b) return 1; 1729 return 0; 1730 }); 1731 } 1732 else { // alt 1733 threadList.sort(function(a, b) { 1734 if (a.entry.b < b.entry.b) return -1; 1735 if (a.entry.b > b.entry.b) return 1; 1736 return 0; 1737 }); 1738 } 1739 } 1740 1741 function getFilteredThreads() { 1742 var i, id, entry, hl, onTop, pinned, teaser, tripcode, af, threads, fid, 1743 filtered; 1744 1745 filtered = 0; 1746 1747 threads = []; 1748 1749 threadloop: for (id in catalog.threads) { 1750 id = +id; 1751 entry = catalog.threads[id]; 1752 hl = onTop = pinned = false; 1753 1754 if (entry.sub) { 1755 teaser = '<b>' + entry.sub + '</b>'; 1756 if (entry.teaser) { 1757 teaser += ': ' + entry.teaser; 1758 } 1759 } 1760 else { 1761 teaser = entry.teaser; 1762 } 1763 1764 if (hiddenMode) { 1765 if (!hiddenThreads[id]) { 1766 continue; 1767 } 1768 ++hiddenThreadsCount; 1769 } 1770 else if(!quickFilterPattern) { 1771 if (hiddenThreads[id]) { 1772 ++hiddenThreadsCount; 1773 continue; 1774 } 1775 if (pinnedThreads[id] >= 0) { 1776 pinned = onTop = true; 1777 } 1778 else { 1779 if (entry.capcode) { 1780 tripcode = (entry.trip || '') + '!#' + entry.capcode; 1781 } 1782 else { 1783 tripcode = entry.trip; 1784 } 1785 for (fid in activeFilters) { 1786 af = activeFilters[fid]; 1787 if ((af.type == 0 && (af.pattern.test(teaser) || af.pattern.test(entry.file))) 1788 || (af.type == 1 && af.pattern.test(tripcode)) 1789 || (af.type == 2 && af.pattern.test(entry.author))) { 1790 if (af.hidden) { 1791 ++filtered; 1792 af.hits += 1; 1793 continue threadloop; 1794 } 1795 hl = af; 1796 onTop = !!af.top; 1797 af.hits += 1; 1798 break; 1799 } 1800 } 1801 } 1802 } 1803 else if (!quickFilterPattern.test(teaser) && !quickFilterPattern.test(entry.file)) { 1804 continue; 1805 } 1806 1807 if (pinnedThreads[id] >= 0) { 1808 pinned = onTop = true; 1809 } 1810 1811 threads.push( 1812 { 1813 id: id, 1814 entry: entry, 1815 pinned: pinned, 1816 onTop: onTop, 1817 hl: hl 1818 } 1819 ); 1820 } 1821 1822 filteredThreadsCount = filtered; 1823 1824 return threads; 1825 } 1826 1827 function formatImageThreads(threads) { 1828 var 1829 i, k, id, entry, item, thread, hl, onTop, pinned, spoiler, 1830 rDiff, html, provider, contentUrl, 1831 pinhl, newtab, watchKey, teaser, topHtml, stickyHtml, 1832 ratio, maxSize, imgWidth, imgHeight, calcSize, 1833 capcodeReplies, capcodeReply, capcodeTitle, page; 1834 1835 provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; 1836 contentUrl = 'i.4cdn.org/' + catalog.slug + '/'; 1837 1838 calcSize = !options.large; 1839 newtab = activeTheme.newtab ? 'target="_blank" ' : ''; 1840 1841 if (catalog.custom_spoiler) { 1842 spoiler = options.imgspoiler + '-' + catalog.slug + catalog.custom_spoiler + '.png'; 1843 } 1844 else { 1845 spoiler = options.imgspoiler + '.png'; 1846 } 1847 1848 html = ''; 1849 topHtml = ''; 1850 stickyHtml = ''; 1851 1852 for (i = 0; item = threads[i]; ++i) { 1853 id = item.id; 1854 entry = item.entry; 1855 hl = item.hl; 1856 onTop = item.onTop; 1857 pinned = item.pinned; 1858 1859 if (entry.sub) { 1860 teaser = '<b>' + entry.sub + '</b>'; 1861 if (entry.teaser) { 1862 teaser += ': ' + entry.teaser; 1863 } 1864 } 1865 else { 1866 teaser = entry.teaser; 1867 } 1868 1869 thread = '<div id="thread-' + id + '" class="thread">'; 1870 1871 if (hasThreadWatcher) { 1872 watchKey = id + '-' + catalog.slug; 1873 thread += '<span id="leaf-' + id + '" data-watch="' + id + '" ' 1874 + (ThreadWatcher.watched[watchKey] ? 1875 'title="Unwatch" class="unwatchIcon"></span>' : 1876 'title="Watch" class="watchIcon"></span>'); 1877 } 1878 1879 thread += '<a ' + newtab + 'href="' + provider + id + '"' 1880 + (entry.imgspoiler ? ' class="imgspoiler"' : '') 1881 + '><img loading="lazy" alt="" id="thumb-' + id + '" class="thumb'; 1882 1883 if (hl.color) { 1884 pinhl = ' hl" style="border-color: ' + hl.color; 1885 } 1886 else if (pinned) { 1887 pinhl = ' pinned'; 1888 } 1889 else { 1890 pinhl = ''; 1891 } 1892 1893 if (entry.imgurl) { 1894 if (entry.imgspoiler && !activeTheme.nospoiler) { 1895 thread += pinhl + '" src="' + spoiler; 1896 } 1897 else { 1898 imgWidth = entry.tn_w; 1899 imgHeight = entry.tn_h; 1900 1901 if (calcSize) { 1902 maxSize = options.smallsize; 1903 if (imgWidth > maxSize) { 1904 ratio = maxSize / imgWidth; 1905 imgWidth = maxSize; 1906 imgHeight = imgHeight * ratio; 1907 } 1908 if (imgHeight > maxSize) { 1909 ratio = maxSize / imgHeight; 1910 imgHeight = maxSize; 1911 imgWidth = imgWidth * ratio; 1912 } 1913 } 1914 thread += pinhl + '" width="' + imgWidth 1915 + '" height="' + imgHeight + '" src="//' 1916 + contentUrl + entry.imgurl + 's.jpg'; 1917 } 1918 } 1919 else if (entry.imgdel) { 1920 thread += ' imgdel' + pinhl + '" src="' + options.imgdel; 1921 } 1922 else { 1923 thread += ' nofile' + pinhl + '" src="' + options.nofile; 1924 } 1925 1926 thread += '" data-id="' + id + '" /></a>'; 1927 1928 if (entry.sticky || entry.closed || entry.capcodereps) { 1929 thread += '<div class="threadIcons">'; 1930 if (entry.sticky) { 1931 thread += '<span title="Sticky" class="threadIcon stickyIcon"></span>'; 1932 } 1933 if (entry.closed) { 1934 thread += '<span title="Closed" class="threadIcon closedIcon"></span>'; 1935 } 1936 if (entry.capcodereps) { 1937 capcodeReplies = entry.capcodereps.split(','); 1938 for (k = 0; capcodeReply = capcodeReplies[k]; ++k) { 1939 if (capcodeTitle = capcodeMap[capcodeReply]) { 1940 thread += '<span title="' 1941 + capcodeTitle + ' Replies" class="threadIcon ' 1942 + capcodeReply + 'Icon"></span>'; 1943 } 1944 } 1945 } 1946 thread += '</div>'; 1947 } 1948 1949 thread += '<div title="(R)eplies / (I)mage Replies' 1950 + (onTop ? ' / (P)age' : '') + '" id="meta-' + id + '" class="meta">'; 1951 1952 if (entry.bumplimit) { 1953 thread += '<i>R: <b>' + entry.r + '</b></i>'; 1954 } 1955 else { 1956 thread += 'R: <b>' + entry.r + '</b>'; 1957 } 1958 if (pinned) { 1959 rDiff = entry.r - pinnedThreads[id]; 1960 if (rDiff > 0) { 1961 thread += ' (+' + rDiff + ')'; 1962 pinnedThreads[id] = entry.r; 1963 } 1964 else { 1965 thread += '(+0)'; 1966 } 1967 } 1968 if (entry.i) { 1969 if (entry.imagelimit) { 1970 thread += ' / <i>I: <b>' + entry.i + '</b></i>'; 1971 } 1972 else { 1973 thread += ' / I: <b>' + entry.i + '</b>'; 1974 } 1975 } 1976 1977 if (onTop && (page = getThreadPage(id)) >= 0) { 1978 thread += ' / P: <b>' + page + '</b>'; 1979 } 1980 1981 thread += '<a href="#" class="postMenuBtn" title="Thread Menu" ' 1982 + 'data-post-menu="' + id + '">▶</a>'; 1983 1984 thread += '</div>'; 1985 1986 if (teaser) { 1987 thread += '<div class="teaser'; 1988 if (hl.color) { 1989 thread += ' style="color:' + hl.color; 1990 } 1991 thread += '">' + teaser + '</div>'; 1992 } 1993 1994 if (window.partyHats) { 1995 thread = '<div class="party-cnt">' + thread 1996 + '</div><img class="party-hat" src="//s.4cdn.org/image/' 1997 + window.partyHats + '"></div>'; 1998 } 1999 else { 2000 thread += '</div>'; 2001 } 2002 2003 if (entry.sticky) { 2004 stickyHtml += thread; 2005 } 2006 else if (onTop) { 2007 topHtml += thread; 2008 } 2009 else { 2010 html += thread; 2011 } 2012 } 2013 2014 topHtml = stickyHtml + topHtml; 2015 2016 if (quickFilterPattern && (html === '' && topHtml === '')) { 2017 html = '<div class="error">Nothing Found</div>'; 2018 } 2019 else if (topHtml) { 2020 html = topHtml + html + '<div class="clear"></div>'; 2021 } 2022 else { 2023 html += '<div class="clear"></div>'; 2024 } 2025 2026 return html; 2027 } 2028 2029 function formatTextThreads(threads) { 2030 var 2031 i, id, entry, item, thread, hl, onTop, pinned, 2032 rDiff, html, provider, 2033 pinhl, newtab, topHtml, aTag; 2034 2035 provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; 2036 2037 newtab = activeTheme.newtab ? 'target="_blank" ' : ''; 2038 2039 html = ''; 2040 topHtml = ''; 2041 2042 for (i = 0; item = threads[i]; ++i) { 2043 id = item.id; 2044 entry = item.entry; 2045 hl = item.hl; 2046 onTop = item.onTop; 2047 pinned = item.pinned; 2048 2049 if (hl.color) { 2050 pinhl = ' class="hl" style="box-shadow: -3px 0 ' + hl.color + '"'; 2051 } 2052 else if (pinned) { 2053 pinhl = ' class="pinned"'; 2054 } 2055 else { 2056 pinhl = ''; 2057 } 2058 2059 aTag = '<a ' + newtab + 'href="' + provider + id + '">'; 2060 2061 thread = '<tr id="thread-' + id + '"' + pinhl 2062 + '><td class="txt-no">' + aTag 2063 + '»</a></td><td class="txt-sub">' + aTag + entry.sub 2064 + '</a></td><td class="txt-rep">'; 2065 2066 if (entry.bumplimit) { 2067 thread += '<i>' + entry.r + '</i>'; 2068 } 2069 else { 2070 thread += entry.r; 2071 } 2072 2073 if (pinned) { 2074 rDiff = entry.r - pinnedThreads[id]; 2075 if (rDiff > 0) { 2076 thread += ' (+' + rDiff + ')'; 2077 pinnedThreads[id] = entry.r; 2078 } 2079 else { 2080 thread += '(+0)'; 2081 } 2082 } 2083 2084 thread += '</td><td class="txt-date" data-id="' + id + '">' + entry.date 2085 + '</td><td class="txt-ctrl"><a href="#" class="postMenuBtn" title="Thread Menu" ' 2086 + 'data-post-menu="' + id + '">▶</a></td></tr>'; 2087 2088 if (onTop) { 2089 topHtml += thread; 2090 } 2091 else { 2092 html += thread; 2093 } 2094 } 2095 2096 if (quickFilterPattern && (html === '' && topHtml === '')) { 2097 html = '<div class="error">Nothing Found</div>'; 2098 } 2099 else if (topHtml) { 2100 html = topHtml + html + '<div class="clear"></div>'; 2101 } 2102 else { 2103 html += '<div class="clear"></div>'; 2104 } 2105 2106 html = '<table><thead><tr><th class="txt-no"></th><th class="txt-sub">Subject</th><th class="txt-rep">Replies</th>' 2107 + '<th class="txt-date">Date</th><th class="txt-ctrl"></th></tr></thead><tbody>' + html + '</tbody></table>'; 2108 2109 return html; 2110 } 2111 2112 function buildThreads() { 2113 var i, tip, fid, threads; 2114 2115 if (catalog.count === 0) { 2116 return; 2117 } 2118 2119 if ($threads.hasChildNodes()) { 2120 if (tip = document.getElementById('th-tip')) { 2121 document.body.removeChild(tip); 2122 } 2123 $threads.textContent = ''; 2124 } 2125 2126 hiddenThreadsCount = 0; 2127 filteredThreadsCount = 0; 2128 2129 for (fid in activeFilters) { 2130 activeFilters[fid].hits = 0; 2131 } 2132 2133 threads = getFilteredThreads(); 2134 2135 sortThreadList(threads); 2136 2137 if (!window.text_only) { 2138 $threads.innerHTML = formatImageThreads(threads); 2139 } 2140 else { 2141 $threads.innerHTML = formatTextThreads(threads); 2142 } 2143 2144 for (i in pinnedThreads) { 2145 localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); 2146 break; 2147 } 2148 2149 setFilteredCount(filteredThreadsCount); 2150 2151 setHiddenCount(hiddenThreadsCount); 2152 } 2153 2154 function onThreadMouseOver(e) { 2155 var t = e.target; 2156 2157 if ($.hasClass(t, 'thumb') || (window.text_only && $.hasClass(t, 'txt-date'))) { 2158 clearTimeout(tooltipTimeout); 2159 if (hasTooltip) { 2160 hideTooltip(); 2161 } 2162 tooltipTimeout = setTimeout(showTooltip, options.tipdelay, t); 2163 } 2164 } 2165 2166 function onThreadMouseOut() { 2167 clearTimeout(tooltipTimeout); 2168 if (hasTooltip) { 2169 hideTooltip(); 2170 } 2171 } 2172 2173 function showTooltip(t) { 2174 var now, tip, el, rect, docWidth, style, page, tid, thread, top, 2175 bottom, docHeight, left; 2176 2177 now = Date.now() / 1000; 2178 2179 rect = t.getBoundingClientRect(); 2180 docWidth = document.documentElement.offsetWidth; 2181 2182 tid = t.getAttribute('data-id'); 2183 2184 if (!tid) { 2185 return; 2186 } 2187 2188 thread = catalog.threads[tid]; 2189 2190 if (page = getThreadPage(tid)) { 2191 page = '<span class="post-page">Page ' + page + '</span>'; 2192 } 2193 else { 2194 page = ''; 2195 } 2196 2197 if (thread.sub && !window.text_only) { 2198 tip = '<span class="post-subject">' + thread.sub + '</span>'; 2199 } 2200 else { 2201 tip = 'Posted'; 2202 } 2203 2204 tip += ' by <span class="' 2205 + (thread.capcode ? (thread.capcode + '-capcode ') : '') 2206 + 'post-author">' + (thread.author || catalog.anon); 2207 2208 if (thread.trip) { 2209 tip += ' <span class="post-tripcode">' + thread.trip + '</span>'; 2210 } 2211 2212 if (thread.capcode) { 2213 tip += ' ## ' 2214 + capcodeMap[thread.capcode]; 2215 } 2216 2217 tip += '</span> '; 2218 2219 if (catalog.flags && thread.country) { 2220 tip += '<div class="flag flag-' + thread.country.toLowerCase() + '"></div> '; 2221 } 2222 2223 tip += '<span class="post-ago">' 2224 + getDuration(now - thread.date) 2225 + ' ago</span>' + page; 2226 2227 if ((!options.extended && thread.teaser) || window.text_only) { 2228 tip += '<p class="post-teaser">' + thread.teaser + '</p>'; 2229 } 2230 2231 if (thread.lr.date) { 2232 tip += '<div class="post-last">Last reply by <span class="' 2233 + (thread.lr.capcode ? (thread.lr.capcode + '-capcode ') : '') 2234 + 'post-author">' + thread.lr.author; 2235 2236 if (thread.lr.trip) { 2237 tip += ' <span class="post-tripcode">' + thread.lr.trip + '</span>'; 2238 } 2239 2240 if (thread.lr.capcode) { 2241 tip += ' ## ' 2242 + thread.lr.capcode.charAt(0).toUpperCase() 2243 + thread.lr.capcode.slice(1); 2244 } 2245 2246 if (thread.lr.date) { 2247 tip += '</span> <span class="post-ago">' 2248 + getDuration(now - thread.lr.date) 2249 + ' ago</span>'; 2250 } 2251 else { 2252 tip += '</span>'; 2253 } 2254 } 2255 2256 el = document.createElement('div'); 2257 el.id = 'post-preview'; 2258 el.innerHTML = tip; 2259 document.body.appendChild(el); 2260 2261 if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { 2262 left = rect.left - el.offsetWidth - 5; 2263 } 2264 else { 2265 left = rect.left + rect.width + 5; 2266 } 2267 2268 docHeight = document.documentElement.clientHeight; 2269 2270 bottom = rect.top + el.offsetHeight; 2271 2272 if (bottom > docHeight) { 2273 top = rect.top - (bottom - docHeight) - 20; 2274 } 2275 else { 2276 top = rect.top; 2277 } 2278 2279 if (top < 0) { 2280 top = 3; 2281 } 2282 2283 style = el.style; 2284 style.left = left + window.pageXOffset + 'px'; 2285 style.top = top + window.pageYOffset + 'px'; 2286 2287 hasTooltip = true; 2288 } 2289 2290 function hideTooltip() { 2291 document.body.removeChild($.id('post-preview')); 2292 hasTooltip = false; 2293 } 2294 2295 function getDuration(delta, precise) { 2296 var count, head, tail; 2297 if (delta < 2) { 2298 return 'less than a second'; 2299 } 2300 if (precise && delta < 300) { 2301 return (0 | delta) + ' seconds'; 2302 } 2303 if (delta < 60) { 2304 return (0 | delta) + ' seconds'; 2305 } 2306 if (delta < 3600) { 2307 count = 0 | (delta / 60); 2308 if (count > 1) { 2309 return count + ' minutes'; 2310 } 2311 else { 2312 return 'one minute'; 2313 } 2314 } 2315 if (delta < 86400) { 2316 count = 0 | (delta / 3600); 2317 if (count > 1) { 2318 head = count + ' hours'; 2319 } 2320 else { 2321 head = 'one hour'; 2322 } 2323 tail = 0 | (delta / 60 - count * 60); 2324 if (tail > 1) { 2325 head += ' and ' + tail + ' minutes'; 2326 } 2327 return head; 2328 } 2329 count = 0 | (delta / 86400); 2330 if (count > 1) { 2331 head = count + ' days'; 2332 } 2333 else { 2334 head = 'one day'; 2335 } 2336 tail = 0 | (delta / 3600 - count * 24); 2337 if (tail > 1) { 2338 head += ' and ' + tail + ' hours'; 2339 } 2340 return head; 2341 } 2342 }; 2343 2344 var Filter = {}; 2345 2346 Filter.init = function() { 2347 this.entities = document.createElement('div'); 2348 Filter.load(); 2349 }; 2350 2351 Filter.match = function(post, board) { 2352 var i, com, f, filters, hit; 2353 2354 hit = false; 2355 filters = Filter.activeFilters; 2356 2357 for (i = 0; f = filters[i]; ++i) { 2358 // boards 2359 if (!f.boards[board]) { 2360 continue; 2361 } 2362 // tripcode 2363 if (f.type == 0) { 2364 if (f.pattern === post.trip) { 2365 hit = true; 2366 break; 2367 } 2368 } 2369 // name 2370 else if (f.type == 1) { 2371 if (f.pattern === post.name) { 2372 hit = true; 2373 break; 2374 } 2375 } 2376 // comment 2377 else if (f.type == 2 && post.com) { 2378 if (com === undefined) { 2379 this.entities.innerHTML 2380 = post.com.replace(/<br>/g, '\n').replace(/[<[^>]+>/g, ''); 2381 com = this.entities.textContent; 2382 } 2383 if (f.pattern.test(com)) { 2384 hit = true; 2385 break; 2386 } 2387 } 2388 // user id 2389 else if (f.type == 4) { 2390 if (f.pattern === post.id) { 2391 hit = true; 2392 break; 2393 } 2394 } 2395 // subject 2396 else if (f.type == 5) { 2397 if (f.pattern.test(post.sub)) { 2398 hit = true; 2399 break; 2400 } 2401 } 2402 // filename 2403 else if (f.type == 6) { 2404 if (f.pattern.test(post.filename)) { 2405 hit = true; 2406 break; 2407 } 2408 } 2409 } 2410 2411 return hit; 2412 }; 2413 2414 FC.getDocTopOffset = function() { 2415 if (window.Config.dropDownNav && !window.Config.autoHideNav) { 2416 return $.id( 2417 window.Config.classicNav ? 'boardNavDesktop' : 'boardNavMobile' 2418 ).offsetHeight; 2419 } 2420 else { 2421 return 0; 2422 } 2423 }; 2424 2425 Filter.load = function() { 2426 var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType, 2427 wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard, boards, 2428 pattern, match, tmp; 2429 2430 this.activeFilters = []; 2431 2432 if (!(rawFilters = localStorage.getItem('4chan-filters'))) { 2433 return; 2434 } 2435 2436 rawFilters = JSON.parse(rawFilters); 2437 2438 regexEscape = new RegExp('(\\' 2439 + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') 2440 + ')', 'g'); 2441 regexType = /^\/(.*)\/(i?)$/; 2442 wordSepS = '(?=.*\\b'; 2443 wordSepE = '\\b)'; 2444 regexWildcard = /\\\*/g; 2445 replaceWildcard = '[^\\s]*'; 2446 2447 try { 2448 for (fid = 0; f = rawFilters[fid]; ++fid) { 2449 if (f.active && f.pattern !== '') { 2450 // Boards 2451 if (f.boards) { 2452 tmp = f.boards.split(/[^a-z0-9]+/i); 2453 boards = {}; 2454 for (i = 0; j = tmp[i]; ++i) { 2455 boards[j] = true; 2456 } 2457 } 2458 else { 2459 boards = false; 2460 } 2461 2462 rawPattern = f.pattern; 2463 // Name, Tripcode or ID, string comparison 2464 if (!f.type || f.type == 1 || f.type == 4) { 2465 pattern = rawPattern; 2466 } 2467 // /RegExp/ 2468 else if (match = rawPattern.match(regexType)) { 2469 pattern = new RegExp(match[1], match[2]); 2470 } 2471 // "Exact match" 2472 else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { 2473 pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); 2474 } 2475 // Full words, AND operator 2476 else { 2477 words = rawPattern.split(' '); 2478 pattern = ''; 2479 for (i = 0, j = words.length; i < j; ++i) { 2480 inner = words[i] 2481 .replace(regexEscape, '\\$1') 2482 .replace(regexWildcard, replaceWildcard); 2483 pattern += wordSepS + inner + wordSepE; 2484 } 2485 pattern = new RegExp('^' + pattern, 'im'); 2486 } 2487 //console.log('Resulting pattern: ' + pattern); 2488 this.activeFilters.push({ 2489 type: f.type, 2490 pattern: pattern, 2491 boards: boards, 2492 color: f.color, 2493 hide: f.hide, 2494 auto: f.auto 2495 }); 2496 } 2497 } 2498 } 2499 catch (e) { 2500 alert('There was an error processing one of the filters: ' 2501 + e + ' in: ' + rawPattern); 2502 } 2503 }; 2504 2505 /** 2506 * Thread watcher 2507 */ 2508 var ThreadWatcher = { 2509 hasFilters: false 2510 }; 2511 2512 ThreadWatcher.init = function() { 2513 var cnt, pos, el; 2514 2515 if (this.hasFilters) { 2516 Filter.init(); 2517 } 2518 2519 this.listNode = null; 2520 this.charLimit = 45; 2521 this.watched = {}; 2522 this.blacklisted = {}; 2523 this.isRefreshing = false; 2524 2525 if (FC.hasMobileLayout) { 2526 el = document.createElement('a'); 2527 el.href = '#'; 2528 el.textContent = 'TW'; 2529 el.addEventListener('click', ThreadWatcher.toggleList, false); 2530 cnt = $.id('settingsWindowLinkMobile'); 2531 cnt.parentNode.insertBefore(el, cnt); 2532 cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); 2533 } 2534 2535 cnt = document.createElement('div'); 2536 cnt.id = 'threadWatcher'; 2537 cnt.setAttribute('data-trackpos', 'TW-position'); 2538 2539 if (FC.hasMobileLayout) { 2540 cnt.style.display = 'none'; 2541 } 2542 else { 2543 if (window.Config['TW-position']) { 2544 cnt.style.cssText = window.Config['TW-position']; 2545 } 2546 else { 2547 cnt.style.left = '10px'; 2548 cnt.style.top = '75px'; 2549 } 2550 } 2551 2552 cnt.innerHTML = '<div class="drag" id="twHeader">' 2553 + (FC.hasMobileLayout ? ('<div id="twClose" class="icon closeIcon"></div>') : '') 2554 + 'Thread Watcher' 2555 + (UA.hasCORS ? ('<div id="twPrune" class="icon refreshIcon" title="Refresh"></div></div>') : '</div>'); 2556 2557 this.listNode = document.createElement('ul'); 2558 this.listNode.id = 'watchList'; 2559 2560 this.load(); 2561 2562 this.build(); 2563 2564 cnt.appendChild(this.listNode); 2565 document.body.appendChild(cnt); 2566 cnt.addEventListener('mouseup', this.onClick, false); 2567 Draggable.set($.id('twHeader')); 2568 window.addEventListener('storage', this.syncStorage, false); 2569 2570 if (!FC.hasMobileLayout && this.canAutoRefresh()) { 2571 this.refresh(); 2572 } 2573 }; 2574 2575 ThreadWatcher.unInit = function() { 2576 var cnt; 2577 2578 if (cnt = $.id('threadWatcher')) { 2579 cnt.removeEventListener('mouseup', this.onClick, false); 2580 Draggable.unset($.id('twHeader')); 2581 window.removeEventListener('storage', this.syncStorage, false); 2582 document.body.removeChild(cnt); 2583 } 2584 }; 2585 2586 ThreadWatcher.toggleList = function(e) { 2587 var el = $.id('threadWatcher'); 2588 2589 e && e.preventDefault(); 2590 2591 if (ThreadWatcher.canAutoRefresh()) { 2592 ThreadWatcher.refresh(); 2593 } 2594 2595 if (el.style.display == 'none') { 2596 el.style.top = (window.pageYOffset + 30) + 'px'; 2597 el.style.display = ''; 2598 } 2599 else { 2600 el.style.display = 'none'; 2601 } 2602 }; 2603 2604 ThreadWatcher.syncStorage = function(e) { 2605 var key; 2606 2607 if (!e.key) { 2608 return; 2609 } 2610 2611 key = e.key.split('-'); 2612 2613 if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) { 2614 ThreadWatcher.load(); 2615 ThreadWatcher.build(); 2616 } 2617 }; 2618 2619 ThreadWatcher.load = function() { 2620 var storage; 2621 2622 if (storage = localStorage.getItem('4chan-watch')) { 2623 this.watched = JSON.parse(storage); 2624 } 2625 if (storage = localStorage.getItem('4chan-watch-bl')) { 2626 this.blacklisted = JSON.parse(storage); 2627 } 2628 }; 2629 2630 ThreadWatcher.build = function() { 2631 var html, tuid, key, cls; 2632 2633 html = ''; 2634 2635 for (key in this.watched) { 2636 tuid = key.split('-'); 2637 html += '<li id="watch-' + key 2638 + '"><span class="pointer" data-cmd="unwatch" data-id="' 2639 + tuid[0] + '" data-board="' + tuid[1] + '">×</span> <a href="' 2640 + this.linkToThread(tuid[0], tuid[1], this.watched[key][1]) + '"'; 2641 2642 if (this.watched[key][1] == -1) { 2643 html += ' class="deadlink">'; 2644 } 2645 else { 2646 cls = []; 2647 2648 if (this.watched[key][3]) { 2649 cls.push('archivelink'); 2650 } 2651 2652 if (this.watched[key][4]) { 2653 cls.push('hasYouReplies'); 2654 html += ' title="This thread has replies to your posts"'; 2655 } 2656 2657 if (this.watched[key][2]) { 2658 html += ' class="' + (cls[0] ? (cls.join(' ') + ' ') : '') 2659 + 'hasNewReplies">(' + this.watched[key][2] + ') '; 2660 } 2661 else { 2662 html += (cls[0] ? ('class="' + cls.join(' ') + '"') : '') + '>'; 2663 } 2664 } 2665 2666 html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '</a></li>'; 2667 } 2668 2669 ThreadWatcher.listNode.innerHTML = html; 2670 }; 2671 2672 ThreadWatcher.onClick = function(e) { 2673 var t = e.target; 2674 2675 if (t.hasAttribute('data-id')) { 2676 ThreadWatcher.toggle( 2677 t.getAttribute('data-id'), 2678 t.getAttribute('data-board') 2679 ); 2680 } 2681 else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { 2682 ThreadWatcher.refreshWithAutoWatch(); 2683 } 2684 else if (t.id == 'twClose') { 2685 ThreadWatcher.toggleList(); 2686 } 2687 }; 2688 2689 ThreadWatcher.generateLabel = function(sub, com, tid) { 2690 var label; 2691 2692 if (label = sub) { 2693 label = label.slice(0, this.charLimit); 2694 } 2695 else if (label = com) { 2696 label = label.replace(/(?:<br>)+/g, ' ') 2697 .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); 2698 } 2699 else { 2700 label = 'No.' + tid; 2701 } 2702 2703 return label; 2704 }; 2705 2706 ThreadWatcher.toggle = function(tid, board, sub, com, lr) { 2707 var key, label, lastReply, icon; 2708 2709 key = tid + '-' + board; 2710 icon = $.id('leaf-' + tid); 2711 2712 if (this.watched[key]) { 2713 delete this.watched[key]; 2714 if (icon) { 2715 icon.className = 'watchIcon'; 2716 icon.title = 'Watch'; 2717 } 2718 } 2719 else { 2720 label = ThreadWatcher.generateLabel(sub, com, tid); 2721 2722 lastReply = lr || tid; 2723 2724 this.watched[key] = [ label, lastReply, 0 ]; 2725 2726 icon.className = 'unwatchIcon'; 2727 icon.title = 'Unwatch'; 2728 } 2729 this.save(); 2730 this.load(); 2731 this.build(); 2732 }; 2733 2734 ThreadWatcher.addRaw = function(post, board) { 2735 var key, label; 2736 2737 key = post.no + '-' + board; 2738 2739 if (this.watched[key]) { 2740 return; 2741 } 2742 2743 label = ThreadWatcher.generateLabel(post.sub, post.com, post.no); 2744 2745 this.watched[key] = [ label, 0, 0 ]; 2746 }; 2747 2748 ThreadWatcher.save = function() { 2749 var i; 2750 2751 ThreadWatcher.sortByBoard(); 2752 2753 localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); 2754 2755 //StorageSync.sync('4chan-watch'); 2756 2757 for (i in ThreadWatcher.blacklisted) { 2758 localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted)); 2759 //StorageSync.sync('4chan-watch-bl'); 2760 break; 2761 } 2762 }; 2763 2764 ThreadWatcher.sortByBoard = function() { 2765 var i, self, key, sorted, keys; 2766 2767 self = ThreadWatcher; 2768 2769 sorted = {}; 2770 keys = []; 2771 2772 for (key in self.watched) { 2773 keys.push(key); 2774 } 2775 2776 keys.sort(function(a, b) { 2777 a = a.split('-')[1]; 2778 b = b.split('-')[1]; 2779 2780 if (a < b) { 2781 return -1; 2782 } 2783 if (a > b) { 2784 return 1; 2785 } 2786 return 0; 2787 }); 2788 2789 for (i = 0; key = keys[i]; ++i) { 2790 sorted[key] = self.watched[key]; 2791 } 2792 2793 self.watched = sorted; 2794 }; 2795 2796 ThreadWatcher.canAutoRefresh = function() { 2797 var time; 2798 2799 if (time = localStorage.getItem('4chan-tw-timestamp')) { 2800 return Date.now() - (+time) >= 60000; 2801 } 2802 return false; 2803 }; 2804 2805 ThreadWatcher.setRefreshTimestamp = function() { 2806 localStorage.setItem('4chan-tw-timestamp', Date.now()); 2807 //StorageSync.sync('4chan-tw-timestamp'); 2808 }; 2809 2810 ThreadWatcher.refreshWithAutoWatch = function() { 2811 var i, f, count, board, boards, img; 2812 2813 if (!this.hasFilters) { 2814 this.refresh(); 2815 return; 2816 } 2817 2818 Filter.load(); 2819 2820 boards = {}; 2821 count = 0; 2822 2823 for (i = 0; f = Filter.activeFilters[i]; ++i) { 2824 if (!f.auto || !f.boards) { 2825 continue; 2826 } 2827 for (board in f.boards) { 2828 if (boards[board]) { 2829 continue; 2830 } 2831 boards[board] = true; 2832 ++count; 2833 } 2834 } 2835 2836 if (!count) { 2837 this.refresh(); 2838 return; 2839 } 2840 2841 img = $.id('twPrune'); 2842 img.className = 'icon rotateIcon'; 2843 this.isRefreshing = true; 2844 2845 this.fetchCatalogs(boards, count); 2846 }; 2847 2848 ThreadWatcher.fetchCatalogs = function(boards, count) { 2849 var to, board, catalogs, meta; 2850 2851 catalogs = {}; 2852 meta = { count: count }; 2853 to = 0; 2854 2855 for (board in boards) { 2856 setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta); 2857 to += 200; 2858 } 2859 }; 2860 2861 ThreadWatcher.parseCatalogJSON = function(data) { 2862 var catalog; 2863 2864 try { 2865 catalog = JSON.parse(data); 2866 } 2867 catch (e) { 2868 console.log(e); 2869 catalog = []; 2870 } 2871 2872 return catalog; 2873 }; 2874 2875 ThreadWatcher.fetchCatalog = function(board, catalogs, meta) { 2876 var xhr; 2877 2878 xhr = new XMLHttpRequest(); 2879 xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json'); 2880 xhr.onload = function() { 2881 meta.count--; 2882 catalogs[board] = ThreadWatcher.parseCatalogJSON(this.responseText); 2883 if (!meta.count) { 2884 ThreadWatcher.onCatalogsLoaded(catalogs); 2885 } 2886 }; 2887 xhr.onerror = function() { 2888 meta.count--; 2889 if (!meta.count) { 2890 ThreadWatcher.onCatalogsLoaded(catalogs); 2891 } 2892 }; 2893 xhr.send(null); 2894 }; 2895 2896 ThreadWatcher.onCatalogsLoaded = function(catalogs) { 2897 var i, j, board, page, pages, threads, thread, key, blacklisted; 2898 2899 $.id('twPrune').className = 'icon rotateIcon'; 2900 this.isRefreshing = false; 2901 2902 blacklisted = {}; 2903 2904 for (board in catalogs) { 2905 pages = catalogs[board]; 2906 for (i = 0; page = pages[i]; ++i) { 2907 threads = page.threads; 2908 for (j = 0; thread = threads[j]; ++j) { 2909 key = thread.no + '-' + board; 2910 if (this.blacklisted[key]) { 2911 blacklisted[key] = 1; 2912 continue; 2913 } 2914 if (Filter.match(thread, board)) { 2915 this.addRaw(thread, board); 2916 } 2917 } 2918 } 2919 } 2920 2921 this.blacklisted = blacklisted; 2922 this.build(true); 2923 this.refresh(); 2924 }; 2925 2926 ThreadWatcher.refresh = function() { 2927 var i, to, key, total, img; 2928 2929 if (total = $.id('watchList').children.length) { 2930 i = to = 0; 2931 img = $.id('twPrune'); 2932 img.className = 'icon rotateIcon'; 2933 ThreadWatcher.isRefreshing = true; 2934 ThreadWatcher.setRefreshTimestamp(); 2935 for (key in ThreadWatcher.watched) { 2936 setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); 2937 to += 200; 2938 } 2939 } 2940 }; 2941 2942 ThreadWatcher.onRefreshEnd = function(img) { 2943 img.className = 'icon refreshIcon'; 2944 this.isRefreshing = false; 2945 this.save(); 2946 this.load(); 2947 this.build(); 2948 }; 2949 2950 ThreadWatcher.parseThreadJSON = function(data) { 2951 var thread; 2952 2953 try { 2954 thread = JSON.parse(data).posts; 2955 } 2956 catch (e) { 2957 console.log(e); 2958 thread = []; 2959 } 2960 2961 return thread; 2962 }; 2963 2964 ThreadWatcher.getTrackedReplies = function(board, tid) { 2965 var tracked = null; 2966 2967 if (tracked = localStorage.getItem('4chan-track-' + board + '-' + tid)) { 2968 tracked = JSON.parse(tracked); 2969 } 2970 2971 return tracked; 2972 }; 2973 2974 ThreadWatcher.fetch = function(key, img) { 2975 var tuid, xhr, li; 2976 2977 li = $.id('watch-' + key); 2978 2979 if (ThreadWatcher.watched[key][1] == -1) { 2980 delete ThreadWatcher.watched[key]; 2981 li.parentNode.removeChild(li); 2982 if (img) { 2983 ThreadWatcher.onRefreshEnd(img); 2984 } 2985 return; 2986 } 2987 2988 tuid = key.split('-'); // tid, board 2989 2990 xhr = new XMLHttpRequest(); 2991 xhr.onload = function() { 2992 var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j; 2993 if (this.status == 200) { 2994 posts = ThreadWatcher.parseThreadJSON(this.responseText); 2995 lastReply = ThreadWatcher.watched[key][1]; 2996 newReplies = 0; 2997 2998 if (!ThreadWatcher.watched[key][4]) { 2999 trackedReplies = ThreadWatcher.getTrackedReplies(tuid[1], tuid[0]); 3000 3001 if (trackedReplies) { 3002 dummy = document.createElement('div'); 3003 } 3004 } 3005 else { 3006 trackedReplies = null; 3007 } 3008 3009 for (i = posts.length - 1; i >= 1; i--) { 3010 if (posts[i].no <= lastReply) { 3011 break; 3012 } 3013 ++newReplies; 3014 3015 if (trackedReplies) { 3016 dummy.innerHTML = posts[i].com; 3017 quotelinks = $.cls('quotelink', dummy); 3018 3019 if (!quotelinks[0]) { 3020 continue; 3021 } 3022 3023 for (j = 0; q = quotelinks[j]; ++j) { 3024 if (trackedReplies[q.textContent]) { 3025 ThreadWatcher.watched[key][4] = 1; 3026 trackedReplies = null; 3027 break; 3028 } 3029 } 3030 } 3031 } 3032 if (newReplies > ThreadWatcher.watched[key][2]) { 3033 ThreadWatcher.watched[key][2] = newReplies; 3034 } 3035 if (posts[0].archived) { 3036 ThreadWatcher.watched[key][3] = 1; 3037 } 3038 } 3039 else if (this.status == 404) { 3040 ThreadWatcher.watched[key][1] = -1; 3041 } 3042 if (img) { 3043 ThreadWatcher.onRefreshEnd(img); 3044 } 3045 }; 3046 if (img) { 3047 xhr.onerror = xhr.onload; 3048 } 3049 xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); 3050 xhr.send(null); 3051 }; 3052 3053 ThreadWatcher.linkToThread = function(tid, board, post) { 3054 return '//' + location.host + '/' 3055 + board + '/thread/' 3056 + tid + (post > 0 ? ('#p' + post) : ''); 3057 }; 3058 3059 /** 3060 * Draggable helper 3061 */ 3062 var Draggable = { 3063 el: null, 3064 key: null, 3065 scrollX: null, 3066 scrollY: null, 3067 dx: null, dy: null, right: null, bottom: null, 3068 3069 set: function(handle) { 3070 handle.addEventListener('mousedown', Draggable.startDrag, false); 3071 }, 3072 3073 unset: function(handle) { 3074 handle.removeEventListener('mousedown', Draggable.startDrag, false); 3075 }, 3076 3077 startDrag: function(e) { 3078 var self, doc, offs; 3079 3080 if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { 3081 return; 3082 } 3083 3084 e.preventDefault(); 3085 3086 self = Draggable; 3087 doc = document.documentElement; 3088 3089 self.el = this.parentNode; 3090 3091 self.key = self.el.getAttribute('data-trackpos'); 3092 offs = self.el.getBoundingClientRect(); 3093 self.dx = e.clientX - offs.left; 3094 self.dy = e.clientY - offs.top; 3095 self.right = doc.clientWidth - offs.width; 3096 self.bottom = doc.clientHeight - offs.height; 3097 3098 if (getComputedStyle(self.el, null).position != 'fixed') { 3099 self.scrollX = window.pageXOffset; 3100 self.scrollY = window.pageYOffset; 3101 } 3102 else { 3103 self.scrollX = self.scrollY = 0; 3104 } 3105 3106 self.offsetTop = FC.getDocTopOffset(); 3107 3108 document.addEventListener('mouseup', self.endDrag, false); 3109 document.addEventListener('mousemove', self.onDrag, false); 3110 }, 3111 3112 endDrag: function() { 3113 document.removeEventListener('mouseup', Draggable.endDrag, false); 3114 document.removeEventListener('mousemove', Draggable.onDrag, false); 3115 if (Draggable.key && window.Config) { 3116 window.Config[Draggable.key] = Draggable.el.style.cssText; 3117 localStorage.setItem('4chan-settings', JSON.stringify(window.Config)); 3118 //StorageSync.sync('4chan-settings'); 3119 } 3120 delete Draggable.el; 3121 }, 3122 3123 onDrag: function(e) { 3124 var left, top, style; 3125 3126 left = e.clientX - Draggable.dx + Draggable.scrollX; 3127 top = e.clientY - Draggable.dy + Draggable.scrollY; 3128 style = Draggable.el.style; 3129 if (left < 1) { 3130 style.left = '0'; 3131 style.right = ''; 3132 } 3133 else if (Draggable.right < left) { 3134 style.left = ''; 3135 style.right = '0'; 3136 } 3137 else { 3138 style.left = (left / document.documentElement.clientWidth * 100) + '%'; 3139 style.right = ''; 3140 } 3141 if (top <= Draggable.offsetTop) { 3142 style.top = Draggable.offsetTop + 'px'; 3143 style.bottom = ''; 3144 } 3145 else if (Draggable.bottom < top && 3146 Draggable.el.clientHeight < document.documentElement.clientHeight) { 3147 style.bottom = '0'; 3148 style.top = ''; 3149 } 3150 else { 3151 style.top = (top / document.documentElement.clientHeight * 100) + '%'; 3152 style.bottom = ''; 3153 } 3154 } 3155 }; 3156 3157 /** 3158 * Custom Menu 3159 */ 3160 var CustomMenu = { 3161 dropDownNav: false, 3162 classicNav: false 3163 }; 3164 3165 CustomMenu.initCtrl = function(dropDownNav, classicNav) { 3166 var el, cnt; 3167 3168 CustomMenu.dropDownNav = dropDownNav; 3169 CustomMenu.classicNav = classicNav; 3170 3171 el = document.createElement('span'); 3172 el.className = 'custom-menu-ctrl'; 3173 el.innerHTML = '[<a data-cm-edit title="Edit Menu" href="#">Edit</a>]'; 3174 3175 if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { 3176 cnt = $.id('boardSelectMobile').parentNode; 3177 cnt.insertBefore(el, cnt.lastChild); 3178 } 3179 else { 3180 cnt = $.cls('boardList'); 3181 cnt[0] && cnt[0].appendChild(el); 3182 cnt[1] && cnt[1].appendChild(el.cloneNode(true)); 3183 } 3184 }; 3185 /* 3186 CustomMenu.showNWSBoards = function() { 3187 var i, el, nodes, len; 3188 3189 nodes = $.cls('nwsb'); 3190 len = nodes.length; 3191 3192 for (i = len - 1; el = nodes[i]; i--) { 3193 $.removeClass(el, 'nwsb'); 3194 } 3195 }; 3196 */ 3197 CustomMenu.reset = function() { 3198 var i, el, full, custom, navs; 3199 3200 full = $.cls('boardList'); 3201 custom = $.cls('customBoardList'); 3202 navs = $.cls('show-all-boards'); 3203 3204 for (i = 0; el = navs[i]; ++i) { 3205 el.removeEventListener('click', CustomMenu.reset, false); 3206 } 3207 3208 for (i = custom.length - 1; el = custom[i]; i--) { 3209 full[i].style.display = null; 3210 el.parentNode.removeChild(el); 3211 } 3212 }; 3213 3214 CustomMenu.apply = function(str) { 3215 var i, el, cntBottom, board, navs, boardList, cnt; 3216 3217 if (!str) { 3218 if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { 3219 if (el = $.cls('customBoardList')[0]) { 3220 el.parentNode.removeChild(el); 3221 } 3222 } 3223 return; 3224 } 3225 3226 boardList = str.split(/[^0-9a-z]/i); 3227 3228 cnt = document.createElement('span'); 3229 cnt.className = 'customBoardList'; 3230 3231 for (i = 0; board = boardList[i]; ++i) { 3232 if (i) { 3233 cnt.appendChild(document.createTextNode(' / ')); 3234 } 3235 else { 3236 cnt.appendChild(document.createTextNode('[')); 3237 } 3238 el = document.createElement('a'); 3239 el.textContent = board; 3240 el.href = '//boards.' + $L.d(board) + '/' + board + (board !== 'f' ? '/catalog' : ''); 3241 cnt.appendChild(el); 3242 } 3243 3244 cnt.appendChild(document.createTextNode(']')); 3245 3246 if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { 3247 if (el = $.cls('customBoardList')[0]) { 3248 el.parentNode.removeChild(el); 3249 } 3250 navs = $.id('boardSelectMobile'); 3251 navs && navs.parentNode.insertBefore(cnt, navs.nextSibling); 3252 } 3253 else { 3254 cnt.appendChild(document.createTextNode(' [')); 3255 el = document.createElement('a'); 3256 el.textContent = '…'; 3257 el.title = 'Show all'; 3258 el.className = 'show-all-boards pointer'; 3259 cnt.appendChild(el); 3260 cnt.appendChild(document.createTextNode('] ')); 3261 3262 cntBottom = cnt.cloneNode(true); 3263 3264 navs = $.cls('boardList'); 3265 3266 for (i = 0; el = navs[i]; ++i) { 3267 el.style.display = 'none'; 3268 el.parentNode.insertBefore(i ? cntBottom : cnt, el); 3269 } 3270 3271 navs = $.cls('show-all-boards'); 3272 3273 for (i = 0; el = navs[i]; ++i) { 3274 el.addEventListener('click', CustomMenu.reset, false); 3275 } 3276 } 3277 }; 3278 3279 CustomMenu.onClick = function(e) { 3280 var t; 3281 3282 if ((t = e.target) == document) { 3283 return; 3284 } 3285 3286 if (t.hasAttribute('data-close')) { 3287 CustomMenu.closeEditor(); 3288 } 3289 else if (t.hasAttribute('data-save')) { 3290 CustomMenu.save($.id('customMenu').hasAttribute('data-standalone')); 3291 } 3292 }; 3293 3294 CustomMenu.showEditor = function(standalone) { 3295 var cnt, extConfig; 3296 3297 cnt = document.createElement('div'); 3298 cnt.id = 'customMenu'; 3299 cnt.className = 'panel'; 3300 cnt.setAttribute('data-close', '1'); 3301 3302 if (standalone === true) { 3303 cnt.setAttribute('data-standalone', '1'); 3304 } 3305 3306 cnt.innerHTML = '\ 3307 <div class="reply"><div class="panelHeader">Custom Board List\ 3308 <span class="panelCtrl"><span data-close="1" class="icon closeIcon"></span></span></div>\ 3309 <input placeholder="Example: jp tg mu" id="customMenuBox" type="text" value="">\ 3310 <div class="center"><button data-save="1">Save</button></div></div>'; 3311 3312 document.body.appendChild(cnt); 3313 3314 cnt.style.top = window.pageYOffset 3315 + (0 | (document.documentElement.clientHeight / 2) - (cnt.offsetHeight / 2)) + 'px'; 3316 3317 $.removeClass($.id('backdrop'), 'hidden'); 3318 3319 extConfig = CustomMenu.getConfig(); 3320 3321 if (extConfig.customMenuList) { 3322 $.id('customMenuBox').value = extConfig.customMenuList; 3323 } 3324 3325 cnt.addEventListener('click', CustomMenu.onClick, false); 3326 }; 3327 3328 CustomMenu.closeEditor = function() { 3329 var el; 3330 3331 if (el = $.id('customMenu')) { 3332 el.removeEventListener('click', CustomMenu.onClick, false); 3333 document.body.removeChild(el); 3334 $.addClass($.id('backdrop'), 'hidden'); 3335 } 3336 }; 3337 3338 CustomMenu.save = function(standalone) { 3339 var input, extConfig; 3340 3341 if (input = $.id('customMenuBox')) { 3342 if (standalone === true) { 3343 CustomMenu.apply(input.value); 3344 3345 extConfig = CustomMenu.getConfig(); 3346 3347 extConfig.customMenu = true; 3348 extConfig.customMenuList = input.value; 3349 3350 localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); 3351 //StorageSync.sync('4chan-settings'); 3352 } 3353 } 3354 3355 CustomMenu.closeEditor(); 3356 }; 3357 3358 CustomMenu.getConfig = function() { 3359 var extConfig; 3360 3361 if (extConfig = localStorage.getItem('4chan-settings')) { 3362 return JSON.parse(extConfig); 3363 } 3364 else { 3365 return {}; 3366 } 3367 }; 3368 3369 function checkMobileLayout() { 3370 var mobile, desktop; 3371 3372 if (window.matchMedia) { 3373 return window.matchMedia('(max-width: 480px)').matches 3374 && localStorage.getItem('4chan_never_show_mobile') != 'true'; 3375 } 3376 3377 mobile = $.id('boardNavMobile'); 3378 desktop = $.id('boardNavDesktop'); 3379 3380 return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth === 0; 3381 } 3382 3383 var StickyNav = { 3384 thres: 5, 3385 pos: 0, 3386 timeout: null, 3387 el: null, 3388 3389 init: function(classicNav) { 3390 this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); 3391 $.addClass(this.el, 'autohide-nav'); 3392 window.addEventListener('scroll', this.onScroll, false); 3393 }, 3394 3395 destroy: function(classicNav) { 3396 this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); 3397 $.removeClass(this.el, 'autohide-nav'); 3398 window.removeEventListener('scroll', this.onScroll, false); 3399 }, 3400 3401 onScroll: function() { 3402 clearTimeout(StickyNav.timeout); 3403 StickyNav.timeout = setTimeout(StickyNav.checkScroll, 50); 3404 }, 3405 3406 checkScroll: function() { 3407 var thisPos; 3408 3409 thisPos = window.pageYOffset; 3410 3411 if (Math.abs(StickyNav.pos - thisPos) <= StickyNav.thres) { 3412 return; 3413 } 3414 3415 if (thisPos < StickyNav.pos) { 3416 StickyNav.el.style.top = ''; 3417 } 3418 else { 3419 StickyNav.el.style.top = '-' + StickyNav.el.offsetHeight + 'px'; 3420 } 3421 3422 StickyNav.pos = thisPos; 3423 } 3424 }; 3425 3426 FC.panelHTML = { 3427 build: function(id, cls) { 3428 var el; 3429 3430 el = document.createElement('div'); 3431 el.id = id; 3432 el.className = cls; 3433 el.innerHTML = FC.panelHTML[id]; 3434 3435 document.body.appendChild(el); 3436 3437 return el; 3438 }, 3439 3440 'theme': '<div class="panelHeader">Settings<span id="theme-close" class="icon closeIcon" title="Close"></span></div>\ 3441 <h4>Options</h4>\ 3442 <ul class="clickset">\ 3443 <li class="desktop"><label><input id="theme-nobinds" type="checkbox"> Disable keybinds</label></li>\ 3444 <li><label><input id="theme-nospoiler" type="checkbox"> Don\'t spoiler images</label></li>\ 3445 <li><label><input id="theme-newtab" type="checkbox"> Open threads in a new tab</label></li>\ 3446 <li class="desktop"><label><input id="theme-tw" type="checkbox"> Thread Watcher</label></li>\ 3447 <li class="desktop"><label><input id="theme-ddn" type="checkbox"> Use drop-down navigation</label></li>\ 3448 </ul>\ 3449 <h4 class="desktop">Shortcuts</h4>\ 3450 <ul class="clickset desktop">\ 3451 <li><kbd>R</kbd> — Refresh current page</li>\ 3452 <li><kbd>X</kbd> — Reorder threads</li>\ 3453 <li><kbd>S</kbd> — Open search box, <kbd>Esc</kbd> to close</li>\ 3454 <li><kbd>Shift</kbd> <kbd title="Left Mouse Button">LMB</kbd> — Hide threads</li>\ 3455 <li><kbd>Alt</kbd> <kbd title="Left Mouse Button">LMB</kbd> — Pin threads</li>\ 3456 <li><kbd title="Right Mouse Button">RMB</kbd> — Threads context menu (Firefox only)</li>\ 3457 </ul>\ 3458 <h4>Custom CSS</h4>\ 3459 <textarea id="theme-css" rows="4" cols="45"></textarea>\ 3460 <div id="theme-btns">\ 3461 <span id="theme-msg"></span>\ 3462 <div class="center"><button id="theme-save">Save Settings</button></div>\ 3463 </div>', 3464 3465 'filters-protip': '<div class="panelHeader">Filters & Highlights Help<span id="filters-help-close" class="icon closeIcon" title="Close"></span></div>\ 3466 <h4>Patterns</h4>\ 3467 <ul><li><ul>\ 3468 <li><strong>Matching whole words:</strong></li>\ 3469 <li><code>feel</code> — will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.</li>\ 3470 </ul></li>\ 3471 <li><ul>\ 3472 <li><strong>AND operator:</strong></li>\ 3473 <li><code>feel girlfriend</code> — will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.</li>\ 3474 </ul></li>\ 3475 <li><ul>\ 3476 <li><strong>OR operator:</strong></li>\ 3477 <li><code>feel|girlfriend</code> — will match <em>"feel"</em> OR <em>"girlfriend"</em>.</li>\ 3478 </ul></li>\ 3479 <li><ul>\ 3480 <li><strong>Mixing both operators:</strong></li>\ 3481 <li><code>girlfriend|boyfriend feel</code> — matches <em>"feel"</em> AND <em>"girlfriend"</em>, or <em>"feel"</em> AND <em>"boyfriend"</em>.</li>\ 3482 </ul></li>\ 3483 <li><ul>\ 3484 <li><strong>Exact match search:</strong></li>\ 3485 <li><code>"that feel when"</code> — place double quotes around the pattern to search for an exact string</li>\ 3486 </ul></li>\ 3487 <li><ul>\ 3488 <li><strong>Wildcards:</strong></li>\ 3489 <li><code>feel*</code> — matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etc…</li>\ 3490 <li><code>idolm*ster</code> — this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etc…</li>\ 3491 </ul></li>\ 3492 <li><ul>\ 3493 <li><strong>Filtering by name or tripcode:</strong></li>\ 3494 <li>Prefix the pattern with <code>#</code> to search by <em>tripcode</em>: <code>#!Ep8pui8Vw2</code></li>\ 3495 <li>Prefix the pattern with <code>##</code> to search by <em>name</em>: <code>##Anonymous</code></li>\ 3496 <li>To filter by <em>capcode</em>: <code>#!#admin</code>, <code>#!#mod</code>, <code>#!#developer</code></li>\ 3497 </ul></li>\ 3498 <li><ul>\ 3499 <li><strong>It is also possible to filter by regular expression:</strong></li>\ 3500 <li><code>/^(?=.*detachable)(?=.*hats).*$/i</code> — AND operator.</li>\ 3501 <li><code>/^(?!.*touhou).*$/i</code> — NOT operator.</li>\ 3502 <li><code>/^&gt;/</code> — threads starting with a quote (<em>">"</em> character as an html entity).</li>\ 3503 <li><code>/^$/</code> — threads with no text.</li>\ 3504 </ul></li>\ 3505 </ul>\ 3506 <hr>\ 3507 <h4>Controls</h4>\ 3508 <ul>\ 3509 <li><strong>On</strong> — enables or disables the filter.</li>\ 3510 <li><strong>Boards</strong> — space separated list of boards on which the filter will be active. Leave blank to apply to all boards.</li>\ 3511 <li><strong>Hide</strong> — hides matched threads.</li>\ 3512 <li><strong>Top</strong> — puts matched threads on top of the list.</li>\ 3513 </ul>', 3514 3515 'filter-palette': '<div id="colorpicker" class="panel"><table id="filter-color-table"><tbody></tbody><tfoot>\ 3516 <tr><td>Custom</td></tr>\ 3517 <tr><td class="middle-txt"><input class="custom-rgb" type="text" name="custom-rgb" value="" id="filter-rgb"><span title="Select Color" id="filter-rgb-ok" class="button clickbox"></span></td></tr>\ 3518 <tr><td>\ 3519 <span class="btn-wrap"><span id="filter-palette-close" class="button">Close</span></span>\ 3520 <span class="btn-wrap"><span id="filter-palette-clear" class="button">Clear</span></span>\ 3521 </td></tr>\ 3522 </tfoot></table></div>', 3523 3524 'filters': '<div class="panelHeader"><input placeholder="Search" type="text" id="filters-search">Filters & Highlights<span id="filters-help-open" class="icon helpIcon" title="Help"></span><span id="filters-close" class="icon closeIcon" title="Close"></span></div>\ 3525 <table id="filter-table">\ 3526 <thead><tr>\ 3527 <th>Order</th>\ 3528 <th>On</th>\ 3529 <th>Pattern</th>\ 3530 <th>Boards</th>\ 3531 <th>Color</th>\ 3532 <th>Hide</th>\ 3533 <th>Top</th>\ 3534 <th>Del</th>\ 3535 <th></th>\ 3536 </tr></thead>\ 3537 <tbody id="filter-list"></tbody>\ 3538 <tfoot><tr><td colspan="9">\ 3539 <button id="filters-add" class="left">Add</button>\ 3540 <span class="right"><span id="filters-msg"></span><button id="filters-save">Save</button></span>\ 3541 </td></tr></tfoot></table>' 3542 }; 3543 3544 var PostMenu = { 3545 activeBtn: null 3546 }; 3547 3548 PostMenu.open = function(btn, pid, hasThreadWatcher, hidden, pinned) { 3549 var div, html, btnPos, left, limit, tr; 3550 3551 if (PostMenu.activeBtn == btn) { 3552 PostMenu.close(); 3553 return; 3554 } 3555 3556 PostMenu.close(); 3557 3558 tr = btn.parentNode.parentNode; 3559 3560 html = '<ul><li data-report="' + pid + '">Report thread</li>' 3561 + '<li data-pin="' + pid + '">' 3562 + (pinned ? 'Unpin' : 'Pin') + ' thread</li>' 3563 + '<li data-hide="' + pid + '">' 3564 + (hidden ? 'Unhide' : 'Hide') + ' thread</li>'; 3565 3566 if (hasThreadWatcher) { 3567 html += '<li data-watch="' + pid + '">' 3568 + (ThreadWatcher.watched[pid + '-' + catalog.slug] ? 'Remove from' : 'Add to') 3569 + ' watch list</li>'; 3570 } 3571 3572 div = document.createElement('div'); 3573 div.id = 'post-menu'; 3574 div.className = 'dd-menu'; 3575 div.innerHTML = html + '</ul>'; 3576 3577 btnPos = btn.getBoundingClientRect(); 3578 3579 div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; 3580 3581 document.addEventListener('click', PostMenu.close, false); 3582 3583 $.addClass(btn, 'menuOpen'); 3584 PostMenu.activeBtn = btn; 3585 3586 UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: true, node: div.firstElementChild }); 3587 3588 document.body.appendChild(div); 3589 3590 left = btnPos.left + window.pageXOffset; 3591 limit = document.documentElement.clientWidth - div.offsetWidth; 3592 3593 if (left > (limit - 75)) { 3594 div.className += ' dd-menu-left'; 3595 } 3596 3597 if (left > limit) { 3598 left = limit; 3599 } 3600 3601 div.style.left = left + 'px'; 3602 }; 3603 3604 PostMenu.close = function() { 3605 var el; 3606 3607 if (el = $.id('post-menu')) { 3608 el.parentNode.removeChild(el); 3609 document.removeEventListener('click', PostMenu.close, false); 3610 $.removeClass(PostMenu.activeBtn, 'menuOpen'); 3611 PostMenu.activeBtn = null; 3612 } 3613 };