extension-test-sync2.js
1 /******************************** 2 * * 3 * 4chan Extension * 4 * * 5 ********************************/ 6 7 /** 8 * Helpers 9 */ 10 $ = {}; 11 12 $.id = function(id) { 13 return document.getElementById(id); 14 }; 15 16 $.cls = function(klass, root) { 17 return (root || document).getElementsByClassName(klass); 18 }; 19 20 $.byName = function(name) { 21 return document.getElementsByName(name); 22 }; 23 24 $.tag = function(tag, root) { 25 return (root || document).getElementsByTagName(tag); 26 }; 27 28 $.qs = function(sel, root) { 29 return (root || document).querySelector(sel); 30 }; 31 32 $.extend = function(destination, source) { 33 for (var key in source) { 34 destination[key] = source[key]; 35 } 36 }; 37 38 if (!document.documentElement.classList) { 39 $.hasClass = function(el, klass) { 40 return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; 41 }; 42 43 $.addClass = function(el, klass) { 44 el.className = (el.className == '') ? klass : el.className + ' ' + klass; 45 }; 46 47 $.removeClass = function(el, klass) { 48 el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); 49 }; 50 } 51 else { 52 $.hasClass = function(el, klass) { 53 return el.classList.contains(klass); 54 }; 55 56 $.addClass = function(el, klass) { 57 el.classList.add(klass); 58 }; 59 60 $.removeClass = function(el, klass) { 61 el.classList.remove(klass); 62 }; 63 } 64 65 $.get = function(url, callbacks, headers) { 66 var key, xhr; 67 68 xhr = new XMLHttpRequest(); 69 xhr.open('GET', url, true); 70 if (callbacks) { 71 for (key in callbacks) { 72 xhr[key] = callbacks[key]; 73 } 74 } 75 if (headers) { 76 for (key in headers) { 77 xhr.setRequestHeader(key, headers[key]); 78 } 79 } 80 xhr.send(null); 81 return xhr; 82 }; 83 84 $.hash = function(str) { 85 var i, j, msg = 0; 86 for (i = 0, j = str.length; i < j; ++i) { 87 msg = ((msg << 5) - msg) + str.charCodeAt(i); 88 } 89 return msg; 90 }; 91 92 $.prettySeconds = function(fs) { 93 var m, s; 94 95 m = Math.floor(fs / 60); 96 s = Math.round(fs - m * 60); 97 98 return [ m, s ]; 99 }; 100 101 $.docEl = document.documentElement; 102 103 $.cache = {}; 104 105 /** 106 * Parser 107 */ 108 var Parser = {}; 109 110 Parser.init = function() { 111 var o, a, h, m, tail, staticPath, tracked, el; 112 113 if (Config.filter || Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) { 114 this.needMsg = true; 115 } 116 117 staticPath = '//s.4cdn.org/image/'; 118 119 tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif'; 120 121 this.icons = { 122 admin: staticPath + 'adminicon' + tail, 123 mod: staticPath + 'modicon' + tail, 124 dev: staticPath + 'developericon' + tail, 125 manager: staticPath + 'managericon' + tail, 126 del: staticPath + 'filedeleted-res' + tail 127 }; 128 129 this.prettify = typeof prettyPrint == 'function'; 130 131 this.customSpoiler = {}; 132 133 if (Config.localTime) { 134 if (o = (new Date).getTimezoneOffset()) { 135 a = Math.abs(o); 136 h = (0 | (a / 60)); 137 138 this.utcOffset = 'Timezone: UTC' + (o < 0 ? '+' : '-') 139 + h + ((m = a - h * 60) ? (':' + m) : ''); 140 } 141 else { 142 this.utcOffset = 'Timezone: UTC'; 143 } 144 145 this.weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 146 } 147 148 if (Main.tid) { 149 this.trackedReplies = this.getTrackedReplies(Main.tid) || {}; 150 } 151 }; 152 153 Parser.getTrackedReplies = function(tid) { 154 var tracked = null; 155 156 if (tracked = sessionStorage.getItem('4chan-track-' + Main.board + '-' + tid)) { 157 tracked = JSON.parse(tracked); 158 } 159 160 return tracked; 161 }; 162 163 Parser.saveTrackedReplies = function(tid, replies) { 164 sessionStorage.setItem( 165 '4chan-track-' + Main.board + '-' + tid, 166 JSON.stringify(replies) 167 ); 168 }; 169 170 Parser.parseThreadJSON = function(data) { 171 var thread; 172 173 try { 174 thread = JSON.parse(data).posts; 175 } 176 catch (e) { 177 console.log(e); 178 thread = []; 179 } 180 181 return thread; 182 }; 183 184 Parser.parseCatalogJSON = function(data) { 185 var catalog; 186 187 try { 188 catalog = JSON.parse(data); 189 } 190 catch (e) { 191 console.log(e); 192 catalog = []; 193 } 194 195 return catalog; 196 }; 197 198 Parser.setCustomSpoiler = function(board, val) { 199 var s; 200 if (!this.customSpoiler[board] && (val = parseInt(val))) { 201 if (board == Main.board && (s = $.cls('imgspoiler')[0])) { 202 this.customSpoiler[board] = 203 s.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1]; 204 } 205 else { 206 this.customSpoiler[board] = '-' + board 207 + (Math.floor(Math.random() * val) + 1); 208 } 209 } 210 }; 211 212 Parser.buildPost = function(thread, board, pid) { 213 var i, j, el = null; 214 215 for (i = 0; j = thread[i]; ++i) { 216 if (j.no != pid) { 217 continue; 218 } 219 220 if (!Config.revealSpoilers && thread[0].custom_spoiler) { 221 Parser.setCustomSpoiler(board, thread[0].custom_spoiler); 222 } 223 224 el = Parser.buildHTMLFromJSON(j, board, false, true).lastElementChild; 225 226 if (Config.IDColor && (uid = $.cls('posteruid', el)[Main.hasMobileLayout ? 0 : 1])) { 227 IDColor.applyRemote(uid.firstElementChild); 228 } 229 } 230 231 return el; 232 }; 233 234 Parser.decodeSpecialChars = function(str) { 235 return str.replace(/&/g, '&') 236 .replace(/"/g, '"') 237 .replace(/'/g, "'") 238 .replace(/</g, '<') 239 .replace(/>/g, '>'); 240 }; 241 242 Parser.encodeSpecialChars = function(str) { 243 return str.replace(/&/g, '&') 244 .replace(/"/g, '"') 245 .replace(/'/g, ''') 246 .replace(/</g, '<') 247 .replace(/>/g, '>'); 248 }; 249 250 Parser.buildHTMLFromJSON = function(data, board, standalone, fromQuote) { 251 var 252 container = document.createElement('div'), 253 isOP = false, 254 255 userId, 256 fileDims = '', 257 imgSrc = '', 258 fileInfo = '', 259 fileHtml = '', 260 fileThumb, 261 filePath, 262 fileName, 263 fileSpoilerTip = '"', 264 size = '', 265 fileClass = '', 266 shortFile = '', 267 longFile = '', 268 tripcode = '', 269 capcodeStart = '', 270 capcodeClass = '', 271 capcode = '', 272 flag, 273 highlight = '', 274 emailStart = '', 275 emailEnd = '', 276 name, 277 subject, 278 noLink, 279 quoteLink, 280 replySpan = '', 281 noFilename, 282 decodedFilename, 283 mobileLink = '', 284 postType = 'reply', 285 summary = '', 286 postCountStr, 287 resto, 288 capcode_replies = '', 289 threadIcons = '', 290 needFileTip = false, 291 292 i, q, href, quotes, 293 294 imgDir = '//i.4cdn.org/' + board; 295 296 if (data.resto == 0) { 297 isOP = true; 298 299 if (standalone) { 300 mobileLink = '<div class="postLink mobile"><span class="info"></span><a href="' 301 + 'thread/' + data.no + '" class="button">View Thread</a></div>'; 302 postType = 'op'; 303 replySpan = ' <span>[<a href="' 304 + 'thread/' + data.no + (data.semantic_url ? ('/' + data.semantic_url) : '') 305 + '" class="replylink" rel="canonical">Reply</a>]</span>' 306 } 307 308 resto = data.no; 309 } 310 else { 311 resto = data.resto; 312 } 313 314 315 if (!Main.tid || board != Main.board) { 316 noLink = 'thread/' + resto + '#p' + data.no; 317 quoteLink = 'thread/' + resto + '#q' + data.no; 318 } 319 else { 320 noLink = '#p' + data.no; 321 quoteLink = 'javascript:quote(\'' + data.no + '\')'; 322 } 323 324 if (!data.capcode && data.id) { 325 userId = ' <span class="posteruid id_' 326 + data.id + '">(ID: <span class="hand" title="Highlight posts by this ID">' 327 + data.id + '</span>)</span> '; 328 } 329 else { 330 userId = ''; 331 } 332 333 switch (data.capcode) { 334 case 'admin_highlight': 335 highlight = ' highlightPost'; 336 case 'admin': 337 capcodeStart = ' <strong class="capcode hand id_admin"' 338 + 'title="Highlight posts by the Administrator">## Admin</strong>'; 339 capcodeClass = ' capcodeAdmin'; 340 341 capcode = ' <img src="' + Parser.icons.admin + '" ' 342 + 'alt="This user is the 4chan Administrator." ' 343 + 'title="This user is the 4chan Administrator." class="identityIcon">'; 344 break; 345 case 'mod': 346 capcodeStart = ' <strong class="capcode hand id_mod" ' 347 + 'title="Highlight posts by Moderators">## Mod</strong>'; 348 capcodeClass = ' capcodeMod'; 349 350 capcode = ' <img src="' + Parser.icons.mod + '" ' 351 + 'alt="This user is a 4chan Moderator." ' 352 + 'title="This user is a 4chan Moderator." class="identityIcon">'; 353 break; 354 case 'developer': 355 capcodeStart = ' <strong class="capcode hand id_developer" ' 356 + 'title="Highlight posts by Developers">## Developer</strong>'; 357 capcodeClass = ' capcodeDeveloper'; 358 359 capcode = ' <img src="' + Parser.icons.dev + '" ' 360 + 'alt="This user is a 4chan Developer." ' 361 + 'title="This user is a 4chan Developer." class="identityIcon">'; 362 break; 363 case 'manager': 364 capcodeStart = ' <strong class="capcode hand id_manager" ' 365 + 'title="Highlight posts by Managers">## Manager</strong>'; 366 capcodeClass = ' capcodeManager'; 367 368 capcode = ' <img src="' + Parser.icons.manager + '" ' 369 + 'alt="This user is a 4chan Manager." ' 370 + 'title="This user is a 4chan Manager." class="identityIcon">'; 371 break; 372 } 373 374 if (data.email) { 375 emailStart = '<a href="mailto:' + data.email.replace(/ /g, '%20') + '" class="useremail">'; 376 emailEnd = '</a>'; 377 } 378 379 if (data.country) { 380 if (board == 'pol') { 381 flag = ' <img src="//s.4cdn.org/image/country/troll/' 382 + data.country.toLowerCase() + '.gif" alt="' 383 + data.country + '" title="' + data.country_name + '" class="countryFlag">'; 384 } 385 else { 386 flag = ' <span title="' + data.country_name + '" class="flag flag-' 387 + data.country.toLowerCase() + '"></span>'; 388 } 389 } 390 else { 391 flag = ''; 392 } 393 394 if (data.filedeleted) { 395 fileHtml = '<div id="f' + data.no + '" class="file"><span class="fileThumb"><img src="' 396 + Parser.icons.del + '" class="fileDeletedRes" alt="File deleted."></span></div>'; 397 } 398 else if (data.ext) { 399 decodedFilename = Parser.decodeSpecialChars(data.filename); 400 401 shortFile = longFile = data.filename + data.ext; 402 403 if (decodedFilename.length > (isOP ? 40 : 30)) { 404 shortFile = Parser.encodeSpecialChars( 405 decodedFilename.slice(0, isOP ? 35 : 25) 406 ) + '(...)' + data.ext; 407 408 needFileTip = true; 409 } 410 411 if (!data.tn_w && !data.tn_h && data.ext == '.gif') { 412 data.tn_w = data.w; 413 data.tn_h = data.h; 414 } 415 if (data.fsize >= 1048576) { 416 size = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M'; 417 } 418 else if (data.fsize > 1024) { 419 size = (0 | (data.fsize / 1024 + 0.5)) + ' K'; 420 } 421 else { 422 size = data.fsize + ' '; 423 } 424 425 if (data.spoiler) { 426 if (!Config.revealSpoilers) { 427 fileName = 'Spoiler Image'; 428 fileSpoilerTip = '" title="' + longFile + '"'; 429 fileClass = ' imgspoiler'; 430 431 fileThumb = '//s.4cdn.org/image/spoiler' 432 + (Parser.customSpoiler[board] || '') + '.png'; 433 data.tn_w = 100; 434 data.tn_h = 100; 435 436 noFilename = true; 437 } 438 else { 439 fileName = shortFile; 440 } 441 } 442 else { 443 fileName = shortFile; 444 } 445 446 if (!fileThumb) { 447 fileThumb = '//0.t.4cdn.org/' + board + '/' + data.tim + 's.jpg'; 448 } 449 450 fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h; 451 452 if (board != 'f') { 453 filePath = imgDir + '/' + data.tim + data.ext; 454 455 imgSrc = '<a class="fileThumb' + fileClass + '" href="' + filePath 456 + '" target="_blank"><img src="' + fileThumb 457 + '" alt="' + size + 'B" data-md5="' + data.md5 458 + '" style="height: ' + data.tn_h + 'px; width: ' 459 + data.tn_w + 'px;">' 460 + '<div class="mFileInfo mobile">' + size + 'B ' 461 + data.ext.slice(1).toUpperCase() 462 + '</div></a>'; 463 464 fileInfo = '<div class="fileText" id="fT' + data.no + fileSpoilerTip 465 + '>File: <a' + (needFileTip ? (' title="' + longFile + '"') : '') 466 + ' href="' + filePath + '" target="_blank">' 467 + fileName + '</a> (' + size + 'B, ' + fileDims + ')</div>'; 468 } 469 else { 470 filePath = imgDir + '/' + data.filename + data.ext; 471 472 fileDims += ', ' + data.tag; 473 474 fileInfo = '<div class="fileText" id="fT' + data.no + '"' 475 + '>File: <a href="' + filePath + '" target="_blank">' 476 + data.filename + '.swf</a> (' + size + 'B, ' + fileDims + ')</div>'; 477 } 478 479 fileHtml = '<div id="f' + data.no + '" class="file">' 480 + fileInfo + imgSrc + '</div>'; 481 } 482 483 if (data.trip) { 484 tripcode = ' <span class="postertrip">' + data.trip + '</span>'; 485 } 486 487 name = data.name || ''; 488 489 490 if (isOP) { 491 if (data.capcode_replies) { 492 capcode_replies = Parser.buildCapcodeReplies(data.capcode_replies, board, data.no); 493 } 494 495 if (fromQuote && data.replies) { 496 postCountStr = data.replies + ' post' + (data.replies > 1 ? 's' : ''); 497 498 if (data.images) { 499 postCountStr += ' and ' + data.images + ' image repl' + 500 (data.images > 1 ? 'ies' : 'y'); 501 } 502 503 summary = '<span class="summary preview-summary">' + postCountStr + '.</span>'; 504 } 505 506 if (data.sticky) { 507 threadIcons += '<img class="stickyIcon retina" title="Sticky" alt="Sticky" src="' 508 + Main.icons2.sticky + '"> '; 509 } 510 511 if (data.closed) { 512 if (data.archived) { 513 threadIcons += '<img class="archivedIcon retina" title="Archived" alt="Archived" src="' 514 + Main.icons2.archived + '"> '; 515 } 516 else { 517 threadIcons += '<img class="closedIcon retina" title="Closed" alt="Closed" src="' 518 + Main.icons2.closed + '"> '; 519 } 520 } 521 522 subject = '<span class="subject">' + (data.sub || '') + '</span> '; 523 } 524 else { 525 subject = ''; 526 } 527 528 container.className = 'postContainer ' + postType + 'Container'; 529 container.id = 'pc' + data.no; 530 531 container.innerHTML = 532 (isOP ? '' : '<div class="sideArrows" id="sa' + data.no + '">>></div>') + 533 '<div id="p' + data.no + '" class="post ' + postType + highlight + '">' + 534 '<div class="postInfoM mobile" id="pim' + data.no + '">' + 535 '<span class="nameBlock' + capcodeClass + '">' + 536 '<span class="name">' + name + '</span>' + tripcode + 537 capcodeStart + capcode + userId + flag + 538 '<br>' + subject + 539 '</span><span class="dateTime postNum" data-utc="' + data.time + '">' + 540 data.now + ' <a href="' + data.no + '#p' + data.no + '" title="Link to this post">No.</a>' + 541 '<a href="javascript:quote(\'' + data.no + '\');" title="Reply to this post">' + 542 data.no + '</a></span>' + 543 '</div>' + 544 (isOP ? fileHtml : '') + 545 '<div class="postInfo desktop" id="pi' + data.no + '"' + 546 (board != Main.board ? (' data-board="' + board + '"') : '') + '>' + 547 '<input type="checkbox" name="' + data.no + '" value="delete"> ' + 548 subject + 549 '<span class="nameBlock' + capcodeClass + '">' + emailStart + 550 '<span class="name">' + name + '</span>' + 551 tripcode + capcodeStart + emailEnd + capcode + userId + flag + 552 ' </span> ' + 553 '<span class="dateTime" data-utc="' + data.time + '">' + data.now + '</span> ' + 554 '<span class="postNum desktop">' + 555 '<a href="' + noLink + '" title="Link to this post">No.</a><a href="' + 556 quoteLink + '" title="Reply to this post">' + data.no + '</a> ' 557 + threadIcons + replySpan + 558 '</span>' + 559 '</div>' + 560 (isOP ? '' : fileHtml) + 561 '<blockquote class="postMessage" id="m' + data.no + '">' 562 + (data.com || '') + capcode_replies + summary + '</blockquote> ' + 563 '</div>' + mobileLink; 564 565 if (!Main.tid || board != Main.board) { 566 quotes = container.getElementsByClassName('quotelink'); 567 for (i = 0; q = quotes[i]; ++i) { 568 href = q.getAttribute('href'); 569 if (href.charAt(0) != '/') { 570 q.href = '/' + board + '/thread/' + resto + href; 571 } 572 } 573 } 574 575 return container; 576 }; 577 578 Parser.buildCapcodeReplies = function(replies, board, tid) { 579 var i, capcode, id, html, map, post_ids, prelink, pretext; 580 581 map = { 582 admin: 'Administrator', 583 mod: 'Moderator', 584 developer: 'Developer', 585 manager: 'Manager' 586 }; 587 588 if (board != Main.board) { 589 prelink = '/' + board + '/thread/'; 590 pretext = '>>>/' + board + '/'; 591 } 592 else { 593 prelink = ''; 594 pretext = '>>'; 595 } 596 597 html = '<br><br><span class="capcodeReplies"><span class="smaller">'; 598 599 for (capcode in replies) { 600 html += '<span class="bold">' + map[capcode] + ' Replies:</span> '; 601 602 post_ids = replies[capcode]; 603 604 for (i = 0; id = post_ids[i]; ++i) { 605 html += '<a class="quotelink" href="' 606 + prelink + tid + '#p' + id + '">' + pretext + id + '</a> '; 607 } 608 } 609 610 return html + '</span></span>'; 611 }; 612 613 Parser.parseBoard = function() { 614 var i, threads = document.getElementsByClassName('thread'); 615 616 for (i = 0; threads[i]; ++i) { 617 Parser.parseThread(threads[i].id.slice(1)); 618 } 619 }; 620 621 Parser.parseThread = function(tid, offset, limit) { 622 var i, j, thread, posts, pi, el, frag, summary, omitted, key, filtered, cnt, 623 frag; 624 625 thread = $.id('t' + tid); 626 posts = thread.getElementsByClassName('post'); 627 628 if (!offset) { 629 pi = document.getElementById('pi' + tid); 630 631 if (!Main.tid) { 632 if (Config.filter) { 633 filtered = Filter.exec( 634 thread, 635 pi, 636 document.getElementById('m' + tid), 637 tid 638 ); 639 } 640 641 if (Config.threadHiding && !filtered) { 642 if (Main.hasMobileLayout) { 643 el = document.createElement('a'); 644 el.href = 'javascript:;'; 645 el.setAttribute('data-cmd', 'hide'); 646 el.setAttribute('data-id', tid); 647 el.className = 'mobileHideButton button'; 648 el.textContent = 'Hide'; 649 posts[0].nextElementSibling.appendChild(el); 650 } 651 else { 652 el = document.createElement('span'); 653 el.innerHTML = '<img alt="H" class="extButton threadHideButton"' 654 + 'data-cmd="hide" data-id="' + tid + '" src="' 655 + Main.icons.minus + '" title="Hide thread">'; 656 posts[0].insertBefore(el, posts[0].firstChild); 657 } 658 el.id = 'sa' + tid; 659 if (ThreadHiding.hidden[tid]) { 660 ThreadHiding.hidden[tid] = Main.now; 661 ThreadHiding.hide(tid); 662 } 663 } 664 665 if (ThreadExpansion.enabled 666 && (summary = $.cls('summary', thread)[0])) { 667 frag = document.createDocumentFragment(); 668 669 omitted = summary.cloneNode(true); 670 omitted.className = ''; 671 summary.textContent = ''; 672 673 el = document.createElement('img'); 674 el.className = 'extButton expbtn'; 675 el.title = 'Expand thread'; 676 el.alt = '+'; 677 el.setAttribute('data-cmd', 'expand'); 678 el.setAttribute('data-id', tid); 679 el.src = Main.icons.plus; 680 frag.appendChild(el); 681 682 frag.appendChild(omitted); 683 684 el = document.createElement('span'); 685 el.style.display = 'none'; 686 el.textContent = 'Showing all replies.' 687 frag.appendChild(el); 688 689 summary.appendChild(frag); 690 } 691 } 692 693 if (Main.tid && Config.threadWatcher && (cnt = $.cls('navLinksBot')[0])) { 694 el = document.createElement('img'); 695 696 if (ThreadWatcher.watched[key = tid + '-' + Main.board]) { 697 el.src = Main.icons.watched; 698 el.setAttribute('data-active', '1'); 699 } 700 else { 701 el.src = Main.icons.notwatched; 702 } 703 704 el.className = 'extButton wbtn wbtn-' + key; 705 el.setAttribute('data-cmd', 'watch'); 706 el.setAttribute('data-id', tid); 707 el.alt = 'W'; 708 el.title = 'Add to watch list'; 709 710 frag = document.createDocumentFragment(); 711 frag.appendChild(document.createTextNode('[')); 712 frag.appendChild(el.cloneNode(true)); 713 frag.appendChild(document.createTextNode('] ')); 714 cnt.insertBefore(frag, cnt.firstChild); 715 } 716 } 717 718 j = offset ? offset < 0 ? posts.length + offset : offset : 0; 719 limit = limit ? j + limit : posts.length; 720 721 if (Main.isMobileDevice && Config.quotePreview) { 722 for (i = j; i < limit; ++i) { 723 Parser.parseMobileQuotelinks(posts[i]); 724 } 725 } 726 727 if (Parser.trackedReplies) { 728 for (i = j; i < limit; ++i) { 729 Parser.parseTrackedReplies(posts[i]); 730 } 731 } 732 733 for (i = j; i < limit; ++i) { 734 Parser.parsePost(posts[i].id.slice(1), tid); 735 } 736 737 if (offset) { 738 if (Parser.prettify) { 739 for (i = j; i < limit; ++i) { 740 Parser.parseMarkup(posts[i]); 741 } 742 } 743 if (window.jsMath) { 744 if (window.jsMath.loaded) { 745 for (i = j; i < limit; ++i) { 746 window.jsMath.ProcessBeforeShowing(posts[i]); 747 } 748 } 749 else { 750 Parser.loadJSMath(); 751 } 752 } 753 } 754 755 UA.dispatchEvent('4chanParsingDone', { threadId: tid, offset: j, limit: limit }); 756 }; 757 758 Parser.loadJSMath = function(root) { 759 if ($.cls('math', root)[0]) { 760 window.jsMath.Autoload.Script.Push('ProcessBeforeShowing', [ null ]); 761 window.jsMath.Autoload.LoadJsMath(); 762 } 763 }; 764 765 Parser.parseMathOne = function(node) { 766 if (window.jsMath.loaded) { 767 window.jsMath.ProcessBeforeShowing(node); 768 } 769 else { 770 Parser.loadJSMath(node); 771 } 772 }; 773 774 Parser.parseTrackedReplies = function(post) { 775 var i, link, quotelinks; 776 777 quotelinks = $.cls('quotelink', post); 778 779 for (i = 0; link = quotelinks[i]; ++i) { 780 if (Parser.trackedReplies[link.textContent]) { 781 link.textContent += ' (You)'; 782 Parser.hasYouMarkers = true; 783 } 784 } 785 }; 786 787 Parser.parseMobileQuotelinks = function(post) { 788 var i, link, quotelinks, t, el; 789 790 quotelinks = $.cls('quotelink', post); 791 792 for (i = 0; link = quotelinks[i]; ++i) { 793 t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/); 794 795 if (!t) { 796 continue; 797 } 798 799 el = document.createElement('a'); 800 el.href = link.href; 801 el.textContent = ' #'; 802 el.className = 'quoteLink'; 803 804 link.parentNode.insertBefore(el, link.nextSibling); 805 } 806 }; 807 808 Parser.parseMarkup = function(post) { 809 var i, pre, el; 810 811 if ((pre = post.getElementsByClassName('prettyprint'))[0]) { 812 for (i = 0; el = pre[i]; ++i) { 813 el.innerHTML = prettyPrintOne(el.innerHTML); 814 } 815 } 816 }; 817 818 Parser.parsePost = function(pid, tid) { 819 var hasMobileLayout, cnt, el, pi, href, img, file, msg, filtered, html, filename, txt, finfo, isOP, uid; 820 821 hasMobileLayout = Main.hasMobileLayout; 822 823 if (!tid) { 824 pi = pid.getElementsByClassName('postInfo')[0]; 825 pid = pi.id.slice(2); 826 } 827 else { 828 pi = document.getElementById('pi' + pid); 829 } 830 831 if (Parser.needMsg) { 832 msg = document.getElementById('m' + pid); 833 } 834 835 if (hasMobileLayout) { 836 if (Config.reportButton) { 837 el = document.createElement('span'); 838 el.className = 'mobile mobile-report'; 839 el.setAttribute('data-cmd', 'report'); 840 el.setAttribute('data-id', pid); 841 el.textContent = 'Report'; 842 pi.parentNode.appendChild(el); 843 } 844 } 845 else { 846 el = document.createElement('a'); 847 el.href = '#'; 848 el.className = 'postMenuBtn'; 849 el.title = 'Post menu'; 850 el.setAttribute('data-cmd', 'post-menu'); 851 el.textContent = 'โถ'; 852 pi.appendChild(el); 853 } 854 855 if (tid && pid != tid) { 856 if (Config.filter) { 857 filtered = Filter.exec(pi.parentNode, pi, msg); 858 } 859 860 if (!filtered && ReplyHiding.hidden[pid]) { 861 ReplyHiding.hidden[pid] = Main.now; 862 ReplyHiding.hide(pid); 863 } 864 865 if (Config.backlinks) { 866 Parser.parseBacklinks(pid, tid); 867 } 868 } 869 870 if (IDColor.enabled && (uid = $.cls('posteruid', pi.parentNode)[hasMobileLayout ? 0 : 1])) { 871 IDColor.apply(uid.firstElementChild); 872 } 873 874 if (Config.embedSoundCloud) { 875 Media.parseSoundCloud(msg); 876 } 877 878 if (Config.embedYouTube) { 879 Media.parseYouTube(msg); 880 } 881 882 if (Config.embedVocaroo) { 883 Media.parseVocaroo(msg); 884 } 885 886 if (Config.revealSpoilers 887 && (file = document.getElementById('f' + pid)) 888 && (file = file.children[1]) 889 ) { 890 if ($.hasClass(file, 'imgspoiler')) { 891 img = file.firstChild; 892 file.removeChild(img); 893 img.removeAttribute('style'); 894 isOP = $.hasClass(pi.parentNode, 'op'); 895 img.style.maxWidth = img.style.maxHeight = isOP ? '250px' : '125px'; 896 img.src = '//0.t.4cdn.org' 897 + (file.pathname.replace(/([0-9]+).+$/, '/$1s.jpg')); 898 899 filename = file.previousElementSibling; 900 finfo = filename.title.split('.'); 901 902 if (finfo[0].length > (isOP ? 40 : 30)) { 903 txt = finfo[0].slice(0, isOP ? 35 : 25) + '(...)' + finfo[1]; 904 } 905 else { 906 txt = filename.title; 907 filename.removeAttribute('title'); 908 } 909 910 filename.firstElementChild.innerHTML = txt; 911 file.insertBefore(img, file.firstElementChild); 912 } 913 } 914 915 if (Config.localTime) { 916 if (hasMobileLayout) { 917 el = pi.parentNode.getElementsByClassName('dateTime')[0]; 918 el.firstChild.nodeValue 919 = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)) + ' '; 920 } 921 else { 922 el = pi.getElementsByClassName('dateTime')[0]; 923 el.title = this.utcOffset; 924 el.textContent 925 = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)); 926 } 927 } 928 929 }; 930 931 Parser.getLocaleDate = function(date) { 932 return ('0' + (1 + date.getMonth())).slice(-2) + '/' 933 + ('0' + date.getDate()).slice(-2) + '/' 934 + ('0' + date.getFullYear()).slice(-2) + '(' 935 + this.weekdays[date.getDay()] + ')' 936 + ('0' + date.getHours()).slice(-2) + ':' 937 + ('0' + date.getMinutes()).slice(-2) + ':' 938 + ('0' + date.getSeconds()).slice(-2); 939 }; 940 941 Parser.parseBacklinks = function(pid, tid) { 942 var i, j, msg, backlinks, linklist, ids, target, bid, html, bl, el, href; 943 944 msg = document.getElementById('m' + pid); 945 946 if (!(backlinks = msg.getElementsByClassName('quotelink'))) { 947 return; 948 } 949 950 linklist = {}; 951 952 for (i = 0; j = backlinks[i]; ++i) { 953 // [tid, pid] 954 ids = j.getAttribute('href').split('#p'); 955 956 if (!ids[1]) { 957 continue; 958 } 959 960 if (ids[1] == tid) { 961 j.textContent += ' (OP)'; 962 } 963 964 if (!(target = document.getElementById('pi' + ids[1]))) { 965 if (Main.tid && j.textContent.charAt(2) != '>' ) { 966 j.textContent += ' โ'; 967 } 968 continue; 969 } 970 971 // Already processed? 972 if (linklist[ids[1]]) { 973 continue; 974 } 975 976 linklist[ids[1]] = true; 977 978 // Backlink node 979 bl = document.createElement('span'); 980 981 if (!Main.tid) { 982 href = 'thread/' + tid + '#p' + pid; 983 } 984 else { 985 href = '#p' + pid; 986 } 987 988 if (!Main.hasMobileLayout) { 989 bl.innerHTML = '<a href="' + href + '" class="quotelink">>>' + pid + '</a> '; 990 } 991 else { 992 bl.innerHTML = '<a href="' + href + '" class="quotelink">>>' + pid 993 + '</a><a href="' + href + '" class="quoteLink"> #</a> '; 994 } 995 996 // Backlinks container 997 if (!(el = document.getElementById('bl_' + ids[1]))) { 998 el = document.createElement('div'); 999 el.id = 'bl_' + ids[1]; 1000 el.className = 'backlink'; 1001 1002 if (Main.hasMobileLayout) { 1003 el.className = 'backlink mobile'; 1004 target = document.getElementById('p' + ids[1]); 1005 } 1006 1007 target.appendChild(el); 1008 } 1009 1010 el.appendChild(bl); 1011 } 1012 }; 1013 1014 Parser.buildSummary = function(tid, oRep, oImg) { 1015 if (oRep) { 1016 oRep = oRep + ' post' + (oRep > 1 ? 's' : ''); 1017 } 1018 else { 1019 return null; 1020 } 1021 1022 if (oImg) { 1023 oImg = ' and ' + oImg + ' image repl' + (oImg > 1 ? 'ies' : 'y'); 1024 } 1025 else { 1026 oImg = ''; 1027 } 1028 1029 el = document.createElement('span'); 1030 el.className = 'summary desktop'; 1031 el.innerHTML = oRep + oImg 1032 + ' omitted. <a href="thread/' 1033 + tid + '" class="replylink">Click here</a> to view.'; 1034 1035 return el; 1036 }; 1037 1038 /** 1039 * Sync 1040 */ 1041 var UserSync = { 1042 url: 'https://sys.4chan.org/sync', 1043 timeout: null, 1044 processing: false, 1045 maxDelay: 3600000, 1046 queue: {} 1047 }; 1048 1049 UserSync.onEnable = function() { 1050 var tkn = Math.random().toString(16).substring(2) 1051 + Math.random().toString(16).substring(2); 1052 1053 Main.setCookie('sync', tkn, '4chan.org'); 1054 }; 1055 1056 UserSync.onDisable = function() { 1057 Main.removeCookie('sync', '4chan.org'); 1058 localStorage.removeItem('4chan-sync-ts'); 1059 }; 1060 1061 UserSync.onSyncNowClick = function() { 1062 UserSync.syncStatus(true); 1063 }; 1064 1065 UserSync.purgeSync = function() { 1066 var xhr, tkn; 1067 1068 tkn = Main.getCookie('sync'); 1069 1070 if (!tkn) { 1071 alert("Syncing doesn't seem to be enabled on this machine"); 1072 return; 1073 } 1074 1075 if (!confirm('All data associated with this sync key will be deleted from the server.')) { 1076 return; 1077 } 1078 1079 xhr = new XMLHttpRequest(); 1080 xhr.open('POST', UserSync.url + '?action=purge'); 1081 xhr.onload = UserSync.onPurgeSyncLoaded; 1082 xhr.onerror = UserSync.onSyncError; 1083 xhr.withCredentials = true; 1084 xhr.withFeedback = true; 1085 1086 //Feedback.notify('Processingโฆ', false); 1087 1088 xhr.send(JSON.stringify({tkn: tkn})); 1089 }; 1090 1091 UserSync.onPurgeSyncLoaded = function() { 1092 var resp = JSON.parse(this.responseText); 1093 1094 if (resp.error) { 1095 return Feedback.error(resp.error); 1096 } 1097 1098 //Feedback.notify('Done'); 1099 1100 UserSync.onDisable(); 1101 }; 1102 1103 UserSync.syncStatus = function(withFeedback) { 1104 var xhr; 1105 1106 if (UserSync.processing) { 1107 console.log('Sync: Already syncing'); 1108 return; 1109 } 1110 1111 UserSync.processing = true; 1112 1113 if (withFeedback) { 1114 //Feedback.notify('Syncingโฆ', false); 1115 } 1116 1117 xhr = new XMLHttpRequest(); 1118 xhr.open('GET', UserSync.url + '?action=status'); 1119 xhr.withCredentials = true; 1120 xhr.withFeedback = withFeedback; 1121 xhr.onerror = UserSync.onSyncError; 1122 xhr.onload = UserSync.onSyncStatusLoaded; 1123 xhr.send(null); 1124 }; 1125 1126 UserSync.onSyncStatusLoaded = function() { 1127 var i, key, item, items, get, set, req, xhr, local_ts, remote_ts, data, syncTs, tkn; 1128 1129 UserSync.processing = false; 1130 1131 items = JSON.parse(this.responseText); 1132 1133 if (items.error) { 1134 console.log('Sync: ' + items.error); 1135 return; 1136 } 1137 1138 syncTs = UserSync.getSyncTs(); 1139 syncTs.ts = Date.now(); 1140 1141 get = []; 1142 set = {}; 1143 1144 for (key in items) { 1145 local_ts = syncTs[key] || 0; 1146 remote_ts = items[key] || 0; 1147 1148 if (remote_ts > local_ts) { 1149 get.push(key); 1150 } 1151 else if (local_ts > remote_ts) { 1152 data = localStorage.getItem(key); 1153 1154 if (data) { 1155 set[key] = { 1156 ts: local_ts, 1157 data: JSON.parse(data) 1158 }; 1159 } 1160 else { 1161 delete syncTs[key]; 1162 } 1163 } 1164 } 1165 1166 UserSync.setSyncTs(syncTs); 1167 1168 req = {}; 1169 1170 if (get.length) { 1171 req['get'] = get; 1172 } 1173 1174 for (i in set) { 1175 req['set'] = set; 1176 break; 1177 } 1178 1179 if (!req['get'] && !req['set']) { 1180 if (this.withFeedback) { 1181 //Feedback.notify('Done'); 1182 } 1183 if (Config.threadWatcher) { 1184 ThreadWatcher.onUserSyncLoaded(); 1185 } 1186 return; 1187 } 1188 1189 tkn = Main.getCookie('sync'); 1190 1191 if (!tkn) { 1192 return; 1193 } 1194 1195 req.tkn = tkn; 1196 1197 xhr = new XMLHttpRequest(); 1198 xhr.open('POST', UserSync.url + '?action=sync'); 1199 xhr.withCredentials = true; 1200 xhr.withFeedback = this.withFeedback; 1201 xhr.onload = UserSync.onSyncLoaded; 1202 xhr.onerror = UserSync.onSyncError; 1203 xhr.send(JSON.stringify(req)); 1204 }; 1205 1206 UserSync.onSyncError = function() { 1207 var msg = 'Sync: Connection Error'; 1208 1209 UserSync.processing = false; 1210 1211 UserSync.resetSyncTs(); 1212 1213 console.log(msg); 1214 }; 1215 1216 UserSync.getSyncTs = function() { 1217 var data = localStorage.getItem('4chan-sync-ts'); 1218 1219 return data ? JSON.parse(data) : {}; 1220 }; 1221 1222 UserSync.setSyncTs = function(data) { 1223 return localStorage.setItem('4chan-sync-ts', JSON.stringify(data)); 1224 }; 1225 1226 UserSync.resetSyncTs = function() { 1227 var tsData = UserSync.getSyncTs(); 1228 delete tsData.ts; 1229 UserSync.setSyncTs(tsData); 1230 }; 1231 1232 UserSync.onSyncLoaded = function() { 1233 var items, key, value, local_ts, syncTs; 1234 1235 items = JSON.parse(this.responseText); 1236 1237 if (items.error) { 1238 console.log('Sync: ' + items.error); 1239 return; 1240 } 1241 1242 if (this.withFeedback) { 1243 //Feedback.notify('Done'); 1244 } 1245 1246 syncTs = UserSync.getSyncTs(); 1247 syncTs.ts = Date.now(); 1248 1249 for (key in items) { 1250 value = items[key]; 1251 1252 local_ts = syncTs[key] || 0; 1253 1254 if (+local_ts > +value['ts']) { 1255 continue; 1256 } 1257 1258 localStorage.setItem(key, JSON.stringify(value['data'])); 1259 1260 syncTs[key] = value['ts']; 1261 } 1262 1263 UserSync.setSyncTs(syncTs); 1264 1265 if (Config.threadWatcher) { 1266 ThreadWatcher.onUserSyncLoaded(); 1267 } 1268 }; 1269 1270 UserSync.onQueueProcessed = function() { 1271 var items; 1272 1273 items = JSON.parse(this.responseText); 1274 1275 if (items.error) { 1276 console.log('Sync: ' + items.error); 1277 } 1278 } 1279 1280 UserSync.syncPush = function(key) { 1281 var ts, tsData; 1282 1283 ts = Math.round(Date.now() / 1000); 1284 1285 tsData = UserSync.getSyncTs(); 1286 tsData[key] = ts; 1287 UserSync.setSyncTs(tsData); 1288 1289 UserSync.queue[key] = ts; 1290 1291 if (UserSync.timeout) { 1292 clearTimeout(UserSync.timeout); 1293 } 1294 1295 UserSync.timeout = setTimeout(UserSync.syncProcessQueue, 1000); 1296 }; 1297 1298 UserSync.syncProcessQueue = function() { 1299 var set, xhr, key, tkn; 1300 1301 tkn = Main.getCookie('sync'); 1302 1303 if (!tkn) { 1304 UserSync.queue = {}; 1305 return; 1306 } 1307 1308 set = {}; 1309 1310 for (key in UserSync.queue) { 1311 set[key] = { 1312 ts: UserSync.queue[key], 1313 data: JSON.parse(localStorage.getItem(key)) 1314 } 1315 } 1316 1317 UserSync.queue = {}; 1318 1319 xhr = new XMLHttpRequest(); 1320 xhr.open('POST', UserSync.url + '?action=sync'); 1321 xhr.withCredentials = true; 1322 xhr.onload = UserSync.onQueueProcessed; 1323 xhr.onerror = UserSync.onSyncError; 1324 xhr.send(JSON.stringify({ 1325 tkn: tkn, 1326 set: set 1327 })); 1328 }; 1329 1330 1331 /** 1332 * Post Menu 1333 */ 1334 var PostMenu = { 1335 activeBtn: null 1336 }; 1337 1338 PostMenu.open = function(btn) { 1339 var div, html, pid, board, btnPos, txt, el, href, left, limit, isOP; 1340 1341 PostMenu.close(); 1342 1343 pid = btn.parentNode.id.split('pi')[1]; 1344 1345 board = btn.parentNode.getAttribute('data-board'); 1346 1347 isOP = !board && !!$.id('t' + pid); 1348 1349 html = '<ul><li data-cmd="report" data-id="' + pid 1350 + (board ? ('" data-board="' + board + '"') : '"') 1351 + '">Report post</li>'; 1352 1353 if (isOP) { 1354 if (!Main.tid) { 1355 html += '<li data-cmd="hide" data-id="' + pid + '">' 1356 + ($.hasClass($.id('t' + pid), 'post-hidden') ? 'Unhide' : 'Hide') 1357 + ' thread</li>'; 1358 } 1359 if (Config.threadWatcher) { 1360 html += '<li data-cmd="watch" data-id="' + pid + '">' 1361 + (ThreadWatcher.watched[pid + '-' + Main.board] ? 'Remove from' : 'Add to') 1362 + ' watch list</li>'; 1363 } 1364 } 1365 else if (el = $.id('pc' + pid)) { 1366 html += '<li data-cmd="hide-r" data-id="' + pid + '">' 1367 + ($.hasClass(el, 'post-hidden') ? 'Unhide' : 'Hide') 1368 + ' post</li>'; 1369 } 1370 1371 if (file = $.id('fT' + pid)) { 1372 el = $.cls('fileThumb', file.parentNode)[0]; 1373 1374 if (el) { 1375 if (/\.(png|jpg)$/.test(el.href)) { 1376 href = el.href; 1377 } 1378 else { 1379 href = 'http://0.t.4cdn.org/' + Main.board + '/' 1380 + el.href.match(/\/([0-9]+)\..+$/)[1] + 's.jpg'; 1381 } 1382 1383 html += '<li><ul>' 1384 + '<li><a href="//www.google.com/searchbyimage?image_url=' + href 1385 + '" target="_blank">Google</a></li>' 1386 + '<li><a href="http://iqdb.org/?url=' 1387 + href + '" target="_blank">iqdb</a></li></ul>Image search »</li>'; 1388 } 1389 } 1390 1391 if (Config.filter) { 1392 html += '<li><a href="#" data-cmd="filter-sel">Filter selected text</a></li>'; 1393 } 1394 1395 div = document.createElement('div'); 1396 div.id = 'post-menu'; 1397 div.className = 'dd-menu'; 1398 div.innerHTML = html + '</ul>'; 1399 1400 btnPos = btn.getBoundingClientRect(); 1401 1402 div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; 1403 1404 document.addEventListener('click', PostMenu.close, false); 1405 1406 $.addClass(btn, 'menuOpen'); 1407 PostMenu.activeBtn = btn; 1408 1409 UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: isOP, node: div.firstElementChild }); 1410 1411 document.body.appendChild(div); 1412 1413 left = btnPos.left + window.pageXOffset; 1414 limit = $.docEl.clientWidth - div.offsetWidth; 1415 1416 if (left > (limit - 75)) { 1417 div.className += ' dd-menu-left'; 1418 } 1419 1420 if (left > limit) { 1421 left = limit; 1422 } 1423 1424 div.style.left = left + 'px'; 1425 }; 1426 1427 PostMenu.close = function() { 1428 var el; 1429 1430 if (el = $.id('post-menu')) { 1431 el.parentNode.removeChild(el); 1432 document.removeEventListener('click', PostMenu.close, false); 1433 $.removeClass(PostMenu.activeBtn, 'menuOpen'); 1434 PostMenu.activeBtn = null; 1435 } 1436 }; 1437 1438 /** 1439 * Depager 1440 */ 1441 var Depager = {}; 1442 1443 Depager.init = function() { 1444 var el, el2, cnt; 1445 1446 this.isLoading = false; 1447 this.isEnabled = false; 1448 this.isComplete = false; 1449 this.threadsLoaded = false; 1450 this.threadQueue = []; 1451 this.debounce = 100; 1452 this.threshold = 350; 1453 1454 this.adId = 'azk53379'; 1455 this.adZones = [ 16258, 16260 ]; 1456 1457 this.boardHasAds = !!$.id(this.adId); 1458 1459 if (this.boardHasAds) { 1460 el = $.cls('ad-plea'); 1461 this.adPlea = el[el.length - 1]; 1462 } 1463 1464 if (el = $.cls('prev')[0]) { 1465 el.innerHTML = '[<a title="Toggle infinite scroll" ' 1466 + 'class="depagelink" href="" data-cmd="depage">All</a>]'; 1467 el = el.firstElementChild; 1468 } 1469 else { 1470 return; 1471 } 1472 1473 if (Config.alwaysDepage) { 1474 this.isEnabled = true; 1475 el.parentNode.parentNode.className += ' depagerEnabled'; 1476 Depager.bindHandlers(); 1477 1478 if (cnt = $.cls('board')[0]) { 1479 el2 = document.createElement('span'); 1480 el2.className = 'depageNumber'; 1481 el2.textContent = 'Page 1'; 1482 cnt.insertBefore(el2, cnt.firstElementChild); 1483 } 1484 } 1485 else { 1486 el.setAttribute('data-cmd', 'depage'); 1487 } 1488 }; 1489 1490 Depager.onScroll = function() { 1491 if (document.documentElement.scrollHeight 1492 <= (window.innerHeight + window.pageYOffset + Depager.threshold)) { 1493 if (Depager.threadsLoaded) { 1494 Depager.renderNext(); 1495 } 1496 else { 1497 Depager.depage(); 1498 } 1499 } 1500 }; 1501 1502 Depager.trackPageview = function(pageId) { 1503 var url; 1504 1505 try { 1506 if (window._gat) { 1507 url = '/' + Main.board + '/' + pageId; 1508 window._gat._getTrackerByName()._trackPageview(url); 1509 } 1510 1511 if (window.__qc) { 1512 window.__qc.qpixelsent = []; 1513 window._qevents.push({ qacct: window.__qc.qopts.qacct }); 1514 window.__qc.firepixels(); 1515 } 1516 } 1517 catch(e) { 1518 console.log(e); 1519 } 1520 }; 1521 1522 Depager.insertAd = function(pageId, frag, zone, isLastPage) { 1523 var wrap, cnt, nodes; 1524 1525 if (!Depager.boardHasAds || !window.ados_add_placement) { 1526 return; 1527 } 1528 1529 if (isLastPage) { 1530 nodes = $.cls('bottomad'); 1531 wrap = nodes[nodes.length - 1]; 1532 cnt = document.createElement('div'); 1533 cnt.id = 'azkDepage' + pageId; 1534 wrap.appendChild(cnt); 1535 window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); 1536 } 1537 else { 1538 wrap = document.createElement('div'); 1539 wrap.className = 'bottomad center'; 1540 1541 if (pageId == 2) { 1542 cnt = $.id(Depager.adId); 1543 } 1544 else { 1545 cnt = document.createElement('div'); 1546 cnt.id = 'azkDepage' + pageId; 1547 } 1548 1549 wrap.appendChild(cnt); 1550 frag.appendChild(wrap); 1551 1552 if (Depager.adPlea) { 1553 frag.appendChild(Depager.adPlea.cloneNode(true)); 1554 } 1555 1556 frag.appendChild(document.createElement('hr')); 1557 1558 if (pageId != 2) { 1559 window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); 1560 } 1561 } 1562 }; 1563 1564 Depager.loadAds = function() { 1565 if (!Depager.boardHasAds || !window.ados_load) { 1566 return; 1567 } 1568 1569 window.ados_load(); 1570 }; 1571 1572 Depager.renderNext = function() { 1573 var el, frag, i, j, k, threads, op, summary, cnt, reply, parseList, scroll, 1574 lastReplies, pageId, data, isLastPage, html; 1575 1576 parseList = []; 1577 1578 scroll = window.pageYOffset; 1579 1580 frag = document.createDocumentFragment(); 1581 1582 data = Depager.threadQueue.shift(); 1583 1584 if (!data) { 1585 return; 1586 } 1587 1588 threads = data.threads; 1589 pageId = data.page; 1590 1591 isLastPage = !Depager.threadQueue.length; 1592 1593 Depager.insertAd(pageId, frag, data.adZone, isLastPage); 1594 1595 el = document.createElement('span'); 1596 el.className = 'depageNumber'; 1597 el.textContent = 'Page ' + pageId; 1598 frag.appendChild(el); 1599 1600 for (j = 0; op = threads[j]; ++j) { 1601 if ($.id('t' + op.no)) { 1602 continue; 1603 } 1604 1605 cnt = document.createElement('div'); 1606 cnt.id = 't' + op.no; 1607 cnt.className = 'thread'; 1608 1609 cnt.appendChild(Parser.buildHTMLFromJSON(op, Main.board, true)); 1610 1611 if (summary = Parser.buildSummary(op.no, op.omitted_posts, op.omitted_images)) { 1612 cnt.appendChild(summary); 1613 } 1614 1615 if (op.replies) { 1616 last_replies = op.last_replies; 1617 1618 for (k = 0; reply = last_replies[k]; ++k) { 1619 cnt.appendChild(Parser.buildHTMLFromJSON(reply, Main.board)); 1620 } 1621 } 1622 1623 frag.appendChild(cnt); 1624 1625 frag.appendChild(document.createElement('hr')); 1626 1627 parseList.push(op.no); 1628 } 1629 1630 if (isLastPage) { 1631 Depager.unbindHandlers(); 1632 Depager.isComplete = true; 1633 Depager.setStatus('disabled'); 1634 } 1635 1636 boardDiv = $.cls('board')[0]; 1637 boardDiv.insertBefore(frag, boardDiv.lastElementChild); 1638 1639 Depager.trackPageview(pageId); 1640 1641 Depager.loadAds(); 1642 1643 for (i = 0; op = parseList[i]; ++i) { 1644 Parser.parseThread(op); 1645 } 1646 1647 window.scrollTo(0, scroll); 1648 }; 1649 1650 Depager.bindHandlers = function() { 1651 window.addEventListener('scroll', Depager.onScroll, false); 1652 window.addEventListener('resize', Depager.onScroll, false); 1653 }; 1654 1655 Depager.unbindHandlers = function() { 1656 window.removeEventListener('scroll', Depager.onScroll, false); 1657 window.removeEventListener('resize', Depager.onScroll, false); 1658 }; 1659 1660 Depager.setStatus = function(type) { 1661 var i, el, links, p; 1662 1663 links = $.cls('depagelink'); 1664 1665 if (!links.length) { 1666 return; 1667 } 1668 1669 if (type == 'enabled') { 1670 for (i = 0; el = links[i]; ++i) { 1671 el.textContent = 'All'; 1672 p = el.parentNode.parentNode; 1673 if (!$.hasClass(p, 'depagerEnabled')) { 1674 $.addClass(p,'depagerEnabled'); 1675 } 1676 } 1677 } 1678 else if (type == 'loading') { 1679 for (i = 0; el = links[i]; ++i) { 1680 el.textContent = 'Loadingโฆ'; 1681 } 1682 } 1683 else if (type == 'disabled') { 1684 for (i = 0; el = links[i]; ++i) { 1685 el.textContent = 'All'; 1686 $.removeClass(el.parentNode.parentNode,'depagerEnabled'); 1687 } 1688 } 1689 else if (type == 'error') { 1690 for (i = 0; el = links[i]; ++i) { 1691 el.textContent = 'Error'; 1692 el.removeAttribute('title'); 1693 el.removeAttribute('data-cmd'); 1694 $.removeClass(el.parentNode.parentNode, 'depagerEnabled'); 1695 } 1696 } 1697 }; 1698 1699 Depager.toggle = function() { 1700 if (Depager.isLoading || Depager.isComplete) { 1701 return; 1702 } 1703 1704 if (Depager.isEnabled) { 1705 Depager.disable(); 1706 } 1707 else { 1708 Depager.enable(); 1709 } 1710 1711 Depager.isEnabled = !Depager.isEnabled; 1712 }; 1713 1714 Depager.enable = function() { 1715 Depager.bindHandlers(); 1716 Depager.setStatus('enabled'); 1717 Depager.onScroll(); 1718 }; 1719 1720 Depager.disable = function() { 1721 Depager.unbindHandlers(); 1722 Depager.setStatus('disabled'); 1723 }; 1724 1725 Depager.depage = function() { 1726 if (Depager.isLoading) { 1727 return; 1728 } 1729 1730 Depager.isLoading = true; 1731 1732 $.get('//a.4cdn.org/' + Main.board + '/catalog.json', { 1733 onload: Depager.onLoad, 1734 onerror: Depager.onError 1735 }); 1736 1737 Depager.setStatus('loading'); 1738 }; 1739 1740 Depager.onLoad = function() { 1741 var catalog, i, page, queue, adZone; 1742 1743 Depager.isLoading = false; 1744 Depager.threadsLoaded = true; 1745 1746 if (this.status == 200) { 1747 Depager.setStatus('enabled'); 1748 1749 if (!Config.alwaysDepage) { 1750 Depager.bindHandlers(); 1751 } 1752 1753 catalog = Parser.parseCatalogJSON(this.responseText); 1754 1755 queue = Depager.threadQueue; 1756 1757 adZone = 0; 1758 for (i = 1; page = catalog[i]; ++i) { 1759 page.adZone = adZone; 1760 queue.push(page); 1761 adZone = adZone ? 0 : 1; 1762 } 1763 1764 Depager.renderNext(); 1765 } 1766 else if (this.status == 404) { 1767 Depager.unbindHandlers(); 1768 Depager.setStatus('error'); 1769 } 1770 else { 1771 Depager.unbindHandlers(); 1772 console.log('Error: ' + this.status); 1773 Depager.setStatus('error'); 1774 } 1775 }; 1776 1777 Depager.onError = function() { 1778 Depager.isLoading = false; 1779 Depager.unbindHandlers(); 1780 console.log('Error: ' + this.status); 1781 Depager.setStatus('error'); 1782 }; 1783 1784 /** 1785 * Quote inlining 1786 */ 1787 var QuoteInline = {}; 1788 1789 QuoteInline.isSelfQuote = function(node, pid, board) { 1790 var cnt; 1791 1792 if (board && board != Main.board) { 1793 return false; 1794 } 1795 1796 node = node.parentNode; 1797 1798 if ((node.nodeName == 'BLOCKQUOTE' && node.id.split('m')[1] == pid) 1799 || node.parentNode.id.split('_')[1] == pid) { 1800 return true; 1801 } 1802 1803 return false; 1804 }; 1805 1806 QuoteInline.toggle = function(link, e) { 1807 var t, pfx, src, el, count; 1808 1809 t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/); 1810 1811 if (!t || t[1] == 'rs' || QuoteInline.isSelfQuote(link, t[3], t[1])) { 1812 return; 1813 } 1814 1815 e && e.preventDefault(); 1816 1817 if (pfx = link.getAttribute('data-pfx')) { 1818 link.removeAttribute('data-pfx'); 1819 $.removeClass(link, 'linkfade'); 1820 1821 el = $.id(pfx + 'p' + t[3]); 1822 el.parentNode.removeChild(el); 1823 1824 if (link.parentNode.parentNode.className == 'backlink') { 1825 el = $.id('pc' + t[3]); 1826 count = +el.getAttribute('data-inline-count') - 1; 1827 if (count == 0) { 1828 el.style.display = ''; 1829 el.removeAttribute('data-inline-count'); 1830 } 1831 else { 1832 el.setAttribute('data-inline-count', count); 1833 } 1834 } 1835 1836 return; 1837 } 1838 1839 if (src = $.id('p' + t[3])) { 1840 QuoteInline.inline(link, src, t[3]); 1841 } 1842 else { 1843 QuoteInline.inlineRemote(link, t[1] || Main.board, t[2], t[3]); 1844 } 1845 }; 1846 1847 QuoteInline.inlineRemote = function(link, board, tid, pid) { 1848 var xhr, onload, onerror, cached, key, el, dummy; 1849 1850 if (link.hasAttribute('data-loading')) { 1851 return; 1852 } 1853 1854 key = board + '-' + tid; 1855 1856 if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { 1857 Parser.parsePost(el); 1858 QuoteInline.inline(link, el); 1859 return; 1860 } 1861 1862 if ((dummy = link.nextElementSibling) && $.hasClass(dummy, 'spinner')) { 1863 dummy.parentNode.removeChild(dummy); 1864 return; 1865 } 1866 else { 1867 dummy = document.createElement('div'); 1868 } 1869 1870 dummy.className = 'preview spinner inlined'; 1871 dummy.textContent = 'Loading...'; 1872 link.parentNode.insertBefore(dummy, link.nextSibling); 1873 1874 onload = function() { 1875 var el, thread; 1876 1877 link.removeAttribute('data-loading'); 1878 1879 if (this.status == 200 || this.status == 304 || this.status == 0) { 1880 thread = Parser.parseThreadJSON(this.responseText); 1881 1882 $.cache[key] = thread; 1883 1884 if (el = Parser.buildPost(thread, board, pid)) { 1885 dummy.parentNode && dummy.parentNode.removeChild(dummy); 1886 Parser.parsePost(el); 1887 QuoteInline.inline(link, el); 1888 } 1889 else { 1890 $.addClass(link, 'deadlink'); 1891 dummy.textContent = 'This post doesn\'t exist anymore'; 1892 } 1893 } 1894 else if (this.status == 404) { 1895 $.addClass(link, 'deadlink'); 1896 dummy.textContent = 'This thread doesn\'t exist anymore'; 1897 } 1898 else { 1899 this.onerror(); 1900 } 1901 }; 1902 1903 onerror = function() { 1904 dummy.textContent = 'Error: ' + this.statusText + ' (' + this.status + ')'; 1905 link.removeAttribute('data-loading'); 1906 }; 1907 1908 link.setAttribute('data-loading', '1'); 1909 1910 $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', 1911 { 1912 onload: onload, 1913 onerror: onerror 1914 } 1915 ); 1916 }; 1917 1918 QuoteInline.inline = function(link, src, id) { 1919 var i, j, now, el, blcnt, isBl, inner, tblcnt, pfx, dest, count, cnt; 1920 1921 now = Date.now(); 1922 1923 if (id) { 1924 if ((blcnt = link.parentNode.parentNode).className == 'backlink') { 1925 el = blcnt.parentNode.parentNode.parentNode; 1926 isBl = true; 1927 } 1928 else { 1929 el = blcnt.parentNode; 1930 } 1931 1932 while (el.parentNode !== document) { 1933 if (el.id.split('m')[1] == id) { 1934 return; 1935 } 1936 el = el.parentNode; 1937 } 1938 } 1939 1940 link.className += ' linkfade'; 1941 link.setAttribute('data-pfx', now); 1942 1943 el = src.cloneNode(true); 1944 el.id = now + el.id; 1945 el.setAttribute('data-pfx', now); 1946 el.className += ' preview inlined'; 1947 $.removeClass(el, 'highlight'); 1948 $.removeClass(el, 'highlight-anti'); 1949 1950 if ((inner = $.cls('inlined', el))[0]) { 1951 while (j = inner[0]) { 1952 j.parentNode.removeChild(j); 1953 } 1954 inner = $.cls('quotelink', el); 1955 for (i = 0; j = inner[i]; ++i) { 1956 j.removeAttribute('data-pfx'); 1957 $.removeClass(j, 'linkfade'); 1958 } 1959 } 1960 1961 for (i = 0; j = el.children[i]; ++i) { 1962 j.id = now + j.id; 1963 } 1964 1965 if (tblcnt = $.cls('backlink', el)[0]) { 1966 tblcnt.id = now + tblcnt.id; 1967 } 1968 1969 if (isBl) { 1970 pfx = blcnt.parentNode.parentNode.getAttribute('data-pfx') || ''; 1971 dest = $.id(pfx + 'm' + blcnt.id.split('_')[1]); 1972 dest.insertBefore(el, dest.firstChild); 1973 if (count = src.parentNode.getAttribute('data-inline-count')) { 1974 count = +count + 1; 1975 } 1976 else { 1977 count = 1; 1978 src.parentNode.style.display = 'none'; 1979 } 1980 src.parentNode.setAttribute('data-inline-count', count); 1981 } 1982 else { 1983 if ($.hasClass(link.parentNode, 'quote')) { 1984 link = link.parentNode; 1985 cnt = link.parentNode; 1986 } 1987 else { 1988 cnt = link.parentNode; 1989 } 1990 cnt.insertBefore(el, link.nextSibling); 1991 } 1992 }; 1993 1994 /** 1995 * Quote preview 1996 */ 1997 var QuotePreview = {}; 1998 1999 QuotePreview.init = function() { 2000 var thread; 2001 2002 this.regex = /^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/; 2003 this.highlight = null; 2004 this.highlightAnti = null; 2005 this.out = true; 2006 }; 2007 2008 QuotePreview.resolve = function(link) { 2009 var self, t, post, ids, offset, pfx; 2010 2011 self = QuotePreview; 2012 self.out = false; 2013 2014 t = link.getAttribute('href').match(self.regex); 2015 2016 if (!t) { 2017 return; 2018 } 2019 2020 // Quoted post in scope 2021 pfx = link.getAttribute('data-pfx') || ''; 2022 2023 if (post = document.getElementById(pfx + 'p' + t[3])) { 2024 // Visible and not filtered out? 2025 offset = post.getBoundingClientRect(); 2026 if (offset.top > 0 2027 && offset.bottom < document.documentElement.clientHeight 2028 && !$.hasClass(post.parentNode, 'post-hidden')) { 2029 if (!$.hasClass(post, 'highlight') && location.hash.slice(1) != post.id) { 2030 self.highlight = post; 2031 $.addClass(post, 'highlight'); 2032 } 2033 else if (!$.hasClass(post, 'op')) { 2034 self.highlightAnti = post; 2035 $.addClass(post, 'highlight-anti'); 2036 } 2037 return; 2038 } 2039 // Nope 2040 self.show(link, post); 2041 } 2042 // Quoted post out of scope 2043 else { 2044 if (!UA.hasCORS) { 2045 return; 2046 } 2047 self.showRemote(link, t[1] || Main.board, t[2], t[3]); 2048 } 2049 }; 2050 2051 QuotePreview.showRemote = function(link, board, tid, pid) { 2052 var xhr, onload, onerror, el, cached, key; 2053 2054 key = board + '-' + tid; 2055 2056 if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { 2057 QuotePreview.show(link, el); 2058 return; 2059 } 2060 2061 link.style.cursor = 'wait'; 2062 2063 onload = function() { 2064 var el, thread; 2065 2066 link.style.cursor = ''; 2067 2068 if (this.status == 200 || this.status == 304 || this.status == 0) { 2069 thread = Parser.parseThreadJSON(this.responseText); 2070 2071 $.cache[key] = thread; 2072 2073 if ($.id('quote-preview') || QuotePreview.out) { 2074 return; 2075 } 2076 2077 if (el = Parser.buildPost(thread, board, pid)) { 2078 el.className = 'post preview'; 2079 el.style.display = 'none'; 2080 el.id = 'quote-preview'; 2081 document.body.appendChild(el); 2082 QuotePreview.show(link, el, true); 2083 } 2084 else { 2085 $.addClass(link, 'deadlink'); 2086 } 2087 } 2088 else if (this.status == 404) { 2089 $.addClass(link, 'deadlink'); 2090 } 2091 }; 2092 2093 onerror = function() { 2094 link.style.cursor = ''; 2095 }; 2096 2097 $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', 2098 { 2099 onload: onload, 2100 onerror: onerror 2101 } 2102 ); 2103 }; 2104 2105 QuotePreview.show = function(link, post, remote) { 2106 var rect, postHeight, postWidth, doc, docWidth, style, pos, quotes, i, j, qid, 2107 top, scrollTop, margin, img; 2108 2109 if (remote) { 2110 Parser.parsePost(post); 2111 post.style.display = ''; 2112 } 2113 else { 2114 post = post.cloneNode(true); 2115 if (location.hash && location.hash == ('#' + post.id)) { 2116 post.className += ' highlight'; 2117 } 2118 post.id = 'quote-preview'; 2119 post.className += ' preview'; 2120 2121 if (Config.imageExpansion && (img = $.cls('expanded-thumb', post)[0])) { 2122 ImageExpansion.contract(img); 2123 } 2124 } 2125 2126 if (!link.parentNode.className) { 2127 quotes = post.querySelectorAll( 2128 '#' + $.cls('postMessage', post)[0].id + ' > .quotelink' 2129 ); 2130 if (quotes[1]) { 2131 qid = '>>' + link.parentNode.parentNode.id.split('_')[1]; 2132 for (i = 0; j = quotes[i]; ++i) { 2133 if (j.textContent == qid) { 2134 $.addClass(j, 'dotted'); 2135 break; 2136 } 2137 } 2138 } 2139 } 2140 2141 rect = link.getBoundingClientRect(); 2142 doc = document.documentElement; 2143 docWidth = doc.offsetWidth; 2144 style = post.style; 2145 2146 document.body.appendChild(post); 2147 2148 if (Main.isMobileDevice) { 2149 style.top = rect.top + link.offsetHeight + window.pageYOffset + 'px'; 2150 2151 if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { 2152 style.right = docWidth - rect.right + 'px'; 2153 } 2154 else { 2155 style.left = rect.left + 'px'; 2156 } 2157 } 2158 else { 2159 if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { 2160 pos = docWidth - rect.left; 2161 style.right = pos + 5 + 'px'; 2162 } 2163 else { 2164 pos = rect.left + rect.width; 2165 style.left = pos + 5 + 'px'; 2166 } 2167 2168 top = rect.top + link.offsetHeight + window.pageYOffset 2169 - post.offsetHeight / 2 - rect.height / 2; 2170 2171 postHeight = post.getBoundingClientRect().height; 2172 2173 if (doc.scrollTop != document.body.scrollTop) { 2174 scrollTop = doc.scrollTop + document.body.scrollTop; 2175 } else { 2176 scrollTop = document.body.scrollTop; 2177 } 2178 2179 if (top < scrollTop) { 2180 style.top = scrollTop + 'px'; 2181 } 2182 else if (top + postHeight > scrollTop + doc.clientHeight) { 2183 style.top = scrollTop + doc.clientHeight - postHeight + 'px'; 2184 } 2185 else { 2186 style.top = top + 'px'; 2187 } 2188 } 2189 }; 2190 2191 QuotePreview.remove = function(el) { 2192 var self, cnt; 2193 2194 self = QuotePreview; 2195 self.out = true; 2196 2197 if (self.highlight) { 2198 $.removeClass(self.highlight, 'highlight'); 2199 self.highlight = null; 2200 } 2201 else if (self.highlightAnti) { 2202 $.removeClass(self.highlightAnti, 'highlight-anti'); 2203 self.highlightAnti = null 2204 } 2205 2206 if (el) { 2207 el.style.cursor = ''; 2208 } 2209 2210 if (cnt = $.id('quote-preview')) { 2211 document.body.removeChild(cnt); 2212 } 2213 }; 2214 2215 /** 2216 * Image expansion 2217 */ 2218 var ImageExpansion = { 2219 activeVideos: [], 2220 timeout: null 2221 }; 2222 2223 ImageExpansion.expand = function(thumb) { 2224 var img, el, href, ext; 2225 2226 if (Config.imageHover) { 2227 ImageHover.hide(); 2228 } 2229 2230 href = thumb.parentNode.getAttribute('href'); 2231 2232 if (ext = href.match(/\.(?:webm|pdf)$/)) { 2233 if (!Main.hasMobileLayout && ext[0] == '.webm') { 2234 return ImageExpansion.expandWebm(thumb); 2235 } 2236 return false; 2237 } 2238 2239 thumb.setAttribute('data-expanding', '1'); 2240 2241 img = document.createElement('img'); 2242 img.alt = 'Image'; 2243 img.setAttribute('src', href); 2244 img.className = 'expanded-thumb'; 2245 img.style.display = 'none'; 2246 img.onerror = this.onError; 2247 2248 thumb.parentNode.insertBefore(img, thumb.nextElementSibling); 2249 2250 if (UA.hasCORS) { 2251 thumb.style.opacity = '0.75'; 2252 this.timeout = this.checkLoadStart(img, thumb); 2253 } 2254 else { 2255 this.onLoadStart(img, thumb); 2256 } 2257 2258 return true; 2259 }; 2260 2261 ImageExpansion.contract = function(img) { 2262 var cnt, p; 2263 2264 clearTimeout(this.timeout); 2265 2266 p = img.parentNode; 2267 cnt = p.parentNode.parentNode; 2268 2269 $.removeClass(p.parentNode, 'image-expanded'); 2270 2271 if (Config.centeredThreads) { 2272 $.removeClass(cnt.parentNode, 'centre-exp'); 2273 cnt.parentNode.style.marginLeft = ''; 2274 } 2275 2276 if (!Main.tid && Config.threadHiding) { 2277 $.removeClass(p, 'image-expanded-anti'); 2278 } 2279 2280 p.firstChild.style.display = ''; 2281 2282 p.removeChild(img); 2283 2284 if (cnt.offsetTop < window.pageYOffset) { 2285 cnt.scrollIntoView(); 2286 } 2287 }; 2288 2289 ImageExpansion.toggle = function(t) { 2290 if (t.hasAttribute('data-md5')) { 2291 if (!t.hasAttribute('data-expanding')) { 2292 return ImageExpansion.expand(t); 2293 } 2294 } 2295 else { 2296 ImageExpansion.contract(t); 2297 } 2298 2299 return true; 2300 }; 2301 2302 ImageExpansion.expandWebm = function(thumb) { 2303 var el, link, fileText, left, width, href, maxWidth, self; 2304 2305 self = ImageExpansion; 2306 2307 if (el = document.getElementById('image-hover')) { 2308 document.body.removeChild(el); 2309 } 2310 2311 link = thumb.parentNode; 2312 2313 href = link.getAttribute('href'); 2314 2315 left = link.getBoundingClientRect().left; 2316 maxWidth = document.documentElement.clientWidth - left - 25; 2317 2318 el = document.createElement('video'); 2319 el.muted = true; 2320 el.controls = true; 2321 el.loop = true; 2322 el.autoplay = true; 2323 el.className = 'expandedWebm'; 2324 el.onloadedmetadata = ImageExpansion.fitWebm; 2325 el.onplay = ImageExpansion.onWebmPlay; 2326 el.src = href; 2327 2328 link.style.display = 'none'; 2329 link.parentNode.appendChild(el); 2330 2331 fileText = thumb.parentNode.previousElementSibling; 2332 2333 el = document.createElement('span'); 2334 el.className = 'collapseWebm'; 2335 el.innerHTML = '-[<a href="#">Close</a>]'; 2336 el.firstElementChild.addEventListener('click', self.collapseWebm, false); 2337 2338 fileText.appendChild(el); 2339 2340 return true; 2341 }; 2342 2343 ImageExpansion.fitWebm = function() { 2344 var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, cntEl, 2345 centerWidth, ofs; 2346 2347 if (Config.centeredThreads) { 2348 centerWidth = $.cls('opContainer')[0].offsetWidth; 2349 cntEl = this.parentNode.parentNode.parentNode; 2350 $.addClass(cntEl, 'centre-exp') 2351 } 2352 2353 left = this.getBoundingClientRect().left; 2354 2355 maxWidth = document.documentElement.clientWidth - left - 25; 2356 maxHeight = document.documentElement.clientHeight; 2357 2358 imgWidth = this.videoWidth; 2359 imgHeight = this.videoHeight; 2360 2361 if (imgWidth > maxWidth) { 2362 ratio = maxWidth / imgWidth; 2363 imgWidth = maxWidth; 2364 imgHeight = imgHeight * ratio; 2365 } 2366 2367 if (Config.fitToScreenExpansion && imgHeight > maxHeight) { 2368 ratio = maxHeight / imgHeight; 2369 imgHeight = maxHeight; 2370 imgWidth = imgWidth * ratio; 2371 } 2372 2373 this.style.maxWidth = imgWidth + 'px'; 2374 this.style.maxHeight = imgHeight + 'px'; 2375 2376 if (Config.centeredThreads) { 2377 left = this.getBoundingClientRect().left; 2378 ofs = this.offsetWidth + left * 2; 2379 if (ofs > centerWidth) { 2380 left = Math.floor(($.docEl.clientWidth - ofs) / 2); 2381 2382 if (left > 0) { 2383 cntEl.style.marginLeft = left + 'px'; 2384 } 2385 } 2386 else { 2387 $.removeClass(cntEl, 'centre-exp') 2388 } 2389 } 2390 }; 2391 2392 ImageExpansion.onWebmPlay = function(e) { 2393 var self = ImageExpansion; 2394 2395 if (!self.activeVideos.length) { 2396 document.addEventListener('scroll', self.onScroll, false); 2397 } 2398 2399 self.activeVideos.push(this); 2400 }; 2401 2402 ImageExpansion.collapseWebm = function(e) { 2403 var cnt, el, el2; 2404 2405 e.preventDefault(); 2406 2407 this.removeEventListener('click', ImageExpansion.collapseWebm, false); 2408 2409 cnt = this.parentNode; 2410 el = cnt.parentNode.parentNode.getElementsByClassName('expandedWebm')[0]; 2411 2412 if (Config.centeredThreads) { 2413 el2 = el.parentNode.parentNode.parentNode; 2414 $.removeClass(el2, 'centre-exp') 2415 el2.style.marginLeft = ''; 2416 } 2417 2418 el.previousElementSibling.style.display = ''; 2419 el.parentNode.removeChild(el); 2420 cnt.parentNode.removeChild(cnt); 2421 }; 2422 2423 ImageExpansion.onScroll = function(e) { 2424 clearTimeout(ImageExpansion.timeout); 2425 ImageExpansion.timeout = setTimeout(ImageExpansion.pauseVideos, 500); 2426 }; 2427 2428 ImageExpansion.pauseVideos = function() { 2429 var self, i, el, pos, min, max, nodes; 2430 2431 self = ImageExpansion; 2432 2433 nodes = []; 2434 min = window.pageYOffset; 2435 max = window.pageYOffset + $.docEl.clientHeight; 2436 2437 for (i = 0; el = self.activeVideos[i]; ++i) { 2438 pos = el.getBoundingClientRect(); 2439 if (pos.top + window.pageYOffset > max || pos.bottom + window.pageYOffset < min) { 2440 el.pause(); 2441 } 2442 else if (!el.paused){ 2443 nodes.push(el); 2444 } 2445 } 2446 2447 if (!nodes.length) { 2448 document.removeEventListener('scroll', self.onScroll, false); 2449 } 2450 2451 self.activeVideos = nodes; 2452 }; 2453 2454 ImageExpansion.onError = function(e) { 2455 var thumb, img; 2456 2457 img = e.target; 2458 thumb = $.qs('img[data-expanding]', img.parentNode); 2459 2460 img.parentNode.removeChild(img); 2461 thumb.style.opacity = ''; 2462 thumb.removeAttribute('data-expanding'); 2463 }; 2464 2465 ImageExpansion.onLoadStart = function(img, thumb) { 2466 var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, fileEl, cntEl, 2467 centerWidth, ofs; 2468 2469 thumb.removeAttribute('data-expanding'); 2470 2471 fileEl = thumb.parentNode.parentNode; 2472 2473 if (Config.centeredThreads) { 2474 cntEl = fileEl.parentNode.parentNode; 2475 centerWidth = $.cls('opContainer')[0].offsetWidth; 2476 $.addClass(cntEl, 'centre-exp'); 2477 } 2478 2479 left = thumb.getBoundingClientRect().left; 2480 2481 maxWidth = $.docEl.clientWidth - left - 25; 2482 maxHeight = $.docEl.clientHeight; 2483 2484 imgWidth = img.naturalWidth; 2485 imgHeight = img.naturalHeight; 2486 2487 if (imgWidth > maxWidth) { 2488 ratio = maxWidth / imgWidth; 2489 imgWidth = maxWidth; 2490 imgHeight = imgHeight * ratio; 2491 } 2492 2493 if (Config.fitToScreenExpansion && imgHeight > maxHeight) { 2494 ratio = maxHeight / imgHeight; 2495 imgHeight = maxHeight; 2496 imgWidth = imgWidth * ratio; 2497 } 2498 2499 img.style.maxWidth = imgWidth + 'px'; 2500 img.style.maxHeight = imgHeight + 'px'; 2501 2502 $.addClass(fileEl, 'image-expanded'); 2503 2504 if (!Main.tid && Config.threadHiding) { 2505 $.addClass(thumb.parentNode, 'image-expanded-anti'); 2506 } 2507 2508 img.style.display = ''; 2509 thumb.style.display = 'none'; 2510 2511 if (Config.centeredThreads) { 2512 left = img.getBoundingClientRect().left; 2513 ofs = img.offsetWidth + left * 2; 2514 if (ofs > centerWidth) { 2515 left = Math.floor(($.docEl.clientWidth - ofs) / 2); 2516 2517 if (left > 0) { 2518 cntEl.style.marginLeft = left + 'px'; 2519 } 2520 } 2521 else { 2522 $.removeClass(cntEl, 'centre-exp'); 2523 } 2524 } 2525 }; 2526 2527 ImageExpansion.checkLoadStart = function(img, thumb) { 2528 if (img.naturalWidth) { 2529 ImageExpansion.onLoadStart(img, thumb); 2530 thumb.style.opacity = ''; 2531 } 2532 else { 2533 return setTimeout(ImageExpansion.checkLoadStart, 15, img, thumb); 2534 } 2535 }; 2536 2537 /** 2538 * Image hover 2539 */ 2540 var ImageHover = {}; 2541 2542 ImageHover.show = function(thumb) { 2543 var el, href, ext; 2544 2545 href = thumb.parentNode.getAttribute('href'); 2546 2547 if (ext = href.match(/\.(?:webm|pdf)$/)) { 2548 if (ext[0] == '.webm') { 2549 ImageHover.showWebm(thumb); 2550 } 2551 return; 2552 } 2553 2554 el = document.createElement('img'); 2555 el.id = 'image-hover'; 2556 el.alt = 'Image'; 2557 el.setAttribute('src', href); 2558 2559 document.body.appendChild(el); 2560 2561 if (UA.hasCORS) { 2562 el.style.display = 'none'; 2563 this.timeout = ImageHover.checkLoadStart(el, thumb); 2564 } 2565 else { 2566 el.style.left = thumb.getBoundingClientRect().right + 10 + 'px'; 2567 } 2568 }; 2569 2570 ImageHover.hide = function() { 2571 var img; 2572 clearTimeout(this.timeout); 2573 if (img = $.id('image-hover')) { 2574 if (img.play) { 2575 Tip.hide(); 2576 } 2577 document.body.removeChild(img); 2578 } 2579 }; 2580 2581 ImageHover.showWebm = function(thumb) { 2582 var dims, el, bounds, limit, width; 2583 2584 dims = thumb.parentNode.previousElementSibling.textContent.match(/, ([0-9]+)x[0-9]+/); 2585 width = +dims[1]; 2586 2587 el = document.createElement('video'); 2588 el.id = 'image-hover'; 2589 el.src = thumb.parentNode.getAttribute('href'); 2590 el.loop = true; 2591 el.muted = true; 2592 el.autoplay = true; 2593 el.onloadedmetadata = function() { ImageHover.showWebMDuration(this, thumb); }; 2594 2595 bounds = thumb.getBoundingClientRect(); 2596 limit = window.innerWidth - bounds.right - 20; 2597 2598 if (width > limit) { 2599 el.style.maxWidth = limit + 'px'; 2600 } 2601 2602 document.body.appendChild(el); 2603 }; 2604 2605 ImageHover.showWebMDuration = function(el, thumb) { 2606 if (!el.parentNode) { 2607 return; 2608 } 2609 2610 var ms = $.prettySeconds(el.duration); 2611 2612 Tip.show(thumb, ms[0] + ':' + ('0' + ms[1]).slice(-2)); 2613 }; 2614 2615 ImageHover.onLoadStart = function(img, thumb) { 2616 var bounds, limit; 2617 2618 bounds = thumb.getBoundingClientRect(); 2619 limit = window.innerWidth - bounds.right - 20; 2620 2621 if (img.naturalWidth > limit) { 2622 img.style.maxWidth = limit + 'px'; 2623 } 2624 2625 img.style.display = ''; 2626 }; 2627 2628 ImageHover.checkLoadStart = function(img, thumb) { 2629 if (img.naturalWidth) { 2630 ImageHover.onLoadStart(img, thumb); 2631 } 2632 else { 2633 return setTimeout(ImageHover.checkLoadStart, 15, img, thumb); 2634 } 2635 }; 2636 2637 /** 2638 * Quick reply 2639 */ 2640 var QR = {}; 2641 2642 QR.init = function() { 2643 var item; 2644 2645 if (!UA.hasFormData) { 2646 return; 2647 } 2648 2649 this.enabled = true; 2650 this.currentTid = null; 2651 this.cooldown = null; 2652 this.timestamp = null; 2653 this.auto = false; 2654 2655 this.btn = null; 2656 this.comField = null; 2657 this.comLength = window.comlen; 2658 this.lenCheckTimeout = null; 2659 2660 this.preuploadSizeLimit = Main.hasMobileLayout ? 0 : 204800; 2661 2662 this.cdElapsed = 0; 2663 this.activeDelay = 0; 2664 2665 this.cooldowns = {}; 2666 2667 for (item in window.cooldowns) { 2668 this.cooldowns[item] = window.cooldowns[item] * 1000; 2669 } 2670 2671 this.captchaDelay = 240500; 2672 this.captchaInterval = null; 2673 this.pulse = null; 2674 this.xhr = null; 2675 2676 this.fileDisabled = !!window.imagelimit; 2677 2678 this.tracked = {}; 2679 2680 this.lastTid = localStorage.getItem('4chan-cd-' + Main.board + '-tid'); 2681 2682 if (Main.tid && !Main.hasMobileLayout && !Main.threadClosed) { 2683 QR.addReplyLink(); 2684 } 2685 2686 window.addEventListener('storage', this.syncStorage, false); 2687 }; 2688 2689 QR.addReplyLink = function() { 2690 var cnt, el; 2691 2692 cnt = $.cls('navLinks')[2]; 2693 2694 el = document.createElement('div'); 2695 el.className = 'open-qr-wrap'; 2696 el.innerHTML = '[<a href="#" class="open-qr-link" data-cmd="open-qr">Post a Reply</a>]'; 2697 2698 cnt.insertBefore(el, cnt.firstChild); 2699 }; 2700 2701 QR.lock = function() { 2702 QR.showPostError('This thread is closed.', 'closed', true); 2703 }; 2704 2705 QR.unlock = function() { 2706 QR.hidePostError('closed'); 2707 }; 2708 2709 QR.syncStorage = function(e) { 2710 var key; 2711 2712 if (!e.key) { 2713 return; 2714 } 2715 2716 key = e.key.split('-'); 2717 2718 if (key[0] != '4chan') { 2719 return; 2720 } 2721 2722 if (key[1] == 'cd' && e.newValue && Main.board == key[2]) { 2723 if (key[3] == 'tid') { 2724 QR.lastTid = e.newValue; 2725 } 2726 else { 2727 QR.startCooldown(); 2728 } 2729 } 2730 }; 2731 2732 QR.quotePost = function(tid, pid) { 2733 if (!QR.noCooldown 2734 && (Main.threadClosed || (!Main.tid && Main.isThreadClosed(tid)))) { 2735 alert('This thread is closed'); 2736 return; 2737 } 2738 QR.show(tid); 2739 QR.addQuote(pid); 2740 }; 2741 2742 QR.addQuote = function(pid) { 2743 var q, pos, sel, ta; 2744 2745 ta = $.tag('textarea', document.forms.qrPost)[0]; 2746 2747 pos = ta.selectionStart; 2748 2749 sel = UA.getSelection(); 2750 2751 if (pid) { 2752 q = '>>' + pid + '\n'; 2753 } 2754 else { 2755 q = ''; 2756 } 2757 2758 if (sel) { 2759 q += '>' + sel.trim().replace(/[\r\n]+/g, '\n>') + '\n'; 2760 } 2761 2762 if (ta.value) { 2763 ta.value = ta.value.slice(0, pos) 2764 + q + ta.value.slice(ta.selectionEnd); 2765 } 2766 else { 2767 ta.value = q; 2768 } 2769 if (UA.isOpera) { 2770 pos += q.split('\n').length; 2771 } 2772 2773 ta.selectionStart = ta.selectionEnd = pos + q.length; 2774 2775 if (ta.selectionStart == ta.value.length) { 2776 ta.scrollTop = ta.scrollHeight; 2777 } 2778 ta.focus(); 2779 }; 2780 2781 QR.show = function(tid) { 2782 var i, j, cnt, postForm, form, qrForm, fields, row, spoiler, file, 2783 el, el2, placeholder, cd, qrError, cookie; 2784 2785 if (QR.currentTid) { 2786 if (!Main.tid && QR.currentTid != tid) { 2787 $.id('qrTid').textContent = $.id('qrResto').value = QR.currentTid = tid; 2788 $.byName('com')[1].value = ''; 2789 2790 QR.startCooldown(); 2791 } 2792 2793 if (Main.hasMobileLayout) { 2794 $.id('quickReply').style.top = window.pageYOffset + 25 + 'px'; 2795 } 2796 2797 return; 2798 } 2799 2800 QR.currentTid = tid; 2801 2802 postForm = $.id('postForm'); 2803 2804 cnt = document.createElement('div'); 2805 cnt.id = 'quickReply'; 2806 cnt.className = 'extPanel reply'; 2807 cnt.setAttribute('data-trackpos', 'QR-position'); 2808 2809 if (Main.hasMobileLayout) { 2810 cnt.style.top = window.pageYOffset + 28 + 'px'; 2811 } 2812 else if (Config['QR-position']) { 2813 cnt.style.cssText = Config['QR-position']; 2814 } 2815 else { 2816 cnt.style.right = '0px'; 2817 cnt.style.top = '10%'; 2818 } 2819 2820 cnt.innerHTML = 2821 '<div id="qrHeader" class="drag postblock">Reply to Thread No.<span id="qrTid">' 2822 + tid + '</span><img alt="X" src="' + Main.icons.cross + '" id="qrClose" ' 2823 + 'class="extButton" title="Close Window"></div>'; 2824 2825 form = postForm.parentNode.cloneNode(false); 2826 form.setAttribute('name', 'qrPost'); 2827 form.innerHTML = 2828 '<input type="hidden" value="' 2829 + $.byName('MAX_FILE_SIZE')[0].value + '" name="MAX_FILE_SIZE">' 2830 + '<input type="hidden" value="regist" name="mode">' 2831 + '<input id="qrResto" type="hidden" value="' + tid + '" name="resto">'; 2832 2833 qrForm = document.createElement('div'); 2834 qrForm.id = 'qrForm'; 2835 2836 fields = postForm.firstElementChild.children; 2837 for (i = 0, j = fields.length - 1; i < j; ++i) { 2838 row = document.createElement('div'); 2839 if (fields[i].id == 'captchaFormPart') { 2840 if (QR.noCaptcha) { 2841 continue; 2842 } 2843 row.id = 'qrCaptchaContainer'; 2844 } 2845 else { 2846 placeholder = fields[i].getAttribute('data-type'); 2847 if (placeholder == 'Password' || placeholder == 'Spoilers') { 2848 continue; 2849 } 2850 else if (placeholder == 'File') { 2851 file = fields[i].children[1].firstChild.cloneNode(false); 2852 file.tabIndex += 20; 2853 file.id = 'qrFile'; 2854 file.size = '19'; 2855 file.addEventListener('change', QR.onFileChange, false); 2856 row.appendChild(file); 2857 2858 if (UA.hasDragAndDrop) { 2859 $.addClass(file, 'qrRealFile'); 2860 2861 file = document.createElement('div'); 2862 file.id = 'qrDummyFile'; 2863 2864 el = document.createElement('button'); 2865 el.id = 'qrDummyFileButton'; 2866 el.type = 'button'; 2867 el.textContent = 'Browseโฆ'; 2868 file.appendChild(el); 2869 2870 el = document.createElement('span'); 2871 el.id = 'qrDummyFileLabel'; 2872 el.textContent = 'No file selected.'; 2873 file.appendChild(el); 2874 2875 row.appendChild(file); 2876 } 2877 2878 file.title = 'Shift + Click to remove the file'; 2879 } 2880 else { 2881 row.innerHTML = fields[i].children[1].innerHTML; 2882 if (row.firstChild.type == 'hidden') { 2883 el = row.lastChild.previousSibling; 2884 } 2885 else { 2886 el = row.firstChild; 2887 } 2888 if (el.tabIndex > 0) { 2889 el.tabIndex += 20; 2890 } 2891 if (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA') { 2892 if (el.name == 'name') { 2893 if (cookie = Main.getCookie('4chan_name')) { 2894 el.value = cookie; 2895 } 2896 } 2897 else if (el.name == 'email') { 2898 el.id = 'qrEmail'; 2899 } 2900 else if (el.name == 'com') { 2901 QR.comField = el; 2902 el.addEventListener('keydown', QR.onKeyDown, false); 2903 el.addEventListener('paste', QR.onKeyDown, false); 2904 el.addEventListener('cut', QR.onKeyDown, false); 2905 if (row.children[1]) { 2906 row.removeChild(el.nextSibling); 2907 } 2908 } 2909 else if (el.name == 'sub') { 2910 continue; 2911 } 2912 if (placeholder !== null) { 2913 el.setAttribute('placeholder', placeholder); 2914 } 2915 } 2916 else if ((el.name == 'flag')) { 2917 if (el2 = el.querySelector('option[selected]')) { 2918 el2.removeAttribute('selected'); 2919 } 2920 if ((cookie = Main.getCookie('4chan_flag')) && 2921 (el2 = el.querySelector('option[value="' + cookie + '"]'))) { 2922 el2.setAttribute('selected', 'selected'); 2923 } 2924 } 2925 } 2926 } 2927 qrForm.appendChild(row); 2928 } 2929 2930 this.btn = qrForm.querySelector('input[type="submit"]'); 2931 this.btn.previousSibling.className = 'presubmit'; 2932 this.btn.tabIndex += 20; 2933 2934 if (el = postForm.querySelector('.desktop > label > input[name="spoiler"]')) { 2935 spoiler = document.createElement('span'); 2936 spoiler.id = 'qrSpoiler'; 2937 spoiler.innerHTML = '<label>[<input type="checkbox" tabindex="' 2938 + (el.tabIndex + 20) + '" value="on" name="spoiler">Spoiler?]</label>'; 2939 file.parentNode.insertBefore(spoiler, file.nextSibling); 2940 } 2941 2942 form.appendChild(qrForm); 2943 cnt.appendChild(form); 2944 2945 qrError = document.createElement('div'); 2946 qrError.id = 'qrError'; 2947 cnt.appendChild(qrError); 2948 2949 cnt.addEventListener('click', QR.onClick, false); 2950 2951 document.body.appendChild(cnt); 2952 2953 QR.startCooldown(); 2954 2955 if (Main.threadClosed) { 2956 QR.lock(); 2957 } 2958 2959 if (!window.passEnabled) { 2960 if (window.captchaReady) { 2961 if (QR.captchaInterval === null) { 2962 QR.onCaptchaReady(); 2963 } 2964 else { 2965 QR.reloadCaptcha(); 2966 } 2967 } 2968 else { 2969 window.loadRecaptcha(); 2970 } 2971 } 2972 2973 if (!Main.hasMobileLayout) { 2974 Draggable.set($.id('qrHeader')); 2975 } 2976 }; 2977 2978 QR.onCaptchaReady = function() { 2979 if (!$.id('qrCaptchaContainer')) { 2980 QR.captchaInterval = 1; 2981 return; 2982 } 2983 2984 QR.pollCaptcha(); 2985 }; 2986 2987 QR.onFileChange = function(e) { 2988 var fsize, maxFilesize; 2989 2990 QR.needPreuploadCaptcha = false; 2991 2992 if (this.value) { 2993 maxFilesize = window.maxFilesize; 2994 2995 if (this.files) { 2996 fsize = this.files[0].size; 2997 if (this.files[0].type == 'video/webm' && window.maxWebmFilesize) { 2998 maxFilesize = window.maxWebmFilesize; 2999 } 3000 } 3001 else { 3002 fsize = 0; 3003 } 3004 3005 if (QR.fileDisabled) { 3006 QR.showPostError('Image limit reached.', 'imagelimit', true); 3007 } 3008 else if (fsize > maxFilesize) { 3009 QR.showPostError('Error: Maximum file size allowed is ' 3010 + Math.floor(maxFilesize / 1048576) + ' MB', 'filesize', true); 3011 } 3012 else { 3013 QR.hidePostError(); 3014 } 3015 3016 if (fsize >= QR.preuploadSizeLimit) { 3017 QR.needPreuploadCaptcha = true; 3018 } 3019 } 3020 else { 3021 QR.hidePostError(); 3022 } 3023 3024 QR.startCooldown(); 3025 }; 3026 3027 QR.onKeyDown = function(e) { 3028 if (e.ctrlKey && e.keyCode == 83) { 3029 var ta, start, end, spoiler; 3030 3031 e.stopPropagation(); 3032 e.preventDefault(); 3033 3034 ta = e.target; 3035 start = ta.selectionStart; 3036 end = ta.selectionEnd; 3037 3038 if (ta.value) { 3039 spoiler = '[spoiler]' + ta.value.slice(start, end) + '[/spoiler]'; 3040 ta.value = ta.value.slice(0, start) + spoiler + ta.value.slice(end); 3041 ta.setSelectionRange(end + 19, end + 19); 3042 } 3043 else { 3044 ta.value = '[spoiler][/spoiler]'; 3045 ta.setSelectionRange(9, 9); 3046 } 3047 } 3048 else if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { 3049 QR.close(); 3050 return; 3051 } 3052 3053 clearTimeout(QR.lenCheckTimeout); 3054 QR.lenCheckTimeout = setTimeout(QR.checkComLength, 500); 3055 }; 3056 3057 QR.checkComLength = function() { 3058 var byteLength, qrError; 3059 3060 if (QR.comLength) { 3061 byteLength = encodeURIComponent(QR.comField.value).split(/%..|./).length - 1; 3062 3063 if (byteLength > QR.comLength) { 3064 QR.showPostError('Error: Comment too long (' 3065 + byteLength + '/' + QR.comLength + ').', 'length', true); 3066 } 3067 else { 3068 QR.hidePostError('length'); 3069 } 3070 } 3071 }; 3072 3073 QR.close = function() { 3074 var el, cnt = $.id('quickReply'); 3075 3076 QR.comField = null; 3077 QR.currentTid = null; 3078 3079 clearInterval(QR.captchaInterval); 3080 clearInterval(QR.pulse); 3081 3082 if (QR.xhr) { 3083 QR.xhr.abort(); 3084 QR.xhr = null; 3085 } 3086 3087 cnt.removeEventListener('click', QR.onClick, false); 3088 3089 (el = $.id('qrFile')) && el.removeEventListener('change', QR.startCooldown, false); 3090 (el = $.id('qrEmail')) && el.removeEventListener('change', QR.startCooldown, false); 3091 $.tag('textarea', cnt)[0].removeEventListener('keydown', QR.onKeyDown, false); 3092 3093 Draggable.unset($.id('qrHeader')); 3094 3095 if (window.RecaptchaState) { 3096 Recaptcha.destroy(); 3097 window.captchaReady = false; 3098 if (el = $.id('captchaContainer')) { 3099 el.innerHTML = '<div class="placeholder">' 3100 + el.getAttribute('data-placeholder') + '</div>'; 3101 } 3102 } 3103 3104 document.body.removeChild(cnt); 3105 }; 3106 3107 QR.cloneCaptcha = function() { 3108 var row = $.id('qrCaptchaContainer'); 3109 3110 if (!row) { 3111 return false; 3112 } 3113 3114 row.innerHTML = '<img id="qrCaptcha" title="Reload" width="300" height="57" src="' 3115 + $.id('recaptcha_challenge_image').src + '" alt="reCAPTCHA challenge image">' 3116 + (window.preupload_captcha ? '<input id="qrCapToken" type="hidden" name="captcha_token" disabled>' : '') 3117 + '<input id="qrCapField" tabindex="25" name="recaptcha_response_field" ' 3118 + 'placeholder="Type the text (Required)" ' 3119 + 'type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">' 3120 + '<input id="qrChallenge" name="recaptcha_challenge_field" type="hidden" value="' 3121 + $.id('recaptcha_challenge_field').value + '">'; 3122 3123 return true; 3124 }; 3125 3126 QR.reloadCaptcha = function(focus) { 3127 var pulse, poll; 3128 3129 if (QR.noCaptcha || !$.id('recaptcha_image') || !window.RecaptchaState) { 3130 return; 3131 } 3132 3133 poll = function() { 3134 var el; 3135 clearTimeout(pulse); 3136 if (el = $.id('recaptcha_challenge_image')) { 3137 QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay); 3138 QR.cloneCaptcha(); 3139 if (focus) { 3140 $.id('qrCapField').focus(); 3141 } 3142 } 3143 else { 3144 pulse = setTimeout(poll, 100); 3145 } 3146 }; 3147 clearInterval(QR.captchaInterval); 3148 Recaptcha.destroy(); 3149 window.loadRecaptcha(); 3150 pulse = setTimeout(poll, 100); 3151 }; 3152 3153 QR.pollCaptcha = function() { 3154 clearTimeout(QR.captchaPollTimeout); 3155 3156 if ($.id('recaptcha_challenge_image')) { 3157 QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay); 3158 QR.cloneCaptcha(); 3159 } 3160 else { 3161 QR.captchaPollTimeout = setTimeout(QR.pollCaptcha, 100); 3162 } 3163 }; 3164 3165 QR.onClick = function(e) { 3166 var t = e.target; 3167 3168 if (t.type == 'submit') { 3169 e.preventDefault(); 3170 QR.submit(e.shiftKey); 3171 } 3172 else { 3173 switch (t.id) { 3174 case 'qrFile': 3175 if (e.shiftKey) { 3176 e.preventDefault(); 3177 QR.resetFile(); 3178 } 3179 break; 3180 case 'qrDummyFile': 3181 case 'qrDummyFileButton': 3182 case 'qrDummyFileLabel': 3183 e.preventDefault(); 3184 if (e.shiftKey) { 3185 QR.resetFile(); 3186 } 3187 else { 3188 $.id('qrFile').click(); 3189 } 3190 break; 3191 case 'qrCaptcha': 3192 QR.reloadCaptcha(true); 3193 break; 3194 case 'qrClose': 3195 QR.close(); 3196 break; 3197 } 3198 } 3199 }; 3200 3201 QR.submit = function(force) { 3202 if (force) { 3203 QR.submitDirect(true); 3204 } 3205 else if (!QR.noCaptcha && window.preupload_captcha && QR.needPreuploadCaptcha) { 3206 QR.submitPreupload(); 3207 } 3208 else { 3209 QR.submitDirect(); 3210 } 3211 }; 3212 3213 QR.showPostError = function(msg, type, silent) { 3214 var qrError; 3215 3216 qrError = $.id('qrError'); 3217 3218 if (!qrError) { 3219 return; 3220 } 3221 3222 qrError.innerHTML = msg; 3223 qrError.style.display = 'block'; 3224 3225 qrError.setAttribute('data-type', type || ''); 3226 3227 if (!silent && (document.hidden 3228 || document.mozHidden 3229 || document.webkitHidden 3230 || document.msHidden)) { 3231 alert('Posting Error'); 3232 } 3233 }; 3234 3235 QR.hidePostError = function(type) { 3236 var el = $.id('qrError'); 3237 3238 if (!el.hasAttribute('style')) { 3239 return; 3240 } 3241 3242 if (!type || el.getAttribute('data-type') == type) { 3243 el.removeAttribute('style'); 3244 } 3245 }; 3246 3247 QR.resetFile = function() { 3248 var file, el; 3249 3250 el = document.createElement('input'); 3251 el.id = 'qrFile'; 3252 el.type = 'file'; 3253 el.size = '19'; 3254 el.name = 'upfile'; 3255 el.addEventListener('change', QR.onFileChange, false); 3256 3257 file = $.id('qrFile'); 3258 file.removeEventListener('change', QR.onFileChange, false); 3259 3260 file.parentNode.replaceChild(el, file); 3261 3262 QR.hidePostError('imagelimit'); 3263 3264 QR.needPreuploadCaptcha = false; 3265 3266 QR.startCooldown(); 3267 }; 3268 3269 QR.submitPreupload = function() { 3270 var token, challenge, response, data; 3271 3272 if (!QR.presubmitChecks()) { 3273 return; 3274 } 3275 3276 challenge = $.id('qrChallenge'); 3277 response = $.id('qrCapField'); 3278 3279 if (response.value == '') { 3280 QR.showPostError('You forgot to type in the CAPTCHA.'); 3281 response.focus(); 3282 return; 3283 } 3284 3285 data = new FormData(); 3286 data.append('mode', 'checkcaptcha'); 3287 data.append('challenge', challenge.value); 3288 data.append('response', response.value); 3289 3290 QR.xhr = new XMLHttpRequest(); 3291 3292 QR.xhr.open('POST', document.forms.post.action, true); 3293 3294 QR.xhr.onerror = function() { 3295 QR.xhr = null; 3296 QR.submitDirect(); 3297 }; 3298 3299 QR.xhr.onload = function() { 3300 var el, resp; 3301 3302 QR.xhr = null; 3303 3304 try { 3305 resp = JSON.parse(this.responseText); 3306 } 3307 catch(e) { 3308 console.log("Couldn't verify captcha."); 3309 QR.submitDirect(); 3310 return; 3311 } 3312 3313 if (resp.token) { 3314 el = $.id('qrCapToken'); 3315 el.value = resp.token; 3316 el.removeAttribute('disabled'); 3317 3318 QR.submitDirect(); 3319 } 3320 else if (resp.error) { 3321 QR.reloadCaptcha(); 3322 QR.btn.value = 'Post'; 3323 QR.showPostError(resp.error); 3324 } 3325 else { 3326 if (resp.fail) { 3327 console.log(resp.fail); 3328 } 3329 QR.submitDirect(); 3330 } 3331 }; 3332 3333 token = $.id('qrCapToken'); 3334 token.value = ''; 3335 token.setAttribute('disabled', '1'); 3336 3337 QR.btn.value = 'Sending'; 3338 3339 QR.xhr.send(data); 3340 }; 3341 3342 QR.submitDirect = function(force) { 3343 var field, formdata, file; 3344 3345 QR.hidePostError(); 3346 3347 if (!QR.presubmitChecks(force)) { 3348 return; 3349 } 3350 3351 QR.auto = false; 3352 3353 if (!force && (field = $.id('qrCapField')) && field.value == '') { 3354 QR.showPostError('You forgot to type in the CAPTCHA.'); 3355 field.focus(); 3356 return; 3357 } 3358 3359 QR.xhr = new XMLHttpRequest(); 3360 3361 QR.xhr.open('POST', document.forms.qrPost.action, true); 3362 3363 QR.xhr.withCredentials = true; 3364 3365 QR.xhr.upload.onprogress = function(e) { 3366 if (e.loaded >= e.total) { 3367 QR.btn.value = '100%'; 3368 } 3369 else { 3370 QR.btn.value = (0 | (e.loaded / e.total * 100)) + '%'; 3371 } 3372 }; 3373 3374 QR.xhr.onerror = function() { 3375 QR.xhr = null; 3376 QR.showPostError('Connection error.'); 3377 }; 3378 3379 QR.xhr.onload = function() { 3380 var resp, el, hasFile, ids, tid, pid, tracked; 3381 3382 QR.xhr = null; 3383 3384 QR.btn.value = 'Post'; 3385 3386 if (this.status == 200) { 3387 if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { 3388 QR.reloadCaptcha(true); 3389 QR.showPostError(resp[1]); 3390 return; 3391 } 3392 3393 if (ids = this.responseText.match(/<!-- thread:([0-9]+),no:([0-9]+) -->/)) { 3394 tid = ids[1]; 3395 pid = ids[2]; 3396 3397 QR.lastTid = tid; 3398 3399 localStorage.setItem('4chan-cd-' + Main.board + '-tid', tid); 3400 3401 hasFile = (el = $.id('qrFile')) && el.value; 3402 3403 QR.setPostTime(); 3404 3405 if (Config.persistentQR) { 3406 $.byName('com')[1].value = ''; 3407 3408 if (el = $.byName('spoiler')[2]) { 3409 el.checked = false; 3410 } 3411 3412 QR.reloadCaptcha(); 3413 3414 if (hasFile) { 3415 QR.resetFile(); 3416 } 3417 3418 QR.startCooldown(); 3419 } 3420 else { 3421 QR.close(); 3422 } 3423 3424 if (Main.tid) { 3425 if (Config.threadWatcher) { 3426 ThreadWatcher.setLastRead(pid, tid); 3427 } 3428 QR.lastReplyId = +pid; 3429 Parser.trackedReplies['>>' + pid] = 1; 3430 Parser.saveTrackedReplies(tid, Parser.trackedReplies); 3431 } 3432 else { 3433 tracked = Parser.getTrackedReplies(tid) || {}; 3434 tracked['>>' + pid] = 1; 3435 Parser.saveTrackedReplies(tid, tracked); 3436 } 3437 3438 UA.dispatchEvent('4chanQRPostSuccess', { threadId: tid, postId: pid }); 3439 } 3440 3441 if (ThreadUpdater.enabled) { 3442 setTimeout(ThreadUpdater.forceUpdate, 500); 3443 } 3444 } 3445 else { 3446 QR.showPostError('Error: ' + this.status + ' ' + this.statusText); 3447 } 3448 }; 3449 3450 formdata = new FormData(document.forms.qrPost); 3451 3452 clearInterval(QR.pulse); 3453 3454 QR.btn.value = 'Sending'; 3455 3456 QR.xhr.send(formdata); 3457 }; 3458 3459 QR.presubmitChecks = function(force) { 3460 if (QR.xhr) { 3461 QR.xhr.abort(); 3462 QR.xhr = null; 3463 QR.showPostError('Aborted.'); 3464 QR.btn.value = 'Post'; 3465 return false; 3466 } 3467 3468 if (!force && QR.cooldown) { 3469 if (QR.auto = !QR.auto) { 3470 QR.btn.value = QR.cooldown + 's (auto)'; 3471 } 3472 else { 3473 QR.btn.value = QR.cooldown + 's'; 3474 } 3475 return false; 3476 } 3477 3478 return true; 3479 }; 3480 3481 QR.getCooldown = function(type) { 3482 if (QR.currentTid != QR.lastTid) { 3483 return QR.cooldowns[type]; 3484 } 3485 else { 3486 return QR.cooldowns[type + '_intra']; 3487 } 3488 }; 3489 3490 QR.setPostTime = function() { 3491 return localStorage.setItem('4chan-cd-' + Main.board, Date.now()); 3492 }; 3493 3494 QR.getPostTime = function() { 3495 return localStorage.getItem('4chan-cd-' + Main.board); 3496 }; 3497 3498 QR.removePostTime = function() { 3499 return localStorage.removeItem('4chan-cd-' + Main.board); 3500 }; 3501 3502 QR.startCooldown = function() { 3503 var type, el, time; 3504 3505 if (QR.noCooldown || !$.id('quickReply') || QR.xhr) { 3506 return; 3507 } 3508 3509 clearInterval(QR.pulse); 3510 3511 type = ((el = $.id('qrFile')) && el.value) ? 'image' : 'reply'; 3512 3513 time = QR.getPostTime(type); 3514 3515 if (!time) { 3516 QR.btn.value = 'Post'; 3517 return; 3518 } 3519 3520 QR.timestamp = parseInt(time, 10); 3521 3522 QR.activeDelay = QR.getCooldown(type); 3523 3524 QR.cdElapsed = Date.now() - QR.timestamp; 3525 3526 QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000); 3527 3528 if (QR.cooldown <= 0 || QR.cdElapsed < 0) { 3529 QR.cooldown = false; 3530 QR.removePostTime(type); 3531 return; 3532 } 3533 3534 QR.btn.value = QR.cooldown + 's'; 3535 3536 QR.pulse = setInterval(QR.onPulse, 1000); 3537 }; 3538 3539 QR.onPulse = function() { 3540 QR.cdElapsed = Date.now() - QR.timestamp; 3541 QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000); 3542 if (QR.cooldown <= 0) { 3543 clearInterval(QR.pulse); 3544 QR.btn.value = 'Post'; 3545 QR.cooldown = false; 3546 if (QR.auto) { 3547 QR.submit(); 3548 } 3549 } 3550 else { 3551 QR.btn.value = QR.cooldown + (QR.auto ? 's (auto)' : 's'); 3552 } 3553 }; 3554 3555 /** 3556 * Thread hiding 3557 */ 3558 var ThreadHiding = {}; 3559 3560 ThreadHiding.init = function() { 3561 this.threshold = 43200000; // 12 hours 3562 3563 this.hidden = {}; 3564 3565 this.load(); 3566 3567 this.purge(); 3568 }; 3569 3570 ThreadHiding.clear = function(silent) { 3571 var i, id, key, msg; 3572 3573 this.load(); 3574 3575 i = 0; 3576 3577 for (id in this.hidden) { 3578 ++i; 3579 } 3580 3581 key = '4chan-hide-t-' + Main.board; 3582 3583 if (!silent) { 3584 if (!i) { 3585 alert("You don't have any hidden threads on /" + Main.board + '/'); 3586 return; 3587 } 3588 3589 msg = 'This will unhide ' + i + ' thread' + (i > 1 ? 's' : '') + ' on /' + Main.board + '/'; 3590 3591 if (!confirm(msg)) { 3592 return; 3593 } 3594 3595 localStorage.removeItem(key); 3596 } 3597 else { 3598 localStorage.removeItem(key); 3599 } 3600 }; 3601 3602 ThreadHiding.isHidden = function(tid) { 3603 var sa = $.id('sa' + tid); 3604 3605 return !sa || sa.hasAttribute('data-hidden'); 3606 }; 3607 3608 ThreadHiding.toggle = function(tid) { 3609 if (this.isHidden(tid)) { 3610 this.show(tid); 3611 } 3612 else { 3613 this.hide(tid); 3614 } 3615 this.save(); 3616 }; 3617 3618 ThreadHiding.show = function(tid) { 3619 var sa, th; 3620 3621 th = $.id('t' + tid); 3622 3623 sa = $.id('sa' + tid); 3624 sa.removeAttribute('data-hidden'); 3625 3626 if (Main.hasMobileLayout) { 3627 sa.textContent = 'Hide'; 3628 $.removeClass(sa, 'mobile-tu-show'); 3629 $.cls('postLink', th)[0].appendChild(sa); 3630 3631 th.style.display = null; 3632 $.removeClass(th.nextElementSibling, 'mobile-hr-hidden'); 3633 } 3634 else { 3635 sa.firstChild.src = Main.icons.minus; 3636 $.removeClass(th, 'post-hidden'); 3637 } 3638 3639 delete this.hidden[tid]; 3640 }; 3641 3642 ThreadHiding.hide = function(tid) { 3643 var sa, th; 3644 3645 th = $.id('t' + tid); 3646 3647 if (Main.hasMobileLayout) { 3648 th.style.display = 'none'; 3649 $.addClass(th.nextElementSibling, 'mobile-hr-hidden'); 3650 3651 sa = $.id('sa' + tid); 3652 sa.setAttribute('data-hidden', tid); 3653 sa.textContent = 'Show Hidden Thread'; 3654 $.addClass(sa, 'mobile-tu-show'); 3655 3656 th.parentNode.insertBefore(sa, th); 3657 } 3658 else { 3659 if (Config.hideStubs && !$.cls('stickyIcon', th)[0]) { 3660 th.style.display = th.nextElementSibling.style.display = 'none'; 3661 } 3662 else { 3663 sa = $.id('sa' + tid); 3664 sa.setAttribute('data-hidden', tid); 3665 sa.firstChild.src = Main.icons.plus; 3666 th.className += ' post-hidden'; 3667 } 3668 } 3669 3670 this.hidden[tid] = Date.now(); 3671 }; 3672 3673 ThreadHiding.load = function() { 3674 var storage; 3675 3676 if (storage = localStorage.getItem('4chan-hide-t-' + Main.board)) { 3677 this.hidden = JSON.parse(storage); 3678 } 3679 }; 3680 3681 ThreadHiding.purge = function() { 3682 var i, hasHidden, lastPurged, key; 3683 3684 key = '4chan-purge-t-' + Main.board; 3685 3686 lastPurged = localStorage.getItem(key); 3687 3688 for (i in this.hidden) { 3689 hasHidden = true; 3690 break; 3691 } 3692 3693 if (!hasHidden) { 3694 return; 3695 } 3696 3697 if (!lastPurged || lastPurged < Date.now() - this.threshold) { 3698 $.get('//a.4cdn.org/' + Main.board + '/threads.json', 3699 { 3700 onload: function() { 3701 var i, j, t, p, pages, threads, alive; 3702 3703 if (this.status == 200) { 3704 alive = {}; 3705 pages = JSON.parse(this.responseText); 3706 for (i = 0; p = pages[i]; ++i) { 3707 threads = p.threads; 3708 for (j = 0; t = threads[j]; ++j) { 3709 if (ThreadHiding.hidden[t.no]) { 3710 alive[t.no] = 1; 3711 } 3712 } 3713 } 3714 ThreadHiding.hidden = alive; 3715 ThreadHiding.save(); 3716 localStorage.setItem(key, Date.now()); 3717 } 3718 else { 3719 console.log('Bad status code while purging threads'); 3720 } 3721 }, 3722 onerror: function() { 3723 console.log('Error while purging hidden threads'); 3724 } 3725 }); 3726 } 3727 }; 3728 3729 ThreadHiding.save = function() { 3730 for (var i in this.hidden) { 3731 localStorage.setItem('4chan-hide-t-' + Main.board, 3732 JSON.stringify(this.hidden) 3733 ); 3734 return; 3735 } 3736 localStorage.removeItem('4chan-hide-t-' + Main.board); 3737 }; 3738 3739 /** 3740 * Reply hiding 3741 */ 3742 var ReplyHiding = {}; 3743 3744 ReplyHiding.init = function() { 3745 this.threshold = 7 * 86400000; 3746 this.hidden = {}; 3747 this.load(); 3748 }; 3749 3750 ReplyHiding.isHidden = function(pid) { 3751 var sa = $.id('sa' + pid); 3752 3753 return !sa || sa.hasAttribute('data-hidden'); 3754 }; 3755 3756 ReplyHiding.toggle = function(pid) { 3757 if (this.isHidden(pid)) { 3758 this.show(pid); 3759 } 3760 else { 3761 this.hide(pid); 3762 } 3763 this.save(); 3764 }; 3765 3766 ReplyHiding.show = function(pid) { 3767 var post, sa; 3768 3769 post = $.id('pc' + pid); 3770 3771 $.removeClass(post, 'post-hidden'); 3772 3773 sa = $.id('sa' + pid); 3774 sa.removeAttribute('data-hidden'); 3775 sa.firstChild.src = Main.icons.minus; 3776 3777 delete this.hidden[pid]; 3778 }; 3779 3780 ReplyHiding.hide = function(pid) { 3781 var post, sa; 3782 3783 post = $.id('pc' + pid); 3784 post.className += ' post-hidden'; 3785 3786 sa = $.id('sa' + pid); 3787 sa.setAttribute('data-hidden', pid); 3788 sa.firstChild.src = Main.icons.plus; 3789 3790 this.hidden[pid] = Date.now(); 3791 }; 3792 3793 ReplyHiding.load = function() { 3794 var storage; 3795 3796 if (storage = localStorage.getItem('4chan-hide-r-' + Main.board)) { 3797 this.hidden = JSON.parse(storage); 3798 } 3799 }; 3800 3801 ReplyHiding.purge = function() { 3802 var tid, now; 3803 3804 now = Date.now(); 3805 3806 for (tid in this.hidden) { 3807 if (now - this.hidden[tid] > this.threshold) { 3808 delete this.hidden[tid]; 3809 } 3810 } 3811 this.save(); 3812 }; 3813 3814 ReplyHiding.save = function() { 3815 for (var i in this.hidden) { 3816 localStorage.setItem('4chan-hide-r-' + Main.board, 3817 JSON.stringify(this.hidden) 3818 ); 3819 return; 3820 } 3821 localStorage.removeItem('4chan-hide-r-' + Main.board); 3822 }; 3823 3824 /** 3825 * Thread watcher 3826 */ 3827 var ThreadWatcher = {}; 3828 3829 ThreadWatcher.init = function() { 3830 var cnt, jumpTo, rect, el; 3831 3832 this.listNode = null; 3833 this.charLimit = 45; 3834 this.watched = {}; 3835 this.isRefreshing = false; 3836 3837 if (Main.hasMobileLayout) { 3838 el = document.createElement('a'); 3839 el.href = '#'; 3840 el.textContent = 'TW'; 3841 el.addEventListener('click', ThreadWatcher.toggleList, false); 3842 cnt = $.id('settingsWindowLinkMobile'); 3843 cnt.parentNode.insertBefore(el, cnt); 3844 cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); 3845 } 3846 3847 if (location.hash && (jumpTo = location.hash.split('lr')[1])) { 3848 if (jumpTo = $.id('pc' + jumpTo)) { 3849 if (jumpTo.nextElementSibling) { 3850 jumpTo = jumpTo.nextElementSibling; 3851 if (el = $.id('p' + jumpTo.id.slice(2))) { 3852 $.addClass(el, 'highlight'); 3853 } 3854 } 3855 3856 rect = jumpTo.getBoundingClientRect(); 3857 3858 if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) { 3859 window.scrollBy(0, rect.top); 3860 } 3861 } 3862 3863 if (window.history && history.replaceState) { 3864 history.replaceState(null, '', location.href.split('#', 1)[0]); 3865 } 3866 } 3867 3868 cnt = document.createElement('div'); 3869 cnt.id = 'threadWatcher'; 3870 cnt.className = 'extPanel reply'; 3871 cnt.setAttribute('data-trackpos', 'TW-position'); 3872 3873 if (Main.hasMobileLayout) { 3874 cnt.style.display = 'none'; 3875 } 3876 else { 3877 if (Config['TW-position']) { 3878 cnt.style.cssText = Config['TW-position']; 3879 } 3880 else { 3881 cnt.style.left = '10px'; 3882 cnt.style.top = '380px'; 3883 } 3884 3885 if (Config.fixedThreadWatcher) { 3886 cnt.style.position = 'fixed'; 3887 } 3888 else { 3889 cnt.style.position = ''; 3890 } 3891 } 3892 3893 cnt.innerHTML = '<div class="drag" id="twHeader">' 3894 + (Main.hasMobileLayout ? ('<img id="twClose" class="pointer" src="' 3895 + Main.icons.cross + '" alt="X">') : '') 3896 + 'Thread Watcher' 3897 + (UA.hasCORS ? ('<img id="twPrune" class="pointer right" src="' 3898 + Main.icons.refresh + '" alt="R" title="Refresh"></div>') : '</div>'); 3899 3900 this.listNode = document.createElement('ul'); 3901 this.listNode.id = 'watchList'; 3902 3903 this.load(); 3904 3905 if (Main.tid) { 3906 this.refreshCurrent(); 3907 } 3908 3909 this.build(); 3910 3911 cnt.appendChild(this.listNode); 3912 document.body.appendChild(cnt); 3913 cnt.addEventListener('mouseup', this.onClick, false); 3914 Draggable.set($.id('twHeader')); 3915 window.addEventListener('storage', this.syncStorage, false); 3916 3917 if (Main.hasMobileLayout) { 3918 if (Main.tid) { 3919 ThreadWatcher.initMobileButtons(); 3920 } 3921 } 3922 else if (!Main.tid && this.canAutoRefresh()) { 3923 this.refresh(); 3924 } 3925 }; 3926 3927 ThreadWatcher.toggleList = function(e) { 3928 var el = $.id('threadWatcher'); 3929 3930 e && e.preventDefault(); 3931 3932 if (!Main.tid && ThreadWatcher.canAutoRefresh()) { 3933 ThreadWatcher.refresh(); 3934 } 3935 3936 if (el.style.display == 'none') { 3937 el.style.top = (window.pageYOffset + 30) + 'px'; 3938 el.style.display = ''; 3939 } 3940 else { 3941 el.style.display = 'none'; 3942 } 3943 }; 3944 3945 ThreadWatcher.syncStorage = function(e) { 3946 var key; 3947 3948 if (!e.key) { 3949 return; 3950 } 3951 3952 key = e.key.split('-'); 3953 3954 if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) { 3955 ThreadWatcher.watched = JSON.parse(e.newValue); 3956 ThreadWatcher.build(true); 3957 } 3958 }; 3959 3960 ThreadWatcher.load = function() { 3961 if (storage = localStorage.getItem('4chan-watch')) { 3962 this.watched = JSON.parse(storage); 3963 } 3964 }; 3965 3966 ThreadWatcher.build = function(rebuildButtons) { 3967 var i, html, tuid, key, nodes, cls; 3968 3969 html = ''; 3970 3971 for (key in this.watched) { 3972 tuid = key.split('-'); 3973 html += '<li id="watch-' + key 3974 + '"><span class="pointer" data-cmd="unwatch" data-id="' 3975 + tuid[0] + '" data-board="' + tuid[1] + '">×</span> <a href="' 3976 + Main.linkToThread(tuid[0], tuid[1]) + '#lr' + this.watched[key][1] + '"'; 3977 3978 if (this.watched[key][1] == -1) { 3979 html += ' class="deadlink">'; 3980 } 3981 else { 3982 if (this.watched[key][3]) { 3983 cls = 'archivelink'; 3984 } 3985 else { 3986 cls = false; 3987 } 3988 if (this.watched[key][2]) { 3989 html += ' class="' + (cls ? (cls + ' ') : '') 3990 + 'hasNewReplies">(' + this.watched[key][2] + ') '; 3991 } 3992 else { 3993 html += (cls ? ('class="' + cls + '"') : '') + '>'; 3994 } 3995 } 3996 3997 html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '</a></li>'; 3998 } 3999 4000 if (rebuildButtons) { 4001 ThreadWatcher.rebuildButtons(); 4002 } 4003 4004 ThreadWatcher.listNode.innerHTML = html; 4005 }; 4006 4007 ThreadWatcher.rebuildButtons = function() { 4008 var i, buttons, key; 4009 4010 buttons = $.cls('wbtn'); 4011 4012 for (i = 0; btn = buttons[i]; ++i) { 4013 key = btn.getAttribute('data-id') + '-' + Main.board; 4014 if (ThreadWatcher.watched[key]) { 4015 if (!btn.hasAttribute('data-active')) { 4016 btn.src = Main.icons.watched; 4017 btn.setAttribute('data-active', '1'); 4018 } 4019 } 4020 else { 4021 if (btn.hasAttribute('data-active')) { 4022 btn.src = Main.icons.notwatched; 4023 btn.removeAttribute('data-active'); 4024 } 4025 } 4026 } 4027 }; 4028 4029 ThreadWatcher.initMobileButtons = function() { 4030 var el, cnt, key, ref; 4031 4032 el = document.createElement('img'); 4033 4034 key = Main.tid + '-' + Main.board; 4035 4036 if (ThreadWatcher.watched[key]) { 4037 el.src = Main.icons.watched; 4038 el.setAttribute('data-active', '1'); 4039 } 4040 else { 4041 el.src = Main.icons.notwatched; 4042 } 4043 4044 el.className = 'extButton wbtn wbtn-' + key; 4045 el.setAttribute('data-cmd', 'watch'); 4046 el.setAttribute('data-id', Main.tid); 4047 el.alt = 'W'; 4048 4049 cnt = document.createElement('span'); 4050 cnt.className = 'mobileib button'; 4051 4052 cnt.appendChild(el); 4053 4054 if (ref = $.cls('navLinks')[0]) { 4055 ref.appendChild(document.createTextNode(' ')); 4056 ref.appendChild(cnt); 4057 } 4058 4059 if (ref = $.cls('navLinks')[3]) { 4060 ref.appendChild(document.createTextNode(' ')); 4061 ref.appendChild(cnt.cloneNode(true)); 4062 } 4063 }; 4064 4065 ThreadWatcher.onClick = function(e) { 4066 var t = e.target; 4067 4068 if (t.hasAttribute('data-id')) { 4069 ThreadWatcher.toggle( 4070 t.getAttribute('data-id'), 4071 t.getAttribute('data-board') 4072 ); 4073 } 4074 else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { 4075 ThreadWatcher.refresh(); 4076 } 4077 else if (t.id == 'twClose') { 4078 ThreadWatcher.toggleList(); 4079 } 4080 }; 4081 4082 ThreadWatcher.toggle = function(tid, board, synced) { 4083 var i, key, label, lastReply, thread; 4084 4085 key = tid + '-' + (board || Main.board); 4086 4087 if (this.watched[key]) { 4088 delete this.watched[key]; 4089 } 4090 else { 4091 if (label = $.cls('subject', $.id('pi' + tid))[0].textContent) { 4092 label = label.slice(0, this.charLimit); 4093 } 4094 else if (label = $.id('m' + tid).innerHTML) { 4095 label = label.replace(/(?:<br>)+/g, ' ') 4096 .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); 4097 } 4098 else { 4099 label = 'No.' + tid; 4100 } 4101 4102 if ((thread = $.id('t' + tid)).children[1]) { 4103 lastReply = thread.lastElementChild.id.slice(2); 4104 } 4105 else { 4106 lastReply = tid; 4107 } 4108 4109 this.watched[key] = [ label, lastReply, 0 ]; 4110 } 4111 this.save(); 4112 this.load(); 4113 this.build(true); 4114 }; 4115 4116 ThreadWatcher.save = function() { 4117 ThreadWatcher.sortByBoard(); 4118 4119 localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); 4120 }; 4121 4122 ThreadWatcher.sortByBoard = function() { 4123 var i, self, key, sorted, keys; 4124 4125 self = ThreadWatcher; 4126 4127 sorted = {}; 4128 keys = []; 4129 4130 for (key in self.watched) { 4131 keys.push(key); 4132 } 4133 4134 keys.sort(function(a, b) { 4135 a = a.split('-')[1]; 4136 b = b.split('-')[1]; 4137 4138 if (a < b) { 4139 return -1; 4140 } 4141 if (a > b) { 4142 return 1; 4143 } 4144 return 0; 4145 }); 4146 4147 for (i = 0; key = keys[i]; ++i) { 4148 sorted[key] = self.watched[key]; 4149 } 4150 4151 self.watched = sorted; 4152 }; 4153 4154 ThreadWatcher.canAutoRefresh = function() { 4155 var time; 4156 4157 if (time = localStorage.getItem('4chan-tw-timestamp')) { 4158 return Date.now() - (+time) >= 60000; 4159 } 4160 return false; 4161 }; 4162 4163 ThreadWatcher.setRefreshTimestamp = function() { 4164 localStorage.setItem('4chan-tw-timestamp', Date.now()); 4165 }; 4166 4167 ThreadWatcher.refresh = function() { 4168 var i, to, key, total, img; 4169 4170 if (total = $.id('watchList').children.length) { 4171 i = to = 0; 4172 img = $.id('twPrune'); 4173 img.src = Main.icons.rotate; 4174 ThreadWatcher.isRefreshing = true; 4175 ThreadWatcher.setRefreshTimestamp(); 4176 for (key in ThreadWatcher.watched) { 4177 setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); 4178 to += 200; 4179 } 4180 } 4181 }; 4182 4183 ThreadWatcher.refreshCurrent = function(rebuild) { 4184 var key, thread, lastReply; 4185 4186 key = Main.tid + '-' + Main.board; 4187 4188 if (this.watched[key]) { 4189 if ((thread = $.id('t' + Main.tid)).children[1]) { 4190 lastReply = thread.lastElementChild.id.slice(2); 4191 } 4192 else { 4193 lastReply = Main.tid; 4194 } 4195 if (this.watched[key][1] < lastReply) { 4196 this.watched[key][1] = lastReply; 4197 } 4198 4199 this.watched[key][2] = 0; 4200 this.save(); 4201 4202 if (rebuild) { 4203 this.build(); 4204 } 4205 } 4206 }; 4207 4208 ThreadWatcher.setLastRead = function(pid, tid) { 4209 var key = tid + '-' + Main.board; 4210 4211 if (this.watched[key]) { 4212 this.watched[key][1] = pid; 4213 this.watched[key][2] = 0; 4214 this.save(); 4215 this.build(); 4216 } 4217 }; 4218 4219 ThreadWatcher.onRefreshEnd = function(img) { 4220 img.src = Main.icons.refresh; 4221 this.isRefreshing = false; 4222 this.save(); 4223 this.load(); 4224 this.build(); 4225 }; 4226 4227 ThreadWatcher.fetch = function(key, img) { 4228 var tuid, xhr, li, method; 4229 4230 li = $.id('watch-' + key); 4231 4232 if (ThreadWatcher.watched[key][1] == -1) { 4233 delete ThreadWatcher.watched[key]; 4234 li.parentNode.removeChild(li); 4235 if (img) { 4236 ThreadWatcher.onRefreshEnd(img); 4237 } 4238 return; 4239 } 4240 4241 tuid = key.split('-'); // tid, board 4242 4243 xhr = new XMLHttpRequest(); 4244 xhr.onload = function() { 4245 var i, newReplies, posts, lastReply; 4246 if (this.status == 200) { 4247 posts = Parser.parseThreadJSON(this.responseText); 4248 lastReply = ThreadWatcher.watched[key][1]; 4249 newReplies = 0; 4250 for (i = posts.length - 1; i >= 1; i--) { 4251 if (posts[i].no <= lastReply) { 4252 break; 4253 } 4254 ++newReplies; 4255 } 4256 if (newReplies > ThreadWatcher.watched[key][2]) { 4257 ThreadWatcher.watched[key][2] = newReplies; 4258 } 4259 if (posts[0].archived) { 4260 ThreadWatcher.watched[key][3] = 1; 4261 } 4262 } 4263 else if (this.status == 404) { 4264 ThreadWatcher.watched[key][1] = -1; 4265 } 4266 if (img) { 4267 ThreadWatcher.onRefreshEnd(img); 4268 } 4269 }; 4270 if (img) { 4271 xhr.onerror = xhr.onload; 4272 } 4273 xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); 4274 xhr.send(null); 4275 }; 4276 4277 /** 4278 * Thread expansion 4279 */ 4280 var ThreadExpansion = {}; 4281 4282 ThreadExpansion.init = function() { 4283 this.enabled = UA.hasCORS; 4284 }; 4285 4286 ThreadExpansion.expandComment = function(link) { 4287 var ids, tid, pid, abbr; 4288 4289 if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) { 4290 return; 4291 } 4292 4293 tid = ids[1]; 4294 pid = ids[2]; 4295 4296 abbr = link.parentNode; 4297 abbr.textContent = 'Loading...'; 4298 4299 $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', 4300 { 4301 onload: function() { 4302 var i, msg, com, post, posts; 4303 4304 if (this.status == 200) { 4305 msg = $.id('m' + pid); 4306 4307 posts = Parser.parseThreadJSON(this.responseText); 4308 4309 if (tid == pid) { 4310 post = posts[0]; 4311 } 4312 else { 4313 for (i = posts.length - 1; i > 0; i--) { 4314 if (posts[i].no == pid) { 4315 post = posts[i]; 4316 break; 4317 } 4318 } 4319 } 4320 4321 if (post) { 4322 post = Parser.buildHTMLFromJSON(post, Main.board); 4323 4324 msg.innerHTML = $.cls('postMessage', post)[0].innerHTML; 4325 4326 if (Parser.prettify) { 4327 Parser.parseMarkup(msg); 4328 } 4329 if (window.jsMath) { 4330 Parser.parseMathOne(msg); 4331 } 4332 } 4333 else { 4334 abbr.textContent = "This post doesn't exist anymore."; 4335 } 4336 } 4337 else if (this.status == 404) { 4338 abbr.textContent = "This thread doesn't exist anymore."; 4339 } 4340 else { 4341 abbr.textContent = 'Connection Error'; 4342 console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); 4343 } 4344 }, 4345 onerror: function() { 4346 abbr.textContent = 'Connection Error'; 4347 console.log('ThreadExpansion: xhr failed'); 4348 } 4349 } 4350 ); 4351 }; 4352 4353 ThreadExpansion.toggle = function(tid) { 4354 var thread, msg, expmsg, summary, tmp; 4355 4356 thread = $.id('t' + tid); 4357 summary = thread.children[1]; 4358 if (thread.hasAttribute('data-truncated')) { 4359 msg = $.id('m' + tid); 4360 expmsg = msg.nextSibling; 4361 } 4362 4363 if ($.hasClass(thread, 'tExpanded')) { 4364 thread.className = thread.className.replace(' tExpanded', ' tCollapsed'); 4365 summary.children[0].src = Main.icons.plus; 4366 summary.children[1].style.display = 'inline'; 4367 summary.children[2].style.display = 'none'; 4368 if (msg) { 4369 tmp = msg.innerHTML; 4370 msg.innerHTML = expmsg.textContent; 4371 expmsg.textContent = tmp; 4372 } 4373 } 4374 else if ($.hasClass(thread, 'tCollapsed')) { 4375 thread.className = thread.className.replace(' tCollapsed', ' tExpanded'); 4376 summary.children[0].src = Main.icons.minus; 4377 summary.children[1].style.display = 'none'; 4378 summary.children[2].style.display = 'inline'; 4379 if (msg) { 4380 tmp = msg.innerHTML; 4381 msg.innerHTML = expmsg.textContent; 4382 expmsg.textContent = tmp; 4383 } 4384 } 4385 else { 4386 summary.children[0].src = Main.icons.rotate; 4387 ThreadExpansion.fetch(tid); 4388 } 4389 }; 4390 4391 ThreadExpansion.fetch = function(tid) { 4392 $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', 4393 { 4394 onload: function() { 4395 var i, p, n, frag, thread, tail, posts, count, msg, metacap, 4396 expmsg, summary, abbr; 4397 4398 thread = $.id('t' + tid); 4399 summary = thread.children[1]; 4400 4401 if (this.status == 200) { 4402 tail = $.cls('reply', thread); 4403 4404 posts = Parser.parseThreadJSON(this.responseText); 4405 4406 if (!Config.revealSpoilers && posts[0].custom_spoiler) { 4407 Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler); 4408 } 4409 4410 frag = document.createDocumentFragment(); 4411 4412 if (tail[0]) { 4413 tail = +tail[0].id.slice(1); 4414 4415 for (i = 1; p = posts[i]; ++i) { 4416 if (p.no < tail) { 4417 n = Parser.buildHTMLFromJSON(p, Main.board); 4418 n.className += ' rExpanded'; 4419 frag.appendChild(n); 4420 } 4421 else { 4422 break; 4423 } 4424 } 4425 } 4426 else { 4427 for (i = 1; p = posts[i]; ++i) { 4428 n = Parser.buildHTMLFromJSON(p, Main.board); 4429 n.className += ' rExpanded'; 4430 frag.appendChild(n); 4431 } 4432 } 4433 4434 msg = $.id('m' + tid); 4435 if ((abbr = $.cls('abbr', msg)[0]) 4436 && /^Comment/.test(abbr.textContent)) { 4437 thread.setAttribute('data-truncated', '1'); 4438 expmsg = document.createElement('div'); 4439 expmsg.style.display = 'none'; 4440 expmsg.textContent = msg.innerHTML; 4441 msg.parentNode.insertBefore(expmsg, msg.nextSibling); 4442 if (metacap = $.cls('capcodeReplies', msg)[0]) { 4443 msg.innerHTML = posts[0].com + '<br><br>'; 4444 msg.appendChild(metacap); 4445 } 4446 else { 4447 msg.innerHTML = posts[0].com; 4448 } 4449 if (Parser.prettify) { 4450 Parser.parseMarkup(msg); 4451 } 4452 if (window.jsMath) { 4453 Parser.parseMathOne(msg); 4454 } 4455 } 4456 4457 thread.insertBefore(frag, summary.nextSibling); 4458 Parser.parseThread(tid, 1, i - 1); 4459 4460 thread.className += ' tExpanded'; 4461 summary.children[0].src = Main.icons.minus; 4462 summary.children[1].style.display = 'none'; 4463 summary.children[2].style.display = 'inline'; 4464 } 4465 else if (this.status == 404) { 4466 summary.children[0].src = Main.icons.plus; 4467 summary.children[0].display = 'none'; 4468 summary.children[1].textContent = "This thread doesn't exist anymore."; 4469 } 4470 else { 4471 summary.children[0].src = Main.icons.plus; 4472 console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); 4473 } 4474 }, 4475 onerror: function() { 4476 $.id('t' + tid).children[1].children[0].src = Main.icons.plus; 4477 console.log('ThreadExpansion: xhr failed'); 4478 } 4479 } 4480 ); 4481 }; 4482 4483 /** 4484 * Thread updater 4485 */ 4486 var ThreadUpdater = {}; 4487 4488 ThreadUpdater.init = function() { 4489 if (!UA.hasCORS) { 4490 return; 4491 } 4492 4493 this.enabled = true; 4494 4495 this.pageTitle = document.title; 4496 4497 this.unreadCount = 0; 4498 this.auto = this.hadAuto = false; 4499 4500 this.delayId = 0; 4501 this.delayIdHidden = 4; 4502 this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ]; 4503 this.timeLeft = 0; 4504 this.interval = null; 4505 4506 this.lastModified = '0'; 4507 this.lastReply = null; 4508 4509 this.currentIcon = null; 4510 this.iconPath = '//s.4cdn.org/image/'; 4511 this.iconNode = document.head.querySelector('link[rel="shortcut icon"]'); 4512 this.iconNode.type = 'image/x-icon'; 4513 this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, ''); 4514 4515 this.deletionQueue = {}; 4516 4517 if (Config.updaterSound) { 4518 this.audioEnabled = false; 4519 this.audio = document.createElement('audio'); 4520 this.audio.src = '//s.4cdn.org/media/beep.ogg'; 4521 } 4522 4523 this.hidden = 'hidden'; 4524 this.visibilitychange = 'visibilitychange'; 4525 4526 this.adRefreshDelay = 1000; 4527 this.adDebounce = 0; 4528 this.ads = {}; 4529 4530 if (typeof document.hidden === 'undefined') { 4531 if ('mozHidden' in document) { 4532 this.hidden = 'mozHidden'; 4533 this.visibilitychange = 'mozvisibilitychange'; 4534 } 4535 else if ('webkitHidden' in document) { 4536 this.hidden = 'webkitHidden'; 4537 this.visibilitychange = 'webkitvisibilitychange'; 4538 } 4539 else if ('msHidden' in document) { 4540 this.hidden = 'msHidden'; 4541 this.visibilitychange = 'msvisibilitychange'; 4542 } 4543 } 4544 4545 this.initAds(); 4546 this.initControls(); 4547 4548 document.addEventListener('scroll', this.onScroll, false); 4549 4550 if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) { 4551 this.start(); 4552 } 4553 }; 4554 4555 ThreadUpdater.buildMobileControl = function(el, bottom) { 4556 var wrap, cnt, ctrl, cb, label, oldBtn, btn; 4557 4558 bottom = (bottom ? 'Bot' : ''); 4559 4560 wrap = document.createElement('div'); 4561 wrap.className = 'btn-row'; 4562 4563 // Update button 4564 oldBtn = el.parentNode; 4565 4566 btn = oldBtn.cloneNode(true); 4567 btn.textContent = 'Update'; 4568 btn.setAttribute('data-cmd', 'update'); 4569 4570 wrap.appendChild(btn); 4571 cnt = el.parentNode.parentNode; 4572 ctrl = document.createElement('span'); 4573 ctrl.className = 'mobileib button'; 4574 4575 // Auto checkbox 4576 label = document.createElement('label'); 4577 cb = document.createElement('input'); 4578 cb.type = 'checkbox'; 4579 cb.setAttribute('data-cmd', 'auto'); 4580 this['autoNode' + bottom] = cb; 4581 label.appendChild(cb); 4582 label.appendChild(document.createTextNode('Auto')); 4583 ctrl.appendChild(label); 4584 wrap.appendChild(document.createTextNode(' ')); 4585 wrap.appendChild(ctrl); 4586 4587 // Status label 4588 label = document.createElement('div'); 4589 label.className = 'mobile-tu-status'; 4590 4591 wrap.appendChild(this['statusNode' + bottom] = label); 4592 4593 cnt.appendChild(wrap); 4594 4595 // Remove Update button 4596 oldBtn.parentNode.removeChild(oldBtn); 4597 4598 $.id('mpostform').parentNode.style.marginTop = ''; 4599 }; 4600 4601 ThreadUpdater.buildDesktopControl = function(bottom) { 4602 var frag, el, label, navlinks; 4603 4604 bottom = (bottom ? 'Bot' : ''); 4605 4606 frag = document.createDocumentFragment(); 4607 4608 // Update button 4609 frag.appendChild(document.createTextNode(' [')); 4610 el = document.createElement('a'); 4611 el.href = ''; 4612 el.textContent = 'Update'; 4613 el.setAttribute('data-cmd', 'update'); 4614 frag.appendChild(el); 4615 frag.appendChild(document.createTextNode(']')); 4616 4617 // Auto checkbox 4618 frag.appendChild(document.createTextNode(' [')); 4619 label = document.createElement('label'); 4620 el = document.createElement('input'); 4621 el.type = 'checkbox'; 4622 el.title = 'Fetch new replies automatically'; 4623 el.setAttribute('data-cmd', 'auto'); 4624 this['autoNode' + bottom] = el; 4625 label.appendChild(el); 4626 label.appendChild(document.createTextNode('Auto')); 4627 frag.appendChild(label); 4628 frag.appendChild(document.createTextNode('] ')); 4629 4630 if (Config.updaterSound) { 4631 // Sound checkbox 4632 frag.appendChild(document.createTextNode(' [')); 4633 label = document.createElement('label'); 4634 el = document.createElement('input'); 4635 el.type = 'checkbox'; 4636 el.title = 'Play a sound on new replies to your posts'; 4637 el.setAttribute('data-cmd', 'sound'); 4638 this['soundNode' + bottom] = el; 4639 label.appendChild(el); 4640 label.appendChild(document.createTextNode('Sound')); 4641 frag.appendChild(label); 4642 frag.appendChild(document.createTextNode('] ')); 4643 } 4644 4645 // Status label 4646 frag.appendChild( 4647 this['statusNode' + bottom] = document.createElement('span') 4648 ); 4649 4650 if (bottom) { 4651 navlinks = $.cls('navLinks' + bottom)[0]; 4652 } 4653 else { 4654 navlinks = $.cls('navLinks')[1]; 4655 } 4656 4657 if (navlinks) { 4658 navlinks.appendChild(frag); 4659 } 4660 }; 4661 4662 ThreadUpdater.initControls = function() { 4663 var i, j, frag, el, label, navlinks; 4664 4665 // Mobile 4666 if (Main.hasMobileLayout) { 4667 this.buildMobileControl($.id('refresh_top')); 4668 this.buildMobileControl($.id('refresh_bottom'), true); 4669 } 4670 // Desktop 4671 else { 4672 this.buildDesktopControl(); 4673 this.buildDesktopControl(true); 4674 } 4675 }; 4676 4677 ThreadUpdater.start = function() { 4678 this.auto = this.hadAuto = true; 4679 this.autoNode.checked = this.autoNodeBot.checked = true; 4680 this.force = this.updating = false; 4681 this.lastUpdated = Date.now(); 4682 if (this.hidden) { 4683 document.addEventListener(this.visibilitychange, 4684 this.onVisibilityChange, false); 4685 } 4686 this.delayId = 0; 4687 this.timeLeft = this.delayRange[0]; 4688 this.pulse(); 4689 sessionStorage.setItem('4chan-auto-' + Main.tid, 1); 4690 }; 4691 4692 ThreadUpdater.stop = function(manual) { 4693 clearTimeout(this.interval); 4694 this.auto = this.updating = this.force = false; 4695 this.autoNode.checked = this.autoNodeBot.checked = false; 4696 if (this.hidden) { 4697 document.removeEventListener(this.visibilitychange, 4698 this.onVisibilityChange, false); 4699 } 4700 if (manual) { 4701 this.setStatus(''); 4702 this.setIcon(null); 4703 } 4704 sessionStorage.removeItem('4chan-auto-' + Main.tid); 4705 }; 4706 4707 ThreadUpdater.pulse = function() { 4708 var self = ThreadUpdater; 4709 4710 if (self.timeLeft == 0) { 4711 self.update(); 4712 } 4713 else { 4714 self.setStatus(self.timeLeft--); 4715 self.interval = setTimeout(self.pulse, 1000); 4716 } 4717 }; 4718 4719 ThreadUpdater.adjustDelay = function(postCount) 4720 { 4721 if (postCount == 0) { 4722 if (!this.force) { 4723 if (this.delayId < this.delayRange.length - 1) { 4724 ++this.delayId; 4725 } 4726 } 4727 } 4728 else { 4729 this.delayId = document[this.hidden] ? this.delayIdHidden : 0; 4730 } 4731 this.timeLeft = this.delayRange[this.delayId]; 4732 if (this.auto) { 4733 this.pulse(); 4734 } 4735 }; 4736 4737 ThreadUpdater.onVisibilityChange = function(e) { 4738 var self = ThreadUpdater; 4739 4740 if (document[self.hidden] && self.delayId < self.delayIdHidden) { 4741 self.delayId = self.delayIdHidden; 4742 } 4743 else { 4744 self.delayId = 0; 4745 self.refreshAds(); 4746 } 4747 4748 self.timeLeft = self.delayRange[0]; 4749 self.lastUpdated = Date.now(); 4750 clearTimeout(self.interval); 4751 self.pulse(); 4752 }; 4753 4754 ThreadUpdater.onScroll = function(e) { 4755 if (ThreadUpdater.hadAuto && 4756 (document.documentElement.scrollHeight 4757 <= (window.innerHeight + window.pageYOffset) 4758 && !document[ThreadUpdater.hidden])) { 4759 ThreadUpdater.clearUnread(); 4760 } 4761 4762 ThreadUpdater.refreshAds(); 4763 }; 4764 4765 ThreadUpdater.clearUnread = function() { 4766 if (!this.dead) { 4767 this.setIcon(null); 4768 } 4769 if (this.lastReply) { 4770 this.unreadCount = 0; 4771 document.title = this.pageTitle; 4772 $.removeClass(this.lastReply, 'newPostsMarker'); 4773 this.lastReply = null; 4774 } 4775 }; 4776 4777 ThreadUpdater.forceUpdate = function() { 4778 ThreadUpdater.force = true; 4779 ThreadUpdater.update(); 4780 }; 4781 4782 ThreadUpdater.toggleAuto = function() { 4783 if (this.updating) { 4784 return; 4785 } 4786 this.auto ? this.stop(true) : this.start(); 4787 }; 4788 4789 ThreadUpdater.toggleSound = function() { 4790 this.soundNode.checked = this.soundNodeBot.checked = 4791 this.audioEnabled = !this.audioEnabled; 4792 }; 4793 4794 ThreadUpdater.update = function() { 4795 var self, now = Date.now(); 4796 4797 self = ThreadUpdater; 4798 4799 if (self.updating) { 4800 return; 4801 } 4802 4803 clearTimeout(self.interval); 4804 4805 self.updating = true; 4806 4807 self.setStatus('Updating...'); 4808 4809 $.get('//a.4cdn.org/' + Main.board + '/thread/' + Main.tid + '.json', 4810 { 4811 onload: self.onload, 4812 onerror: self.onerror 4813 }, 4814 { 4815 'If-Modified-Since': self.lastModified 4816 } 4817 ); 4818 }; 4819 4820 ThreadUpdater.initAds = function() { 4821 var i, id, adIds = [ '_top_ad', '_middle_ad', '_bottom_ad' ]; 4822 4823 for (i = 0; id = adIds[i]; ++i) { 4824 ThreadUpdater.ads[id] = { 4825 time: 0, 4826 seenOnce: false, 4827 isStale: false 4828 }; 4829 } 4830 }; 4831 4832 ThreadUpdater.invalidateAds = function() { 4833 var id, self = ThreadUpdater; 4834 4835 for (id in self.ads) { 4836 meta = self.ads[id]; 4837 if (meta.seenOnce) { 4838 meta.isStale = true; 4839 } 4840 } 4841 }; 4842 4843 ThreadUpdater.refreshAds = function() { 4844 var self, now, el, id, ad, meta, hidden, docHeight, offset; 4845 4846 self = ThreadUpdater; 4847 4848 now = Date.now(); 4849 4850 if (now - self.adDebounce < 100) { 4851 return; 4852 } 4853 4854 self.adDebounce = now; 4855 4856 hidden = document[self.hidden]; 4857 docHeight = document.documentElement.clientHeight; 4858 4859 for (id in self.ads) { 4860 meta = self.ads[id]; 4861 4862 if (hidden) { 4863 continue; 4864 } 4865 4866 ad = window[id]; 4867 4868 if (!ad) { 4869 continue; 4870 } 4871 4872 el = $.id(ad.D); 4873 4874 if (!el) { 4875 continue; 4876 } 4877 4878 offset = el.getBoundingClientRect(); 4879 4880 if (offset.top < 0 || offset.bottom > docHeight) { 4881 continue; 4882 } 4883 4884 meta.seenOnce = true; 4885 4886 if (!meta.isStale || now - meta.time < self.adRefreshDelay) { 4887 continue; 4888 } 4889 4890 meta.time = now; 4891 meta.isStale = false; 4892 4893 ados_refresh(ad, 0, false); 4894 } 4895 }; 4896 4897 ThreadUpdater.markDeletedReplies = function(newposts) { 4898 var i, j, posthash, oldposts, el; 4899 4900 posthash = {}; 4901 for (i = 0; j = newposts[i]; ++i) { 4902 posthash['pc' + j.no] = 1; 4903 } 4904 4905 oldposts = $.cls('replyContainer'); 4906 for (i = 0; j = oldposts[i]; ++i) { 4907 if (!posthash[j.id] && !$.hasClass(j, 'deleted')) { 4908 if (this.deletionQueue[j.id]) { 4909 el = document.createElement('img'); 4910 el.src = Main.icons2.trash; 4911 el.className = 'trashIcon'; 4912 el.title = 'This post has been deleted'; 4913 $.addClass(j, 'deleted'); 4914 $.cls('postNum', j)[1].appendChild(el); 4915 delete this.deletionQueue[j.id]; 4916 } 4917 else { 4918 this.deletionQueue[j.id] = 1; 4919 } 4920 } 4921 } 4922 }; 4923 4924 ThreadUpdater.onload = function() { 4925 var i, el, state, self, nodes, thread, newposts, frag, lastrep, lastid, 4926 spoiler, op, doc, autoscroll, count, fromQR, lastRepPos; 4927 4928 self = ThreadUpdater; 4929 nodes = []; 4930 4931 self.setStatus(''); 4932 4933 if (this.status == 200) { 4934 self.lastModified = this.getResponseHeader('Last-Modified'); 4935 4936 thread = $.id('t' + Main.tid); 4937 4938 lastrep = thread.children[thread.childElementCount - 1]; 4939 lastid = +lastrep.id.slice(2); 4940 4941 newposts = Parser.parseThreadJSON(this.responseText); 4942 4943 state = !!newposts[0].archived; 4944 if (window.thread_archived !== undefined && state != window.thread_archived) { 4945 QR.enabled && $.id('quickReply') && QR.lock(); 4946 Main.setThreadState('archived', state); 4947 } 4948 4949 state = !!newposts[0].closed; 4950 if (state != Main.threadClosed) { 4951 if (newposts[0].archived) { 4952 state = false; 4953 } 4954 else if (QR.enabled && $.id('quickReply')) { 4955 if (state) { 4956 QR.lock(); 4957 } 4958 else { 4959 QR.unlock(); 4960 } 4961 } 4962 Main.setThreadState('closed', state); 4963 } 4964 4965 state = !!newposts[0].sticky; 4966 if (state != Main.threadSticky) { 4967 Main.setThreadState('sticky', state); 4968 } 4969 4970 state = !!newposts[0].imagelimit; 4971 if (QR.enabled && state != QR.fileDisabled) { 4972 QR.fileDisabled = state; 4973 } 4974 4975 if (!Config.revealSpoilers && newposts[0].custom_spoiler) { 4976 Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler); 4977 } 4978 4979 for (i = newposts.length - 1; i >= 0; i--) { 4980 if (newposts[i].no <= lastid) { 4981 break; 4982 } 4983 nodes.push(newposts[i]); 4984 } 4985 4986 count = nodes.length; 4987 4988 if (count == 1 && QR.lastReplyId == nodes[0].no) { 4989 fromQR = true; 4990 QR.lastReplyId = null; 4991 } 4992 4993 if (!fromQR) { 4994 self.markDeletedReplies(newposts); 4995 } 4996 4997 if (count) { 4998 doc = document.documentElement; 4999 5000 autoscroll = ( 5001 Config.autoScroll 5002 && document[self.hidden] 5003 && doc.scrollHeight == (window.innerHeight + window.pageYOffset) 5004 ); 5005 5006 frag = document.createDocumentFragment(); 5007 for (i = nodes.length - 1; i >= 0; i--) { 5008 frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board)); 5009 } 5010 thread.appendChild(frag); 5011 5012 lastRepPos = lastrep.offsetTop; 5013 5014 Parser.hasYouMarkers = false; 5015 Parser.hasHighlightedPosts = false; 5016 Parser.parseThread(thread.id.slice(1), -nodes.length); 5017 5018 if (lastRepPos != lastrep.offsetTop) { 5019 window.scrollBy(0, lastrep.offsetTop - lastRepPos); 5020 } 5021 5022 if (!fromQR) { 5023 if (!self.force && doc.scrollHeight > window.innerHeight) { 5024 if (!self.lastReply && lastid != Main.tid) { 5025 (self.lastReply = lastrep.lastChild).className += ' newPostsMarker'; 5026 } 5027 if (Parser.hasYouMarkers) { 5028 self.setIcon('rep'); 5029 if (self.audioEnabled && document[self.hidden]) { 5030 self.audio.play(); 5031 } 5032 } 5033 else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') { 5034 self.setIcon('hl'); 5035 } 5036 else if (self.unreadCount == 0) { 5037 self.setIcon('new'); 5038 } 5039 self.unreadCount += count; 5040 document.title = '(' + self.unreadCount + ') ' + self.pageTitle; 5041 } 5042 else { 5043 self.setStatus(count + ' new post' + (count > 1 ? 's' : '')); 5044 } 5045 } 5046 5047 if (autoscroll) { 5048 window.scrollTo(0, document.documentElement.scrollHeight); 5049 } 5050 5051 if (Config.threadWatcher) { 5052 ThreadWatcher.refreshCurrent(true); 5053 } 5054 5055 if (Config.threadStats) { 5056 op = newposts[0]; 5057 ThreadStats.update(op.replies, op.images, op.bumplimit, op.imagelimit); 5058 } 5059 5060 self.invalidateAds(); 5061 self.refreshAds(); 5062 5063 UA.dispatchEvent('4chanThreadUpdated', { count: count }); 5064 } 5065 else { 5066 self.setStatus('No new posts'); 5067 } 5068 5069 if (newposts[0].archived) { 5070 self.setError('This thread is archived'); 5071 if (!self.dead) { 5072 self.setIcon('dead'); 5073 window.thread_archived = true; 5074 self.dead = true; 5075 self.stop(); 5076 } 5077 } 5078 } 5079 else if (this.status == 304 || this.status == 0) { 5080 self.setStatus('No new posts'); 5081 } 5082 else if (this.status == 404) { 5083 self.setIcon('dead'); 5084 self.setError('This thread has been pruned or deleted'); 5085 self.dead = true; 5086 self.stop(); 5087 return; 5088 } 5089 5090 self.lastUpdated = Date.now(); 5091 self.adjustDelay(nodes.length); 5092 self.updating = self.force = false; 5093 }; 5094 5095 ThreadUpdater.onerror = function() { 5096 var self = ThreadUpdater; 5097 5098 if (UA.isOpera && !this.statusText && this.status == 0) { 5099 self.setStatus('No new posts'); 5100 } 5101 else { 5102 self.setError('Connection Error'); 5103 } 5104 5105 self.lastUpdated = Date.now(); 5106 self.adjustDelay(0); 5107 self.updating = self.force = false; 5108 }; 5109 5110 ThreadUpdater.setStatus = function(msg) { 5111 this.statusNode.textContent = this.statusNodeBot.textContent = msg; 5112 }; 5113 5114 ThreadUpdater.setError = function(msg) { 5115 this.statusNode.innerHTML 5116 = this.statusNodeBot.innerHTML 5117 = '<span class="tu-error">' + msg + '</span>'; 5118 }; 5119 5120 ThreadUpdater.setIcon = function(type) { 5121 var icon; 5122 5123 if (type === null) { 5124 icon = this.defaultIcon; 5125 } 5126 else { 5127 icon = this.icons[Main.type + type]; 5128 } 5129 5130 this.currentIcon = type; 5131 this.iconNode.href = this.iconPath + icon; 5132 document.head.appendChild(this.iconNode); 5133 }; 5134 5135 ThreadUpdater.icons = { 5136 wsnew: 'favicon-ws-newposts.ico', 5137 nwsnew: 'favicon-nws-newposts.ico', 5138 wsrep: 'favicon-ws-newreplies.ico', 5139 nwsrep: 'favicon-nws-newreplies.ico', 5140 wsdead: 'favicon-ws-deadthread.ico', 5141 nwsdead: 'favicon-nws-deadthread.ico', 5142 wshl: 'favicon-ws-newfilters.ico', 5143 nwshl: 'favicon-nws-newfilters.ico' 5144 }; 5145 5146 /** 5147 * Thread stats 5148 */ 5149 var ThreadStats = {}; 5150 5151 ThreadStats.init = function() { 5152 var i, cnt; 5153 5154 this.nodeTop = document.createElement('div'); 5155 this.nodeTop.className = 'thread-stats'; 5156 this.nodeBot = this.nodeTop.cloneNode(false); 5157 5158 cnt = $.cls('navLinks'); 5159 cnt[1] && cnt[1].appendChild(this.nodeTop); 5160 cnt[2] && cnt[2].appendChild(this.nodeBot); 5161 5162 this.pageNumber = null; 5163 this.update(null, null, window.bumplimit, window.imagelimit); 5164 5165 if (!window.thread_archived) { 5166 this.updatePageNumber(); 5167 this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000); 5168 } 5169 }; 5170 5171 ThreadStats.update = function(replies, images, isBumpFull, isImageFull) { 5172 var stats, repStr, imgStr, pageStr, stateStr; 5173 5174 if (replies === null) { 5175 replies = $.cls('replyContainer').length; 5176 images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0); 5177 } 5178 5179 stats = []; 5180 5181 if (Main.threadSticky) { 5182 stats.push('Sticky'); 5183 } 5184 5185 if (window.thread_archived) { 5186 stats.push('Archived'); 5187 } 5188 else if (Main.threadClosed) { 5189 stats.push('Closed'); 5190 } 5191 5192 if (isBumpFull) { 5193 stats.push('<em data-tip="Bump limit reached">' + replies + '</em>'); 5194 } 5195 else { 5196 stats.push('<span data-tip="Replies">' + replies + '</span>'); 5197 } 5198 5199 if (isImageFull) { 5200 stats.push('<em data-tip="Image limit reached">' + images + '</em>'); 5201 } 5202 else { 5203 stats.push('<span data-tip="Images">' + images + '</span>'); 5204 } 5205 5206 if (!window.thread_archived) { 5207 stats.push('<span data-tip="Page" class="ts-page">' + (this.pageNumber || '?') + '</span>'); 5208 } 5209 5210 this.nodeTop.innerHTML = this.nodeBot.innerHTML 5211 = stats.join(' / '); 5212 }; 5213 5214 ThreadStats.updatePageNumber = function() { 5215 $.get('//a.4cdn.org/' + Main.board + '/threads.json', 5216 { 5217 onload: ThreadStats.onCatalogLoad, 5218 onerror: ThreadStats.onCatalogError 5219 } 5220 ); 5221 }; 5222 5223 ThreadStats.onCatalogLoad = function() { 5224 var self, i, j, page, post, threads, catalog, tid, nodes; 5225 5226 self = ThreadStats; 5227 5228 if (this.status == 200) { 5229 tid = +Main.tid; 5230 catalog = JSON.parse(this.responseText); 5231 for (i = 0; page = catalog[i]; ++i) { 5232 threads = page.threads; 5233 for (j = 0; post = threads[j]; ++j) { 5234 if (post.no == tid) { 5235 nodes = $.cls('ts-page'); 5236 nodes[0].textContent = nodes[1].textContent = page.page 5237 self.pageNumber = page.page; 5238 return; 5239 } 5240 } 5241 } 5242 clearInterval(self.pageInterval); 5243 } 5244 else { 5245 ThreadStats.onCatalogError(); 5246 } 5247 }; 5248 5249 ThreadStats.onCatalogError = function() { 5250 console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')'); 5251 }; 5252 5253 /** 5254 * Filter 5255 */ 5256 var Filter = {}; 5257 5258 Filter.init = function() { 5259 this.entities = document.createElement('div'); 5260 Filter.load(); 5261 }; 5262 5263 Filter.onClick = function(e) { 5264 var cmd; 5265 5266 if (cmd = e.target.getAttribute('data-cmd')) { 5267 switch (cmd) { 5268 case 'filters-add': 5269 Filter.add(); 5270 break; 5271 case 'filters-save': 5272 Filter.save(); 5273 Filter.close(); 5274 break; 5275 case 'filters-close': 5276 Filter.close(); 5277 break; 5278 case 'filters-palette': 5279 Filter.openPalette(e.target); 5280 break; 5281 case 'filters-palette-close': 5282 Filter.closePalette(); 5283 break; 5284 case 'filters-palette-clear': 5285 Filter.clearPalette(); 5286 break; 5287 case 'filters-up': 5288 Filter.moveUp(e.target.parentNode.parentNode); 5289 break; 5290 case 'filters-del': 5291 Filter.remove(e.target.parentNode.parentNode); 5292 break; 5293 case 'filters-help-open': 5294 Filter.openHelp(); 5295 break; 5296 case 'filters-help-close': 5297 Filter.closeHelp(); 5298 break; 5299 } 5300 } 5301 }; 5302 5303 Filter.onPaletteClick = function(e) { 5304 var cmd; 5305 5306 if (cmd = e.target.getAttribute('data-cmd')) { 5307 switch (cmd) { 5308 case 'palette-pick': 5309 Filter.pickColor(e.target); 5310 break; 5311 case 'palette-clear': 5312 Filter.pickColor(e.target, true); 5313 break; 5314 case 'palette-close': 5315 Filter.closePalette(); 5316 break; 5317 } 5318 } 5319 }; 5320 5321 Filter.exec = function(cnt, pi, msg, tid) { 5322 var trip, name, com, uid, sub, fname, f, filters, hit, currentBoard; 5323 5324 if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) { 5325 return false; 5326 } 5327 5328 currentBoard = Main.board; 5329 filters = Filter.activeFilters; 5330 hit = false; 5331 5332 for (i = 0; f = filters[i]; ++i) { 5333 // boards 5334 if (f.boards && !f.boards[currentBoard]) { 5335 continue; 5336 } 5337 // tripcode 5338 if (f.type == 0) { 5339 if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0]) 5340 ) && f.pattern == trip.textContent) { 5341 hit = true; 5342 break; 5343 } 5344 } 5345 // name 5346 else if (f.type == 1) { 5347 if ((name || (name = pi.getElementsByClassName('name')[0])) 5348 && f.pattern == name.textContent) { 5349 hit = true; 5350 break; 5351 } 5352 } 5353 // comment 5354 else if (f.type == 2) { 5355 if (com === undefined) { 5356 this.entities.innerHTML 5357 = msg.innerHTML.replace(/<br>/g, '\n').replace(/[<[^>]+>/g, ''); 5358 com = this.entities.textContent; 5359 } 5360 if (f.pattern.test(com)) { 5361 hit = true; 5362 break; 5363 } 5364 } 5365 // user id 5366 else if (f.type == 4) { 5367 if ((uid || 5368 ((uid = pi.getElementsByClassName('posteruid')[0]) 5369 && (uid = uid.firstElementChild.textContent) 5370 ) 5371 ) && f.pattern == uid) { 5372 hit = true; 5373 break; 5374 } 5375 } 5376 // subject 5377 else if (!Main.tid && f.type == 5) { 5378 if ((sub || 5379 ((sub = pi.getElementsByClassName('subject')[0]) 5380 && (sub = sub.textContent) 5381 ) 5382 ) && f.pattern.test(sub)) { 5383 hit = true; 5384 break; 5385 } 5386 } 5387 // filename 5388 else if (f.type == 6) { 5389 if (fname === undefined) { 5390 if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) { 5391 fname = fname.firstElementChild.textContent; 5392 } 5393 else { 5394 fname = ''; 5395 } 5396 } 5397 if (f.pattern.test(fname)) { 5398 hit = true; 5399 break; 5400 } 5401 } 5402 } 5403 5404 if (hit) { 5405 if (f.hide) { 5406 cnt.className += ' post-hidden'; 5407 el = document.createElement('span'); 5408 if (!tid) { 5409 el.textContent = '[View]'; 5410 el.setAttribute('data-filtered', '1'); 5411 } 5412 else { 5413 el.innerHTML = '[<a data-filtered="1" href="thread/' + tid + '">View</a>]'; 5414 } 5415 el.className = 'filter-preview'; 5416 pi.appendChild(el); 5417 return true; 5418 } 5419 else { 5420 cnt.className += ' filter-hl'; 5421 cnt.style.boxShadow = '-3px 0 ' + f.color; 5422 Parser.hasHighlightedPosts = true; 5423 } 5424 } 5425 return false; 5426 }; 5427 5428 Filter.load = function() { 5429 var i, j, w, f, rawFilters, rawPattern, fid, regexEscape, regexType, 5430 wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard; 5431 5432 this.activeFilters = []; 5433 5434 if (!(rawFilters = localStorage.getItem('4chan-filters'))) { 5435 return; 5436 } 5437 5438 rawFilters = JSON.parse(rawFilters); 5439 5440 regexEscape = new RegExp('(\\' 5441 + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') 5442 + ')', 'g'); 5443 regexType = /^\/(.*)\/(i?)$/; 5444 wordSepS = '(?=.*\\b'; 5445 wordSepE = '\\b)'; 5446 regexWildcard = /\\\*/g; 5447 replaceWildcard = '[^\\s]*'; 5448 5449 try { 5450 for (fid = 0; f = rawFilters[fid]; ++fid) { 5451 if (f.active && f.pattern != '') { 5452 // Boards 5453 if (f.boards) { 5454 tmp = f.boards.split(/[^a-z0-9]+/i); 5455 boards = {}; 5456 for (i = 0; j = tmp[i]; ++i) { 5457 boards[j] = true; 5458 } 5459 } 5460 else { 5461 boards = false; 5462 } 5463 5464 rawPattern = f.pattern; 5465 // Name, Tripcode or ID, string comparison 5466 if (!f.type || f.type == 1 || f.type == 4) { 5467 pattern = rawPattern; 5468 } 5469 // /RegExp/ 5470 else if (match = rawPattern.match(regexType)) { 5471 pattern = new RegExp(match[1], match[2]); 5472 } 5473 // "Exact match" 5474 else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { 5475 pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); 5476 } 5477 // Full words, AND operator 5478 else { 5479 words = rawPattern.split(' '); 5480 pattern = ''; 5481 for (i = 0, j = words.length; i < j; ++i) { 5482 inner = words[i] 5483 .replace(regexEscape, '\\$1') 5484 .replace(regexWildcard, replaceWildcard); 5485 pattern += wordSepS + inner + wordSepE; 5486 } 5487 pattern = new RegExp('^' + pattern, 'im'); 5488 } 5489 //console.log('Resulting pattern: ' + pattern); 5490 this.activeFilters.push({ 5491 type: f.type, 5492 pattern: pattern, 5493 boards: boards, 5494 color: f.color, 5495 hide: f.hide 5496 }); 5497 } 5498 } 5499 } 5500 catch (e) { 5501 alert('There was an error processing one of the filters: ' 5502 + e + ' in: ' + rawPattern); 5503 } 5504 }; 5505 5506 Filter.addSelection = function() { 5507 var text, type, node, sel = UA.getSelection(true); 5508 5509 if (Filter.open() === false) { 5510 return; 5511 } 5512 5513 if (typeof sel == 'string') { 5514 text = sel.trim(); 5515 } 5516 else { 5517 node = sel.anchorNode.parentNode; 5518 text = sel.toString().trim(); 5519 5520 if ($.hasClass(node, 'name')) { 5521 type = 1; 5522 } 5523 else if ($.hasClass(node, 'postertrip')) { 5524 type = 0; 5525 } 5526 else if ($.hasClass(node, 'subject')) { 5527 type = 5; 5528 } 5529 else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) { 5530 type = 4; 5531 } 5532 else if ($.hasClass(node, 'fileText')) { 5533 type = 6; 5534 } 5535 else { 5536 type = 2; 5537 } 5538 } 5539 5540 Filter.add(text, type); 5541 }; 5542 5543 Filter.openHelp = function() { 5544 var cnt; 5545 5546 if ($.id('filtersHelp')) { 5547 return; 5548 } 5549 5550 cnt = document.createElement('div'); 5551 cnt.id = 'filtersHelp'; 5552 cnt.className = 'UIPanel'; 5553 cnt.setAttribute('data-cmd', 'filters-help-close'); 5554 cnt.innerHTML = '\ 5555 <div class="extPanel reply"><div class="panelHeader">Filters & Highlights Help\ 5556 <span><img alt="Close" title="Close" class="pointer" data-cmd="filters-help-close" src="' 5557 + Main.icons.cross + '"></span></div>\ 5558 <h4>Tripcode, Name and ID filters:</h4>\ 5559 <ul><li>Those use simple string comparison.</li>\ 5560 <li>Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.</li>\ 5561 <li>Example: <code>!Ep8pui8Vw2</code></li></ul>\ 5562 <h4>Comment, Subject and E-mail filters:</h4>\ 5563 <ul><li><strong>Matching whole words:</strong></li>\ 5564 <li><code>feel</code> — will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.</li></ul>\ 5565 <ul><li><strong>AND operator:</strong></li>\ 5566 <li><code>feel girlfriend</code> — will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.</li></ul>\ 5567 <ul><li><strong>Exact match:</strong></li>\ 5568 <li><code>"that feel when"</code> — place double quotes around the pattern to search for an exact string</li></ul>\ 5569 <ul><li><strong>Wildcards:</strong></li>\ 5570 <li><code>feel*</code> — matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etcโฆ</li>\ 5571 <li><code>idolm*ster</code> — this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etcโฆ</li></ul>\ 5572 <ul><li><strong>Regular expressions:</strong></li>\ 5573 <li><code>/feel when no (girl|boy)friend/i</code></li>\ 5574 <li><code>/^(?!.*touhou).*$/i</code> — NOT operator.</li>\ 5575 <li><code>/^>/</code> — comments starting with a quote.</li>\ 5576 <li><code>/^$/</code> — comments with no text.</li></ul>\ 5577 <h4>Colors:</h4>\ 5578 <ul><li>The color field can accept any valid CSS color:</li>\ 5579 <li><code>red</code>, <code>#0f0</code>, <code>#00ff00</code>, <code>rgba( 34, 12, 64, 0.3)</code>, etcโฆ</li></ul>\ 5580 <h4>Boards:</h4>\ 5581 <ul><li>A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.</li></ul>\ 5582 <h4>Shortcut:</h4>\ 5583 <ul><li>If you have <code>Keyboard shortcuts</code> enabled, pressing <kbd>F</kbd> will add the selected text to your filters.</li></ul>'; 5584 5585 document.body.appendChild(cnt); 5586 cnt.addEventListener('click', this.onClick, false); 5587 }; 5588 5589 Filter.closeHelp = function() { 5590 var cnt; 5591 5592 if (cnt = $.id('filtersHelp')) { 5593 cnt.removeEventListener('click', this.onClick, false); 5594 document.body.removeChild(cnt); 5595 } 5596 }; 5597 5598 Filter.open = function() { 5599 var i, f, cnt, menu, html, rawFilters, filterId, filterList; 5600 5601 if ($.id('filtersMenu')) { 5602 return false; 5603 } 5604 5605 cnt = document.createElement('div'); 5606 cnt.id = 'filtersMenu'; 5607 cnt.className = 'UIPanel'; 5608 cnt.style.display = 'none'; 5609 cnt.setAttribute('data-cmd', 'filters-close'); 5610 cnt.innerHTML = '\ 5611 <div class="extPanel reply"><div class="panelHeader">Filters & Highlights\ 5612 <span><img alt="Help" class="pointer" title="Help" data-cmd="filters-help-open" src="' 5613 + Main.icons.help 5614 + '"><img alt="Close" title="Close" class="pointer" data-cmd="filters-close" src="' 5615 + Main.icons.cross + '"></span></div>\ 5616 <table><thead><tr>\ 5617 <th>Order</th>\ 5618 <th>On</th>\ 5619 <th>Pattern</th>\ 5620 <th>Boards</th>\ 5621 <th>Type</th>\ 5622 <th>Color</th>\ 5623 <th>Hide</th>\ 5624 <th>Del</th>\ 5625 </tr></thead><tbody id="filter-list"></tbody><tfoot><tr><td colspan="8">\ 5626 <button data-cmd="filters-add">Add</button>\ 5627 <button class="right" data-cmd="filters-save">Save</button>\ 5628 </td></tr></tfoot></table></div>'; 5629 5630 document.body.appendChild(cnt); 5631 cnt.addEventListener('click', this.onClick, false); 5632 5633 filterList = $.id('filter-list'); 5634 5635 if (rawFilters = localStorage.getItem('4chan-filters')) { 5636 rawFilters = JSON.parse(rawFilters); 5637 for (i = 0; f = rawFilters[i]; ++i) { 5638 filterList.appendChild(this.buildEntry(f, i)); 5639 } 5640 } 5641 5642 cnt.style.display = ''; 5643 }; 5644 5645 Filter.close = function() { 5646 var cnt; 5647 5648 if (cnt = $.id('filtersMenu')) { 5649 this.closePalette(); 5650 cnt.removeEventListener('click', this.onClick, false); 5651 document.body.removeChild(cnt); 5652 } 5653 }; 5654 5655 Filter.moveUp = function(el) { 5656 var prev; 5657 5658 if (prev = el.previousElementSibling) { 5659 el.parentNode.insertBefore(el, prev); 5660 } 5661 }; 5662 5663 Filter.add = function(pattern, type, boards) { 5664 var filter, id, el; 5665 5666 filter = { 5667 active: true, 5668 type: type || 0, 5669 pattern: pattern || '', 5670 boards: boards || '', 5671 color: '', 5672 hide: false 5673 }; 5674 5675 id = this.getNextFilterId(); 5676 el = this.buildEntry(filter, id); 5677 5678 $.id('filter-list').appendChild(el); 5679 $.cls('fPattern', el)[0].focus(); 5680 }; 5681 5682 Filter.remove = function(tr) { 5683 $.id('filter-list').removeChild(tr); 5684 }; 5685 5686 Filter.save = function() { 5687 var i, rawFilters, entries, tr, f, color, type; 5688 5689 rawFilters = []; 5690 entries = $.id('filter-list').children; 5691 5692 for (i = 0; tr = entries[i]; ++i) { 5693 type = tr.children[4].firstChild; 5694 f = { 5695 active: tr.children[1].firstChild.checked, 5696 pattern: tr.children[2].firstChild.value, 5697 boards: tr.children[3].firstChild.value, 5698 type: +type.options[type.selectedIndex].value, 5699 hide: tr.children[6].firstChild.checked 5700 } 5701 5702 color = tr.children[5].firstChild; 5703 5704 if (!color.hasAttribute('data-nocolor')) { 5705 f.color = color.style.backgroundColor; 5706 } 5707 5708 rawFilters.push(f); 5709 } 5710 5711 if (rawFilters[0]) { 5712 localStorage.setItem('4chan-filters', JSON.stringify(rawFilters)); 5713 } 5714 else { 5715 localStorage.removeItem('4chan-filters'); 5716 } 5717 }; 5718 5719 Filter.getNextFilterId = function() { 5720 var i, j, max, entries = $.id('filter-list').children; 5721 5722 if (!entries.length) { 5723 return 0; 5724 } 5725 else { 5726 max = 0; 5727 for (i = 0; j = entries[i]; ++i) { 5728 j = +j.id.slice(7); 5729 if (j > max) { 5730 max = j; 5731 } 5732 } 5733 return max + 1; 5734 } 5735 }; 5736 5737 Filter.buildEntry = function(filter, id) { 5738 var tr, html, sel; 5739 5740 tr = document.createElement('tr'); 5741 tr.id = 'filter-' + id; 5742 5743 html = ''; 5744 5745 // Move up 5746 html += '<td><span data-cmd="filters-up" class="pointer">↑</span></td>'; 5747 5748 // On 5749 html += '<td><input type="checkbox"' 5750 + (filter.active ? ' checked="checked"></td>' : '></td>'); 5751 5752 // Pattern 5753 html += '<td><input class="fPattern" type="text" value="' 5754 + filter.pattern.replace(/"/g, '"') + '"></td>'; 5755 5756 // Boards 5757 html += '<td><input class="fBoards" type="text" value="' 5758 + (filter.boards !== undefined ? filter.boards : '') + '"></td>'; 5759 5760 // FIXME 5761 if (filter.type === 3) { 5762 filter.type = 4; 5763 } 5764 5765 // Type 5766 sel = [ '', '', '', '', '', '', '' ]; 5767 sel[filter.type] = ' selected="selected"'; 5768 5769 html += '<td><select size="1"><option value="0"' 5770 + sel[0] + '>Tripcode</option><option value="1"' 5771 + sel[1] + '>Name</option><option value="2"' 5772 + sel[2] + '>Comment</option><option value="4"' 5773 + sel[4] + '>ID</option><option value="5"' 5774 + sel[5] + '>Subject</option><option value="6"' 5775 + sel[6] + '>Filename</option></select></td>'; 5776 5777 // Color 5778 html += '<td><span data-cmd="filters-palette" title="Change Color" class="colorbox fColor" '; 5779 5780 if (!filter.color) { 5781 html += ' data-nocolor="1">∕'; 5782 } 5783 else { 5784 html += ' style="background-color:' + filter.color + '">'; 5785 } 5786 html += '</span></td>'; 5787 5788 // Hide 5789 html += '<td><input type="checkbox"' 5790 + (filter.hide ? ' checked="checked"></td>' : '></td>'); 5791 5792 // Del 5793 html += '<td><span data-cmd="filters-del" class="pointer fDel">×</span></td>'; 5794 5795 tr.innerHTML = html; 5796 5797 return tr; 5798 } 5799 5800 Filter.buildPalette = function(id) { 5801 var i, j, cnt, html, colors, rowCount, colCount; 5802 5803 colors = [ 5804 ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], 5805 ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], 5806 ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] 5807 ]; 5808 5809 rowCount = colors.length; 5810 colCount = colors[0].length; 5811 5812 html = '<div id="colorpicker" class="reply extPanel"><table><tbody>'; 5813 5814 for (i = 0; i < rowCount; ++i) { 5815 html += '<tr>' 5816 for (j = 0; j < colCount; ++j) { 5817 html += '<td><div data-cmd="palette-pick" class="colorbox" style="background:' 5818 + colors[i][j] + '"></div></td>'; 5819 } 5820 html += '</tr>' 5821 } 5822 5823 html += '</tbody></table>Custom\ 5824 <div id="palette-custom"><input id="palette-custom-input" type="text">\ 5825 <div id="palette-custom-ok" data-cmd="palette-pick" title="Select Color" class="colorbox"></div></div>\ 5826 [<a href="javascript:;" data-cmd="palette-close">Close</a>]\ 5827 [<a href="javascript:;" data-cmd="palette-clear">Clear</a>]</div>'; 5828 5829 cnt = document.createElement('div'); 5830 cnt.id = 'filter-palette'; 5831 cnt.setAttribute('data-target', id); 5832 cnt.setAttribute('data-cmd', 'palette-close'); 5833 cnt.className = 'UIMenu'; 5834 cnt.innerHTML = html; 5835 5836 return cnt; 5837 }; 5838 5839 Filter.openPalette = function(target) { 5840 var el, pos, id, picker; 5841 5842 Filter.closePalette(); 5843 5844 pos = target.getBoundingClientRect(); 5845 id = target.parentNode.parentNode.id.slice(7); 5846 5847 el = Filter.buildPalette(id); 5848 document.body.appendChild(el); 5849 5850 $.id('filter-palette').addEventListener('click', Filter.onPaletteClick, false); 5851 $.id('palette-custom-input').addEventListener('keyup', Filter.setCustomColor, false); 5852 5853 picker = el.firstElementChild; 5854 picker.style.cssText = 'top:' + pos.top + 'px;left:' 5855 + (pos.left - picker.clientWidth - 10) + 'px;'; 5856 }; 5857 5858 Filter.closePalette = function() { 5859 var el; 5860 5861 if (el = $.id('filter-palette')) { 5862 $.id('filter-palette').removeEventListener('click', Filter.onPaletteClick, false); 5863 $.id('palette-custom-input').removeEventListener('keyup', Filter.setCustomColor, false); 5864 el.parentNode.removeChild(el); 5865 } 5866 }; 5867 5868 Filter.pickColor = function(el, clear) { 5869 var id, target; 5870 5871 id = $.id('filter-palette').getAttribute('data-target'); 5872 target = $.id('filter-' + id); 5873 5874 if (!target) { 5875 return; 5876 } 5877 5878 target = $.cls('colorbox', target)[0]; 5879 5880 if (clear === true) { 5881 target.setAttribute('data-nocolor', '1'); 5882 target.innerHTML = '∕'; 5883 target.style.background = ''; 5884 } 5885 else { 5886 target.removeAttribute('data-nocolor'); 5887 target.innerHTML = ''; 5888 target.style.background = el.style.backgroundColor; 5889 } 5890 5891 Filter.closePalette(); 5892 }; 5893 5894 Filter.setCustomColor = function() { 5895 var input, box; 5896 5897 input = $.id('palette-custom-input'); 5898 box = $.id('palette-custom-ok'); 5899 5900 box.style.backgroundColor = input.value; 5901 }; 5902 5903 /** 5904 * ID colors 5905 */ 5906 var IDColor = { 5907 css: 'padding: 0 5px; border-radius: 6px; font-size: 0.8em;', 5908 ids: {} 5909 }; 5910 5911 IDColor.init = function() { 5912 var style; 5913 5914 if (window.user_ids) { 5915 this.enabled = true; 5916 5917 style = document.createElement('style'); 5918 style.setAttribute('type', 'text/css'); 5919 style.textContent = '.posteruid .hand {' + this.css + '}'; 5920 document.head.appendChild(style); 5921 } 5922 }; 5923 5924 IDColor.compute = function(str) { 5925 var rgb, hash; 5926 5927 rgb = []; 5928 hash = $.hash(str); 5929 5930 rgb[0] = (hash >> 24) & 0xFF; 5931 rgb[1] = (hash >> 16) & 0xFF; 5932 rgb[2] = (hash >> 8) & 0xFF; 5933 rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125; 5934 5935 this.ids[str] = rgb; 5936 5937 return rgb; 5938 }; 5939 5940 IDColor.apply = function(uid) { 5941 var rgb; 5942 5943 rgb = IDColor.ids[uid.textContent] || IDColor.compute(uid.textContent); 5944 uid.style.cssText = '\ 5945 background-color: rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');\ 5946 color: ' + (rgb[3] ? 'black;' : 'white;'); 5947 }; 5948 5949 IDColor.applyRemote = function(uid) { 5950 this.apply(uid); 5951 uid.style.cssText += this.css; 5952 }; 5953 5954 /** 5955 * SWF embed 5956 */ 5957 var SWFEmbed = {}; 5958 5959 SWFEmbed.init = function() { 5960 if (Main.tid) { 5961 this.processThread(); 5962 } 5963 else { 5964 this.processIndex(); 5965 } 5966 }; 5967 5968 SWFEmbed.processThread = function() { 5969 var fileText, el; 5970 5971 fileText = $.id('fT' + Main.tid); 5972 5973 if (!fileText) { 5974 return; 5975 } 5976 5977 el = document.createElement('a'); 5978 el.href = 'javascript:;'; 5979 el.textContent = 'Embed'; 5980 el.addEventListener('click', SWFEmbed.toggleThread, false); 5981 5982 fileText.appendChild(document.createTextNode('-[')); 5983 fileText.appendChild(el); 5984 fileText.appendChild(document.createTextNode(']')); 5985 }; 5986 5987 SWFEmbed.processIndex = function() { 5988 var i, tr, el, cnt, nodes, srcIndex, src; 5989 5990 srcIndex = 2; 5991 5992 cnt = $.cls('postblock')[0]; 5993 5994 if (!cnt) { 5995 return; 5996 } 5997 5998 tr = cnt.parentNode; 5999 6000 el = document.createElement('td'); 6001 el.className = 'postblock'; 6002 tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); 6003 6004 cnt = $.cls('flashListing')[0]; 6005 6006 if (!cnt) { 6007 return; 6008 } 6009 6010 nodes = $.tag('tr', cnt); 6011 6012 for (i = 1; tr = nodes[i]; ++i) { 6013 src = tr.children[srcIndex].firstElementChild; 6014 el = document.createElement('td'); 6015 el.innerHTML = '[<a href="' + src.href + '">Embed</a>]'; 6016 el.firstElementChild.addEventListener('click', SWFEmbed.embedIndex, false); 6017 tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); 6018 }; 6019 }; 6020 6021 SWFEmbed.toggleThread = function(e) { 6022 var cnt, link, el, post, maxWidth, ratio, width, height; 6023 6024 if (cnt = $.id('swf-embed')) { 6025 cnt.parentNode.removeChild(cnt); 6026 e.target.textContent = 'Embed'; 6027 return; 6028 } 6029 6030 link = $.tag('a', e.target.parentNode)[0]; 6031 6032 maxWidth = document.documentElement.clientWidth - 100; 6033 6034 width = +link.getAttribute('data-width'); 6035 height = +link.getAttribute('data-height'); 6036 6037 if (width > maxWidth) { 6038 ratio = width / height; 6039 width = maxWidth; 6040 height = Math.round(maxWidth / ratio); 6041 } 6042 6043 cnt = document.createElement('div'); 6044 cnt.id = 'swf-embed'; 6045 6046 el = document.createElement('embed'); 6047 el.setAttribute('allowScriptAccess', 'never'); 6048 el.type = 'application/x-shockwave-flash'; 6049 el.width = width; 6050 el.height = height; 6051 el.src = link.href; 6052 6053 cnt.appendChild(el); 6054 6055 post = $.id('m' + Main.tid); 6056 post.insertBefore(cnt, post.firstChild); 6057 6058 $.cls('thread')[0].scrollIntoView(true); 6059 6060 e.target.textContent = 'Remove'; 6061 }; 6062 6063 SWFEmbed.embedIndex = function(e) { 6064 var el, cnt, header, icon, backdrop, width, height, cntWidth, cntHeight, 6065 maxWidth, maxHeight, docWidth, docHeight, margins, headerHeight, fileName; 6066 6067 e.preventDefault(); 6068 6069 margins = 10; 6070 headerHeight = 20; 6071 6072 el = e.target.parentNode.parentNode.children[2].firstElementChild; 6073 6074 fileName = el.getAttribute('title') || el.textContent; 6075 6076 cntWidth = width = +el.getAttribute('data-width'); 6077 cntHeight = height = +el.getAttribute('data-height'); 6078 6079 docWidth = document.documentElement.clientWidth; 6080 docHeight = document.documentElement.clientHeight; 6081 6082 maxWidth = docWidth - margins; 6083 maxHeight = docHeight - margins - headerHeight; 6084 6085 ratio = width / height; 6086 6087 if (cntWidth > maxWidth) { 6088 cntWidth = maxWidth; 6089 cntHeight = Math.round(maxWidth / ratio); 6090 } 6091 6092 if (cntHeight > maxHeight) { 6093 cntHeight = maxHeight; 6094 cntWidth = Math.round(maxHeight * ratio); 6095 } 6096 6097 el = document.createElement('embed'); 6098 el.setAttribute('allowScriptAccess', 'never'); 6099 el.src = e.target.href; 6100 el.width = '100%'; 6101 el.height = '100%'; 6102 6103 cnt = document.createElement('div'); 6104 cnt.style.position = 'fixed'; 6105 cnt.style.width = cntWidth + 'px'; 6106 cnt.style.height = cntHeight + 'px'; 6107 cnt.style.top = '50%'; 6108 cnt.style.left = '50%'; 6109 cnt.style.marginTop = (-cntHeight / 2 - headerHeight / 2) + 'px'; 6110 cnt.style.marginLeft = (-cntWidth / 2) + 'px'; 6111 cnt.style.background = 'white'; 6112 6113 header = document.createElement('div'); 6114 header.id = 'swf-embed-header'; 6115 header.className = 'postblock'; 6116 header.textContent = fileName + ', ' + width + 'x' + height; 6117 6118 icon = document.createElement('img'); 6119 icon.id = 'swf-embed-close'; 6120 icon.className = 'pointer'; 6121 icon.src = Main.icons.cross; 6122 6123 header.appendChild(icon); 6124 6125 cnt.appendChild(header); 6126 cnt.appendChild(el); 6127 6128 backdrop = document.createElement('div'); 6129 backdrop.id = 'swf-embed'; 6130 backdrop.style.cssText = 'width: 100%; height: 100%; position: fixed;\ 6131 top: 0; left: 0; background: rgba(128, 128, 128, 0.5)'; 6132 6133 backdrop.appendChild(cnt); 6134 backdrop.addEventListener('click', SWFEmbed.onBackdropClick, false); 6135 6136 document.body.appendChild(backdrop); 6137 }; 6138 6139 SWFEmbed.onBackdropClick = function(e) { 6140 var backdrop = $.id('swf-embed'); 6141 6142 if (e.target === backdrop || e.target.id == 'swf-embed-close') { 6143 backdrop.removeEventListener('click', SWFEmbed.onBackdropClick, false); 6144 backdrop.parentNode.removeChild(backdrop); 6145 } 6146 }; 6147 6148 /** 6149 * Media 6150 */ 6151 var Media = {}; 6152 6153 Media.init = function() { 6154 this.matchSC = /(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:<wbr>)?[^\s<]*/g; 6155 this.matchYT = /(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:<wbr>)?[^\s<]*(?:<wbr>)?[^\s<]*/g; 6156 this.toggleYT = /(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/; 6157 this.timeYT = /#t=([ms0-9]+)/; 6158 this.matchVocaroo = /vocaroo\.com\/i\/([a-z0-9]{12})/gi; 6159 6160 this.map = { 6161 yt: this.toggleYouTube, 6162 sc: this.toggleSoundCloud, 6163 vocaroo: this.toggleVocaroo 6164 }; 6165 }; 6166 6167 Media.parseSoundCloud = function(msg) { 6168 msg.innerHTML = msg.innerHTML.replace(this.matchSC, this.replaceSoundCloud); 6169 }; 6170 6171 Media.replaceSoundCloud = function(link) { 6172 return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="sc">Embed</a>]'; 6173 }; 6174 6175 Media.toggleSoundCloud = function(node) { 6176 var xhr, url; 6177 6178 if (node.textContent == 'Remove') { 6179 node.parentNode.removeChild(node.nextElementSibling); 6180 node.textContent = 'Embed'; 6181 } 6182 else if (node.textContent == 'Embed') { 6183 url = node.previousElementSibling.textContent; 6184 6185 xhr = new XMLHttpRequest(); 6186 xhr.open('GET', '//soundcloud.com/oembed?show_artwork=false&' 6187 + 'maxwidth=500px&show_comments=false&format=json&url=' 6188 + 'http://' + url); 6189 xhr.onload = function() { 6190 var el; 6191 6192 if (this.status == 200 || this.status == 304) { 6193 el = document.createElement('div'); 6194 el.className = 'media-embed'; 6195 el.innerHTML = JSON.parse(this.responseText).html; 6196 node.parentNode.insertBefore(el, node.nextElementSibling); 6197 node.textContent = 'Remove'; 6198 } 6199 else { 6200 node.textContent = 'Error'; 6201 console.log('SoundCloud Error (HTTP ' + this.status + ')'); 6202 } 6203 }; 6204 node.textContent = 'Loading...'; 6205 xhr.send(null); 6206 } 6207 }; 6208 6209 Media.parseYouTube = function(msg) { 6210 msg.innerHTML = msg.innerHTML.replace(this.matchYT, this.replaceYouTube); 6211 }; 6212 6213 Media.replaceYouTube = function(link) { 6214 return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="yt">Embed</a>]'; 6215 }; 6216 6217 Media.showYTPreview = function(link) { 6218 var cnt, img, vid, aabb, x, y, tw, th, pad; 6219 6220 tw = 320; th = 180; pad = 5; 6221 6222 aabb = link.getBoundingClientRect(); 6223 6224 vid = link.previousElementSibling.textContent.match(this.toggleYT)[1]; 6225 6226 if (aabb.right + tw + pad > $.docEl.clientWidth) { 6227 x = aabb.left - tw - pad; 6228 } 6229 else { 6230 x = aabb.right + pad; 6231 } 6232 6233 y = aabb.top - th / 2 + aabb.height / 2; 6234 6235 img = document.createElement('img'); 6236 img.width = tw; 6237 img.height = th; 6238 img.alt = ''; 6239 img.src = '//i1.ytimg.com/vi/' + encodeURIComponent(vid) + '/mqdefault.jpg'; 6240 6241 cnt = document.createElement('div'); 6242 cnt.id = 'yt-preview'; 6243 cnt.className = 'reply'; 6244 cnt.style.left = (x + window.pageXOffset) + 'px'; 6245 cnt.style.top = (y + window.pageYOffset) + 'px'; 6246 6247 cnt.appendChild(img); 6248 6249 document.body.appendChild(cnt); 6250 }; 6251 6252 Media.removeYTPreview = function() { 6253 var el; 6254 6255 if (el = $.id('yt-preview')) { 6256 document.body.removeChild(el); 6257 } 6258 } 6259 6260 Media.toggleYouTube = function(node) { 6261 var vid, time, el, url; 6262 6263 if (node.textContent == 'Remove') { 6264 node.parentNode.removeChild(node.nextElementSibling); 6265 node.textContent = 'Embed'; 6266 } 6267 else { 6268 url = node.previousElementSibling.textContent; 6269 vid = url.match(this.toggleYT); 6270 time = url.match(this.timeYT); 6271 6272 if (vid && (vid = vid[1])) { 6273 vid = encodeURIComponent(vid); 6274 6275 if (time && (time = time[1])) { 6276 vid += '#t=' + encodeURIComponent(time); 6277 } 6278 6279 el = document.createElement('div'); 6280 el.className = 'media-embed'; 6281 el.innerHTML = '<iframe src="//www.youtube.com/embed/' 6282 + vid 6283 + '" width="640" height="360" frameborder="0"></iframe>' 6284 6285 node.parentNode.insertBefore(el, node.nextElementSibling); 6286 6287 node.textContent = 'Remove'; 6288 } 6289 else { 6290 node.textContent = 'Error'; 6291 } 6292 } 6293 }; 6294 6295 Media.parseVocaroo = function(msg) { 6296 msg.innerHTML = msg.innerHTML.replace(this.matchVocaroo, this.replaceVocaroo); 6297 }; 6298 6299 Media.replaceVocaroo = function(link) { 6300 return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="vocaroo">Embed</a>]'; 6301 }; 6302 6303 Media.toggleVocaroo = function(node) { 6304 var vid, time, el, url; 6305 6306 if (node.textContent == 'Remove') { 6307 node.parentNode.removeChild(node.nextElementSibling); 6308 node.textContent = 'Embed'; 6309 } 6310 else { 6311 url = node.previousElementSibling.textContent; 6312 vid = url.match(Media.matchVocaroo); 6313 6314 if (vid && (vid = vid[0].split('/').pop())) { 6315 vid = encodeURIComponent(vid); 6316 6317 el = document.createElement('div'); 6318 el.className = 'media-embed'; 6319 el.innerHTML = '<embed width="220" height="140" class="media-embed" ' 6320 + 'src="//vocaroo.com/mediafoo.swf?playMediaID=' + vid + '&autoplay=0">'; 6321 6322 node.parentNode.insertBefore(el, node.nextElementSibling); 6323 6324 node.textContent = 'Remove'; 6325 } 6326 else { 6327 node.textContent = 'Error'; 6328 } 6329 } 6330 }; 6331 6332 Media.toggleEmbed = function(node) { 6333 var fn, type = node.getAttribute('data-type'); 6334 6335 if (type && (fn = Media.map[type])) { 6336 fn.call(this, node); 6337 } 6338 }; 6339 6340 /** 6341 * Custom CSS 6342 */ 6343 var CustomCSS = {}; 6344 6345 CustomCSS.init = function() { 6346 var style, css; 6347 if (css = localStorage.getItem('4chan-css')) { 6348 style = document.createElement('style'); 6349 style.id = 'customCSS'; 6350 style.setAttribute('type', 'text/css'); 6351 style.textContent = css; 6352 document.head.appendChild(style); 6353 } 6354 }; 6355 6356 CustomCSS.open = function() { 6357 var cnt, ta, data; 6358 6359 if ($.id('customCSSMenu')) { 6360 return; 6361 } 6362 6363 cnt = document.createElement('div'); 6364 cnt.id = 'customCSSMenu'; 6365 cnt.className = 'UIPanel'; 6366 cnt.setAttribute('data-cmd', 'css-close'); 6367 cnt.innerHTML = '\ 6368 <div class="extPanel reply"><div class="panelHeader">Custom CSS\ 6369 <span><img alt="Close" title="Close" class="pointer" data-cmd="css-close" src="' 6370 + Main.icons.cross + '"></span></div>\ 6371 <textarea id="customCSSBox"></textarea>\ 6372 <div class="center"><button data-cmd="css-save">Save CSS</button></div>\ 6373 </td></tr></tfoot></table></div>'; 6374 6375 document.body.appendChild(cnt); 6376 6377 cnt.addEventListener('click', this.onClick, false); 6378 6379 ta = $.id('customCSSBox'); 6380 6381 if (data = localStorage.getItem('4chan-css')) { 6382 ta.textContent = data; 6383 } 6384 6385 ta.focus(); 6386 }; 6387 6388 CustomCSS.save = function() { 6389 var ta, style; 6390 6391 if (ta = $.id('customCSSBox')) { 6392 localStorage.setItem('4chan-css', ta.value); 6393 if (Config.customCSS && (style = $.id('customCSS'))) { 6394 document.head.removeChild(style); 6395 CustomCSS.init(); 6396 } 6397 } 6398 }; 6399 6400 CustomCSS.close = function() { 6401 var cnt; 6402 6403 if (cnt = $.id('customCSSMenu')) { 6404 cnt.removeEventListener('click', this.onClick, false); 6405 document.body.removeChild(cnt); 6406 } 6407 }; 6408 6409 CustomCSS.onClick = function(e) { 6410 var cmd; 6411 6412 if (cmd = e.target.getAttribute('data-cmd')) { 6413 switch (cmd) { 6414 case 'css-close': 6415 CustomCSS.close(); 6416 break; 6417 case 'css-save': 6418 CustomCSS.save(); 6419 CustomCSS.close(); 6420 break; 6421 } 6422 } 6423 }; 6424 6425 /** 6426 * Keyboard shortcuts 6427 */ 6428 var Keybinds = {}; 6429 6430 Keybinds.init = function() { 6431 this.map = { 6432 // A 6433 65: function() { 6434 if (ThreadUpdater.enabled) ThreadUpdater.toggleAuto(); 6435 }, 6436 // F 6437 70: function() { 6438 if (Config.filter) { 6439 Filter.addSelection(); 6440 } 6441 }, 6442 // Q 6443 81: function() { 6444 if (QR.enabled && Main.tid) { 6445 QR.quotePost(Main.tid); 6446 } 6447 }, 6448 // R 6449 82: function() { 6450 if (ThreadUpdater.enabled) ThreadUpdater.forceUpdate(); 6451 }, 6452 // W 6453 87: function() { 6454 if (Config.threadWatcher && Main.tid) ThreadWatcher.toggle(Main.tid); 6455 }, 6456 // B 6457 66: function() { 6458 var el; 6459 (el = $.cls('prev')[0]) && (el = $.tag('form', el)[0]) && el.submit(); 6460 }, 6461 // C 6462 67: function() { 6463 location.href = '/' + Main.board + '/catalog'; 6464 }, 6465 // N 6466 78: function() { 6467 var el; 6468 (el = $.cls('next')[0]) && (el = $.tag('form', el)[0]) && el.submit(); 6469 }, 6470 // I 6471 73: function() { 6472 location.href = '/' + Main.board + '/'; 6473 } 6474 }; 6475 6476 document.addEventListener('keydown', this.resolve, false); 6477 }; 6478 6479 Keybinds.resolve = function(e) { 6480 var bind, el = e.target; 6481 6482 if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { 6483 return; 6484 } 6485 6486 bind = Keybinds.map[e.keyCode]; 6487 6488 if (bind && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { 6489 e.preventDefault(); 6490 e.stopPropagation(); 6491 bind(); 6492 } 6493 }; 6494 6495 Keybinds.open = function() { 6496 var cnt; 6497 6498 if ($.id('keybindsHelp')) { 6499 return; 6500 } 6501 6502 cnt = document.createElement('div'); 6503 cnt.id = 'keybindsHelp'; 6504 cnt.className = 'UIPanel'; 6505 cnt.setAttribute('data-cmd', 'keybinds-close'); 6506 cnt.innerHTML = '\ 6507 <div class="extPanel reply"><div class="panelHeader">Keyboard Shortcuts\ 6508 <span><img data-cmd="keybinds-close" class="pointer" alt="Close" title="Close" src="' 6509 + Main.icons.cross + '"></span></div>\ 6510 <ul>\ 6511 <li><strong>Global</strong></li>\ 6512 <li><kbd>A</kbd> — Toggle auto-updater</li>\ 6513 <li><kbd>Q</kbd> — Open Quick Reply</li>\ 6514 <li><kbd>R</kbd> — Update thread</li>\ 6515 <li><kbd>W</kbd> — Watch/Unwatch thread</li>\ 6516 <li><kbd>B</kbd> — Previous page</li>\ 6517 <li><kbd>N</kbd> — Next page</li>\ 6518 <li><kbd>I</kbd> — Return to index</li>\ 6519 <li><kbd>C</kbd> — Open catalog</li>\ 6520 <li><kbd>F</kbd> — Filter selected text</li>\ 6521 </ul><ul>\ 6522 <li><strong>Quick Reply (always enabled)</strong></li>\ 6523 <li><kbd>Ctrl + Click</kbd> the post number — Quote without linking</li>\ 6524 <li><kbd>Ctrl + S</kbd> — Spoiler tags</li>\ 6525 <li><kbd>Esc</kbd> — Close the Quick Reply</li>\ 6526 </ul>'; 6527 6528 document.body.appendChild(cnt); 6529 cnt.addEventListener('click', this.onClick, false); 6530 }; 6531 6532 Keybinds.close = function() { 6533 var cnt; 6534 6535 if (cnt = $.id('keybindsHelp')) { 6536 cnt.removeEventListener('click', this.onClick, false); 6537 document.body.removeChild(cnt); 6538 } 6539 }; 6540 6541 Keybinds.onClick = function(e) { 6542 var cmd; 6543 6544 if ((cmd = e.target.getAttribute('data-cmd')) && cmd == 'keybinds-close') { 6545 Keybinds.close(); 6546 } 6547 }; 6548 6549 /** 6550 * Reporting 6551 */ 6552 var Report = { 6553 init: function() { 6554 window.addEventListener('message', Report.onMessage, false); 6555 } 6556 }; 6557 6558 Report.onMessage = function(e) { 6559 var id; 6560 6561 if (e.origin === 'https://sys.4chan.org' && /^done-report/.test(e.data)) { 6562 id = e.data.split('-')[2]; 6563 6564 if (Config.threadHiding && $.id('t' + id)) { 6565 if (!ThreadHiding.isHidden(id)) { 6566 ThreadHiding.hide(id); 6567 ThreadHiding.save(); 6568 } 6569 6570 return; 6571 } 6572 6573 if ($.id('p' + id)) { 6574 if (!ReplyHiding.isHidden(id)) { 6575 ReplyHiding.hide(id); 6576 ReplyHiding.save(); 6577 } 6578 6579 return; 6580 } 6581 } 6582 }; 6583 6584 Report.open = function(pid, board) { 6585 window.open('https://sys.4chan.org/' 6586 + (board || Main.board) + '/imgboard.php?mode=report&no=' + pid 6587 , Date.now(), 6588 "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=600,height=170"); 6589 }; 6590 6591 /** 6592 * Custom Menu 6593 */ 6594 var CustomMenu = {}; 6595 6596 CustomMenu.reset = function() { 6597 var i, el, full, custom, navs; 6598 6599 full = $.cls('boardList'); 6600 custom = $.cls('customBoardList'); 6601 navs = $.cls('show-all-boards'); 6602 6603 for (i = 0; el = navs[i]; ++i) { 6604 el.removeEventListener('click', CustomMenu.reset, false); 6605 } 6606 6607 for (i = custom.length - 1; el = custom[i]; i--) { 6608 full[i].style.display = null; 6609 el.parentNode.removeChild(el); 6610 } 6611 }; 6612 6613 CustomMenu.apply = function(str) { 6614 var i, j, el, cntBottom, board, navs, boardList, more; 6615 6616 if (!str) { 6617 return; 6618 } 6619 6620 boardList = str.split(/[^0-9a-z]/i); 6621 6622 cnt = document.createElement('span'); 6623 cnt.className = 'customBoardList'; 6624 6625 for (i = 0; board = boardList[i]; ++i) { 6626 if (i) { 6627 cnt.appendChild(document.createTextNode(' / ')); 6628 } 6629 else { 6630 cnt.appendChild(document.createTextNode('[')); 6631 } 6632 el = document.createElement('a'); 6633 el.textContent = board; 6634 el.href = '//boards.4chan.org/' + board + '/'; 6635 cnt.appendChild(el); 6636 } 6637 6638 cnt.appendChild(document.createTextNode(']')); 6639 6640 cnt.appendChild(document.createTextNode(' [')); 6641 el = document.createElement('a'); 6642 el.textContent = 'โฆ'; 6643 el.title = 'Show all'; 6644 el.className = 'show-all-boards pointer'; 6645 cnt.appendChild(el); 6646 cnt.appendChild(document.createTextNode('] ')); 6647 6648 cntBottom = cnt.cloneNode(true); 6649 6650 navs = $.cls('boardList'); 6651 6652 for (i = 0; el = navs[i]; ++i) { 6653 el.style.display = 'none'; 6654 el.parentNode.insertBefore(i ? cntBottom : cnt, el); 6655 } 6656 6657 navs = $.cls('show-all-boards'); 6658 6659 for (i = 0; el = navs[i]; ++i) { 6660 el.addEventListener('click', CustomMenu.reset, false); 6661 } 6662 }; 6663 6664 CustomMenu.onClick = function(e) { 6665 var t; 6666 6667 if ((t = e.target) == document) { 6668 return; 6669 } 6670 6671 if (t.hasAttribute('data-close')) { 6672 CustomMenu.closeEditor(); 6673 } 6674 else if (t.hasAttribute('data-save')) { 6675 CustomMenu.save(); 6676 } 6677 }; 6678 6679 CustomMenu.showEditor = function() { 6680 var cnt; 6681 6682 cnt = document.createElement('div'); 6683 cnt.id = 'customMenu'; 6684 cnt.className = 'UIPanel'; 6685 cnt.setAttribute('data-close', '1'); 6686 cnt.innerHTML = '\ 6687 <div class="extPanel reply"><div class="panelHeader">Custom Board List\ 6688 <span><img alt="Close" title="Close" class="pointer" data-close="1" src="' 6689 + Main.icons.cross + '"></a></span></div>\ 6690 <input placeholder="Example: jp tg mu" id="customMenuBox" type="text" value="">\ 6691 <div class="center"><button data-save="1">Save</button></div></div>'; 6692 6693 document.body.appendChild(cnt); 6694 6695 if (Config.customMenuList) { 6696 $.id('customMenuBox').value = Config.customMenuList; 6697 } 6698 6699 cnt.addEventListener('click', CustomMenu.onClick, false); 6700 }; 6701 6702 CustomMenu.closeEditor = function() { 6703 var el; 6704 6705 if (el = $.id('customMenu')) { 6706 el.removeEventListener('click', CustomMenu.onClick, false); 6707 document.body.removeChild(el); 6708 } 6709 }; 6710 6711 CustomMenu.save = function() { 6712 var input; 6713 6714 if (input = $.id('customMenuBox')) { 6715 Config.customMenuList = input.value; 6716 } 6717 6718 CustomMenu.closeEditor(); 6719 }; 6720 6721 /** 6722 * Draggable helper 6723 */ 6724 var Draggable = { 6725 el: null, 6726 key: null, 6727 scrollX: null, 6728 scrollY: null, 6729 dx: null, dy: null, right: null, bottom: null, 6730 6731 set: function(handle) { 6732 handle.addEventListener('mousedown', Draggable.startDrag, false); 6733 }, 6734 6735 unset: function(handle) { 6736 handle.removeEventListener('mousedown', Draggable.startDrag, false); 6737 }, 6738 6739 startDrag: function(e) { 6740 var self, doc, offs; 6741 6742 if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { 6743 return; 6744 } 6745 6746 e.preventDefault(); 6747 6748 self = Draggable; 6749 doc = document.documentElement; 6750 6751 self.el = this.parentNode; 6752 6753 self.key = self.el.getAttribute('data-trackpos'); 6754 offs = self.el.getBoundingClientRect(); 6755 self.dx = e.clientX - offs.left; 6756 self.dy = e.clientY - offs.top; 6757 self.right = doc.clientWidth - offs.width; 6758 self.bottom = doc.clientHeight - offs.height; 6759 6760 if (getComputedStyle(self.el, null).position != 'fixed') { 6761 self.scrollX = window.pageXOffset; 6762 self.scrollY = window.pageYOffset; 6763 } 6764 else { 6765 self.scrollX = self.scrollY = 0; 6766 } 6767 6768 document.addEventListener('mouseup', self.endDrag, false); 6769 document.addEventListener('mousemove', self.onDrag, false); 6770 }, 6771 6772 endDrag: function(e) { 6773 document.removeEventListener('mouseup', Draggable.endDrag, false); 6774 document.removeEventListener('mousemove', Draggable.onDrag, false); 6775 if (Draggable.key) { 6776 Config[Draggable.key] = Draggable.el.style.cssText; 6777 Config.save(); 6778 } 6779 delete Draggable.el; 6780 }, 6781 6782 onDrag: function(e) { 6783 var left, top, style; 6784 6785 left = e.clientX - Draggable.dx + Draggable.scrollX; 6786 top = e.clientY - Draggable.dy + Draggable.scrollY; 6787 style = Draggable.el.style; 6788 if (left < 1) { 6789 style.left = '0'; 6790 style.right = ''; 6791 } 6792 else if (Draggable.right < left) { 6793 style.left = ''; 6794 style.right = '0'; 6795 } 6796 else { 6797 style.left = (left / document.documentElement.clientWidth * 100) + '%'; 6798 style.right = ''; 6799 } 6800 if (top < 1) { 6801 style.top = '0'; 6802 style.bottom = ''; 6803 } 6804 else if (Draggable.bottom < top) { 6805 style.bottom = '0'; 6806 style.top = ''; 6807 } 6808 else { 6809 style.top = (top / document.documentElement.clientHeight * 100) + '%'; 6810 style.bottom = ''; 6811 } 6812 } 6813 }; 6814 6815 /** 6816 * User Agent 6817 */ 6818 var UA = {}; 6819 6820 UA.init = function() { 6821 document.head = document.head || $.tag('head')[0]; 6822 6823 this.isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; 6824 6825 this.hasCORS = 'withCredentials' in new XMLHttpRequest; 6826 6827 this.hasFormData = 'FormData' in window; 6828 6829 this.hasDragAndDrop = false; /*'draggable' in document.createElement('div');*/ 6830 }; 6831 6832 UA.dispatchEvent = function(name, detail) { 6833 var e = document.createEvent('Event'); 6834 e.initEvent(name, false, false); 6835 if (detail) { 6836 e.detail = detail; 6837 } 6838 document.dispatchEvent(e); 6839 }; 6840 6841 UA.getSelection = function(raw) { 6842 var sel; 6843 6844 if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {} 6845 else { 6846 sel = window.getSelection(); 6847 6848 if (!raw) { 6849 sel = sel.toString(); 6850 } 6851 } 6852 6853 return sel; 6854 }; 6855 6856 /** 6857 * Config 6858 */ 6859 var Config = { 6860 quotePreview: true, 6861 backlinks: true, 6862 quickReply: true, 6863 threadUpdater: true, 6864 threadHiding: true, 6865 6866 alwaysAutoUpdate: false, 6867 topPageNav: false, 6868 threadWatcher: false, 6869 imageExpansion: true, 6870 fitToScreenExpansion: false, 6871 threadExpansion: true, 6872 alwaysDepage: false, 6873 localTime: true, 6874 stickyNav: false, 6875 keyBinds: false, 6876 inlineQuotes: false, 6877 6878 filter: false, 6879 revealSpoilers: false, 6880 imageHover: false, 6881 threadStats: true, 6882 IDColor: true, 6883 noPictures: false, 6884 embedYouTube: true, 6885 embedSoundCloud: false, 6886 updaterSound: false, 6887 6888 customCSS: false, 6889 autoScroll: false, 6890 hideStubs: false, 6891 compactThreads: false, 6892 centeredThreads: false, 6893 dropDownNav: false, 6894 classicNav: false, 6895 fixedThreadWatcher: false, 6896 persistentQR: false, 6897 forceHTTPS: false, 6898 reportButton: false, 6899 6900 disableAll: false 6901 }; 6902 6903 var ConfigMobile = { 6904 embedYouTube: false, 6905 compactThreads: false 6906 }; 6907 6908 Config.load = function() { 6909 if (storage = localStorage.getItem('4chan-settings')) { 6910 storage = JSON.parse(storage); 6911 $.extend(Config, storage); 6912 6913 if (Main.getCookie('https') === '1') { 6914 Config.forceHTTPS = true; 6915 } 6916 else { 6917 Config.forceHTTPS = false; 6918 } 6919 } 6920 else { 6921 Main.firstRun = true; 6922 } 6923 }; 6924 6925 Config.loadFromURL = function() { 6926 var cmd, data; 6927 6928 cmd = location.href.split('=', 2); 6929 6930 if (/#cfg$/.test(cmd[0])) { 6931 try { 6932 data = JSON.parse(decodeURIComponent(cmd[1])); 6933 6934 history.replaceState(null, '', location.href.split('#', 1)[0]); 6935 6936 $.extend(Config, JSON.parse(data.settings)); 6937 6938 Config.save(); 6939 6940 if (data.filters) { 6941 localStorage.setItem('4chan-filters', data.filters); 6942 } 6943 6944 if (data.css) { 6945 localStorage.setItem('4chan-css', data.css); 6946 } 6947 6948 if (data.catalogFilters) { 6949 localStorage.setItem('catalog-filters', data.catalogFilters); 6950 } 6951 6952 if (data.catalogSettings) { 6953 localStorage.setItem('catalog-settings', data.catalogSettings); 6954 } 6955 6956 return true; 6957 } 6958 catch (e) { 6959 console.log(e); 6960 } 6961 } 6962 6963 return false; 6964 }; 6965 6966 Config.toURL = function() { 6967 var data, cfg = {}; 6968 6969 cfg.settings = localStorage.getItem('4chan-settings'); 6970 6971 if (data = localStorage.getItem('4chan-filters')) { 6972 cfg.filters = data; 6973 } 6974 6975 if (data = localStorage.getItem('4chan-css')) { 6976 cfg.css = data; 6977 } 6978 6979 if (data = localStorage.getItem('catalog-filters')) { 6980 cfg.catalogFilters = data; 6981 } 6982 6983 if (data = localStorage.getItem('catalog-settings')) { 6984 cfg.catalogSettings = data; 6985 } 6986 6987 return encodeURIComponent(JSON.stringify(cfg)); 6988 }; 6989 6990 Config.save = function() { 6991 localStorage.setItem('4chan-settings', JSON.stringify(Config)); 6992 6993 if (Config.forceHTTPS) { 6994 Main.setCookie('https', 1); 6995 } 6996 else { 6997 Main.removeCookie('https'); 6998 } 6999 }; 7000 7001 /** 7002 * Settings menu 7003 */ 7004 var SettingsMenu = {}; 7005 7006 // [ Name, Subtitle, available on mobile?, is sub-option?, is mobile only? ] 7007 SettingsMenu.options = { 7008 'Quotes & Replying': { 7009 quotePreview: [ 'Quote preview', 'Show post when mousing over post links', true ], 7010 backlinks: [ 'Backlinks', 'Show who has replied to a post', true ], 7011 inlineQuotes: [ 'Inline quote links', 'Clicking quote links will inline expand the quoted post, Shift-click to bypass inlining' ], 7012 quickReply: [ 'Quick Reply', 'Quickly respond to a post by clicking its post number', true ], 7013 persistentQR: [ 'Persistent Quick Reply', 'Keep Quick Reply window open after posting' ] 7014 }, 7015 'Monitoring': { 7016 threadUpdater: [ 'Thread updater', 'Append new posts to bottom of thread without refreshing the page', true ], 7017 alwaysAutoUpdate:[ 'Auto-update by default', 'Always auto-update threads', true ], 7018 threadWatcher: [ 'Thread Watcher', 'Keep track of threads you\'re watching and see when they receive new posts', true ], 7019 autoScroll: [ 'Auto-scroll with auto-updated posts', 'Automatically scroll the page as new posts are added' ], 7020 updaterSound: [ 'Sound notification', 'Play a sound when somebody replies to your post(s)' ], 7021 fixedThreadWatcher: [ 'Pin Thread Watcher to the page', 'Thread Watcher will scroll with you' ], 7022 threadStats: [ 'Thread statistics', 'Display post and image counts on the right of the page, <em>italics</em> signify bump/image limit has been met' ], 7023 }, 7024 'Filters & Post Hiding': { 7025 filter: [ 'Filter and highlight specific threads/posts [<a href="javascript:;" data-cmd="filters-open">Edit</a>]', 'Enable pattern-based filters' ], 7026 threadHiding: [ 'Thread hiding [<a href="javascript:;" data-cmd="thread-hiding-clear">Clear History</a>]', 'Hide entire threads by clicking the minus button', true ], 7027 hideStubs: [ 'Hide thread stubs', "Don't display stubs of hidden threads" ] 7028 }, 7029 'Navigation': { 7030 threadExpansion: [ 'Thread expansion', 'Expand threads inline on board indexes', true ], 7031 dropDownNav: [ 'Use persistent drop-down navigation bar', '' ], 7032 classicNav: [ 'Use traditional board list', '', false, true ], 7033 customMenu: [ 'Custom board list [<a href="javascript:;" data-cmd="custom-menu-edit">Edit</a>]', 'Only show selected boards in top and bottom board lists' ], 7034 alwaysDepage: [ 'Always use infinite scroll', 'Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages' ], 7035 topPageNav: [ 'Page navigation at top of page', 'Show the page switcher at the top of the page, hold Shift and drag to move' ], 7036 stickyNav: [ 'Navigation arrows', 'Show top and bottom navigation arrows, hold Shift and drag to move' ], 7037 keyBinds: [ 'Use keyboard shortcuts [<a href="javascript:;" data-cmd="keybinds-open">Show</a>]', 'Enable handy keyboard shortcuts for common actions' ] 7038 }, 7039 'Images & Media': { 7040 imageExpansion: [ 'Image expansion', 'Enable inline image expansion, limited to browser width', true ], 7041 fitToScreenExpansion: [ 'Fit expanded images to screen', 'Limit expanded images to both browser width and height' ], 7042 imageHover: [ 'Image hover', 'Mouse over images to view full size, limited to browser size' ], 7043 revealSpoilers: [ "Don't spoiler images", 'Show image thumbnail and original filename instead of spoiler placeholders' ], 7044 noPictures: [ 'Hide thumbnails', 'Don\'t display thumbnails while browsing', true ], 7045 embedYouTube: [ 'Embed YouTube links', 'Embed YouTube player into replies' ], 7046 embedSoundCloud: [ 'Embed SoundCloud links', 'Embed SoundCloud player into replies' ], 7047 embedVocaroo: [ 'Embed Vocaroo links', 'Embed Vocaroo player into replies' ] 7048 }, 7049 'Miscellaneous': { 7050 customCSS: [ 'Custom CSS [<a href="javascript:;" data-cmd="css-open">Edit</a>]', 'Include your own CSS rules', true ], 7051 IDColor: [ 'Color user IDs', 'Assign unique colors to user IDs on boards that use them', true ], 7052 compactThreads: [ 'Force long posts to wrap', 'Long posts will wrap at 75% browser width' ], 7053 centeredThreads: [ 'Center threads', 'Align threads to the center of page', false ], 7054 reportButton: [ 'Report button', 'Add a report button next to posts for easy reporting', true, false, true ], 7055 localTime: [ 'Convert dates to local time', 'Convert 4chan server time (US Eastern Time) to your local time', true ], 7056 forceHTTPS: [ 'Always use HTTPS', 'Rewrite 4chan URLs to always use HTTPS', true ] 7057 } 7058 }; 7059 7060 SettingsMenu.save = function() { 7061 var i, options, el, key; 7062 7063 options = $.id('settingsMenu').getElementsByClassName('menuOption'); 7064 7065 for (i = 0; el = options[i]; ++i) { 7066 key = el.getAttribute('data-option'); 7067 Config[key] = el.type == 'checkbox' ? el.checked : el.value; 7068 } 7069 7070 Config.save(); 7071 7072 SettingsMenu.close(); 7073 location.href = location.href.replace(/#.+$/, ''); 7074 }; 7075 7076 SettingsMenu.toggle = function() { 7077 if ($.id('settingsMenu')) { 7078 SettingsMenu.close(); 7079 } 7080 else { 7081 SettingsMenu.open(); 7082 } 7083 }; 7084 7085 SettingsMenu.open = function() { 7086 var i, cat, categories, key, html, cnt, opts, mobileOpts, el; 7087 7088 if (Main.firstRun) { 7089 if (el = $.id('settingsTip')) { 7090 el.parentNode.removeChild(el); 7091 } 7092 if (el = $.id('settingsTipBottom')) { 7093 el.parentNode.removeChild(el); 7094 } 7095 Config.save(); 7096 } 7097 7098 cnt = document.createElement('div'); 7099 cnt.id = 'settingsMenu'; 7100 cnt.className = 'UIPanel'; 7101 7102 html = '<div class="extPanel reply"><div class="panelHeader">Settings' 7103 + '<span><img alt="Close" title="Close" class="pointer" data-cmd="settings-toggle" src="' 7104 + Main.icons.cross + '"></a>' 7105 + '</span></div><ul>'; 7106 7107 html += '<ul><li id="settings-exp-all">[<a href="#" data-cmd="settings-exp-all">Expand All Settings</a>]</li></ul>'; 7108 7109 if (Main.hasMobileLayout) { 7110 categories = {}; 7111 for (cat in SettingsMenu.options) { 7112 mobileOpts = {}; 7113 opts = SettingsMenu.options[cat]; 7114 for (key in opts) { 7115 if (opts[key][2]) { 7116 mobileOpts[key] = opts[key]; 7117 } 7118 } 7119 for (i in mobileOpts) { 7120 categories[cat] = mobileOpts; 7121 break; 7122 } 7123 } 7124 } 7125 else { 7126 categories = SettingsMenu.options; 7127 } 7128 7129 for (cat in categories) { 7130 opts = categories[cat]; 7131 html += '<ul><li class="settings-cat-lbl">' 7132 + '<img alt="" class="settings-expand" src="' + Main.icons.plus + '">' 7133 + '<span class="settings-expand pointer">' 7134 + cat + '</span></li><ul class="settings-cat">'; 7135 for (key in opts) { 7136 // Mobile layout only? 7137 if (opts[key][4] && !Main.hasMobileLayout) { 7138 continue; 7139 } 7140 html += '<li' + (opts[key][3] ? ' class="settings-sub">' : '>') 7141 + '<label><input type="checkbox" class="menuOption" data-option="' 7142 + key + '"' + (Config[key] ? ' checked="checked">' : '>') 7143 + opts[key][0] + '</label>' 7144 + (opts[key][1] !== false ? '</li><li class="settings-tip' 7145 + (opts[key][3] ? ' settings-sub">' : '">') + opts[key][1] : '') 7146 + '</li>'; 7147 } 7148 html += '</ul></ul>'; 7149 } 7150 7151 html += '</ul><ul><li class="settings-off">' 7152 + '<label title="Completely disable the native extension (overrides any checked boxes)">' 7153 + '<input type="checkbox" class="menuOption" data-option="disableAll"' 7154 + (Config.disableAll ? ' checked="checked">' : '>') 7155 + 'Disable the native extension</label></li></ul>' 7156 + '<div class="center"><button data-cmd="settings-export">Export Settings</button>' 7157 + '<button data-cmd="settings-save">Save Settings</button></div>'; 7158 7159 cnt.innerHTML = html; 7160 cnt.addEventListener('click', SettingsMenu.onClick, false); 7161 document.body.appendChild(cnt); 7162 7163 if (Main.firstRun) { 7164 SettingsMenu.expandAll(); 7165 } 7166 7167 (el = $.cls('menuOption', cnt)[0]) && el.focus(); 7168 }; 7169 7170 SettingsMenu.showExport = function() { 7171 var cnt, str, el; 7172 7173 if ($.id('exportSettings')) { 7174 return; 7175 } 7176 7177 str = location.href.replace(location.hash, '') + '#cfg=' + Config.toURL(); 7178 7179 cnt = document.createElement('div'); 7180 cnt.id = 'exportSettings'; 7181 cnt.className = 'UIPanel'; 7182 cnt.setAttribute('data-cmd', 'export-close'); 7183 cnt.innerHTML = '\ 7184 <div class="extPanel reply"><div class="panelHeader">Export Settings\ 7185 <span><img data-cmd="export-close" class="pointer" alt="Close" title="Close" src="' 7186 + Main.icons.cross + '"></span></div>\ 7187 <p class="center">Copy and save the URL below, and visit it from another \ 7188 browser or computer to restore your extension and catalog settings.</p>\ 7189 <p class="center">\ 7190 <input class="export-field" type="text" readonly="readonly" value="' + str + '"></p>\ 7191 <p style="margin-top:15px" class="center">Alternatively, you can drag the link below into your \ 7192 bookmarks bar and click it to restore.</p>\ 7193 <p class="center">[<a target="_blank" href="' 7194 + str + '">Restore 4chan Settings</a>]</p>'; 7195 7196 document.body.appendChild(cnt); 7197 cnt.addEventListener('click', this.onExportClick, false); 7198 el = $.cls('export-field', cnt)[0]; 7199 el.focus(); 7200 el.select(); 7201 }; 7202 7203 SettingsMenu.closeExport = function() { 7204 var cnt; 7205 7206 if (cnt = $.id('exportSettings')) { 7207 cnt.removeEventListener('click', this.onExportClick, false); 7208 document.body.removeChild(cnt); 7209 } 7210 }; 7211 7212 SettingsMenu.onExportClick = function(e) { 7213 var el; 7214 7215 if (e.target.id == 'exportSettings') { 7216 e.preventDefault(); 7217 e.stopPropagation(); 7218 SettingsMenu.closeExport(); 7219 } 7220 }; 7221 7222 SettingsMenu.expandAll = function() { 7223 var i, el, nodes = $.cls('settings-expand'); 7224 7225 for (i = 0; el = nodes[i]; ++i) { 7226 el.src = Main.icons.minus; 7227 el.parentNode.nextElementSibling.style.display = 'block'; 7228 } 7229 }; 7230 7231 SettingsMenu.toggleCat = function(t) { 7232 var icon, disp, el = t.parentNode.nextElementSibling; 7233 7234 if (!el.style.display) { 7235 disp = 'block'; 7236 icon = 'minus'; 7237 } 7238 else { 7239 disp = ''; 7240 icon = 'plus'; 7241 } 7242 7243 el.style.display = disp; 7244 t.parentNode.firstElementChild.src = Main.icons[icon]; 7245 }; 7246 7247 SettingsMenu.onClick = function(e) { 7248 var el, t, i, j; 7249 7250 t = e.target; 7251 7252 if ($.hasClass(t, 'settings-expand')) { 7253 SettingsMenu.toggleCat(t); 7254 } 7255 else if (t.getAttribute('data-cmd') == 'settings-exp-all') { 7256 e.preventDefault(); 7257 SettingsMenu.expandAll(); 7258 } 7259 else if (t.id == 'settingsMenu' && (el = $.id('settingsMenu'))) { 7260 e.preventDefault(); 7261 SettingsMenu.close(el); 7262 } 7263 }; 7264 7265 SettingsMenu.close = function(el) { 7266 if (el = (el || $.id('settingsMenu'))) { 7267 el.removeEventListener('click', SettingsMenu.onClick, false); 7268 document.body.removeChild(el); 7269 } 7270 }; 7271 7272 /** 7273 * Main 7274 */ 7275 var Main = {}; 7276 7277 Main.addTooltip = function(link, message, id) { 7278 var el, pos; 7279 7280 el = document.createElement('div'); 7281 el.className = 'click-me'; 7282 if (id) { 7283 el.id = id; 7284 } 7285 el.innerHTML = message || 'Change your settings'; 7286 link.parentNode.appendChild(el); 7287 7288 pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; 7289 el.style.marginLeft = pos + 'px'; 7290 7291 return el; 7292 }; 7293 7294 Main.init = function() { 7295 var params; 7296 7297 document.addEventListener('DOMContentLoaded', Main.run, false); 7298 7299 Main.now = Date.now(); 7300 7301 UA.init(); 7302 7303 Config.load(); 7304 7305 if (Config.forceHTTPS && location.protocol != 'https:') { 7306 location.href = location.href.replace(/^http:/, 'https:'); 7307 return; 7308 } 7309 7310 if (Main.firstRun && Config.loadFromURL()) { 7311 Main.firstRun = false; 7312 } 7313 7314 if (Main.stylesheet = Main.getCookie(style_group)) { 7315 Main.stylesheet = Main.stylesheet.toLowerCase().replace(/ /g, '_'); 7316 } 7317 else { 7318 Main.stylesheet = 7319 style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; 7320 } 7321 7322 Main.passEnabled = Main.getCookie('pass_enabled'); 7323 QR.noCaptcha = QR.noCaptcha || Main.passEnabled; 7324 7325 Main.initIcons(); 7326 7327 Main.addCSS(); 7328 7329 Main.type = style_group.split('_')[0]; 7330 7331 params = location.pathname.split(/\//); 7332 Main.board = params[1]; 7333 Main.page = params[2]; 7334 Main.tid = params[3]; 7335 7336 Report.init(); 7337 7338 if (Config.IDColor) { 7339 IDColor.init(); 7340 } 7341 7342 if (Config.customCSS) { 7343 CustomCSS.init(); 7344 } 7345 7346 if (Config.keyBinds) { 7347 Keybinds.init(); 7348 } 7349 7350 UA.dispatchEvent('4chanMainInit'); 7351 }; 7352 7353 Main.initPersistentNav = function() { 7354 var el, top, bottom; 7355 7356 top = $.id('boardNavDesktop'); 7357 bottom = $.id('boardNavDesktopFoot'); 7358 7359 if (Config.classicNav) { 7360 el = document.createElement('div'); 7361 el.className = 'pageJump'; 7362 el.innerHTML = '<a href="#bottom">▼</a>' 7363 + '<a href="javascript:void(0);" id="settingsWindowLinkClassic">Settings</a>' 7364 + '<a href="//www.4chan.org" target="_top">Home</a></div>'; 7365 7366 top.appendChild(el); 7367 7368 $.id('settingsWindowLinkClassic') 7369 .addEventListener('click', SettingsMenu.toggle, false); 7370 7371 $.addClass(top, 'persistentNav'); 7372 } 7373 else { 7374 top.style.display = 'none'; 7375 $.removeClass($.id('boardNavMobile'), 'mobile'); 7376 } 7377 7378 bottom.style.display = 'none'; 7379 7380 $.addClass(document.body, 'hasDropDownNav'); 7381 }; 7382 7383 Main.checkMobileLayout = function() { 7384 var mobile, desktop; 7385 7386 if (window.matchMedia) { 7387 return window.matchMedia('(max-width: 480px)').matches 7388 && localStorage.getItem('4chan_never_show_mobile') != 'true'; 7389 } 7390 7391 mobile = $.id('boardNavMobile'); 7392 desktop = $.id('boardNavDesktop'); 7393 7394 return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth == 0; 7395 }; 7396 7397 Main.run = function() { 7398 var thread; 7399 7400 document.removeEventListener('DOMContentLoaded', Main.run, false); 7401 7402 document.addEventListener('click', Main.onclick, false); 7403 7404 $.id('settingsWindowLink').addEventListener('click', SettingsMenu.toggle, false); 7405 $.id('settingsWindowLinkBot').addEventListener('click', SettingsMenu.toggle, false); 7406 $.id('settingsWindowLinkMobile').addEventListener('click', SettingsMenu.toggle, false); 7407 7408 if (Config.disableAll) { 7409 return; 7410 } 7411 7412 Main.hasMobileLayout = Main.checkMobileLayout(); 7413 Main.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); 7414 7415 if (Main.hasMobileLayout) { 7416 $.extend(Config, ConfigMobile); 7417 } 7418 else { 7419 $.id('bottomReportBtn').style.display = 'none'; 7420 7421 if (Main.isMobileDevice) { 7422 $.addClass(document.body, 'isMobileDevice'); 7423 } 7424 } 7425 7426 if (Main.firstRun && Main.isMobileDevice) { 7427 Config.topPageNav = false; 7428 Config.dropDownNav = true; 7429 } 7430 7431 if (Config.dropDownNav && !Main.hasMobileLayout) { 7432 Main.initPersistentNav(); 7433 } 7434 7435 $.addClass(document.body, Main.stylesheet); 7436 $.addClass(document.body, Main.type); 7437 7438 if (Config.compactThreads) { 7439 $.addClass(document.body, 'compact'); 7440 } 7441 else if (Config.centeredThreads) { 7442 $.addClass(document.body, 'centeredThreads'); 7443 } 7444 7445 if (Config.noPictures) { 7446 $.addClass(document.body, 'noPictures'); 7447 } 7448 7449 if (Config.customMenu) { 7450 CustomMenu.apply(Config.customMenuList); 7451 } 7452 7453 if (Config.quotePreview || Config.imageHover|| Config.filter) { 7454 thread = $.id('delform'); 7455 thread.addEventListener('mouseover', Main.onThreadMouseOver, false); 7456 thread.addEventListener('mouseout', Main.onThreadMouseOut, false); 7457 } 7458 7459 if (!Main.hasMobileLayout) { 7460 Main.initGlobalMessage(); 7461 } 7462 7463 if (Config.stickyNav) { 7464 Main.setStickyNav(); 7465 } 7466 7467 if (Config.threadExpansion) { 7468 ThreadExpansion.init(); 7469 } 7470 7471 if (Config.threadWatcher) { 7472 ThreadWatcher.init(); 7473 } 7474 7475 if (Config.filter) { 7476 Filter.init(); 7477 } 7478 7479 if (Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) { 7480 Media.init(); 7481 } 7482 7483 ReplyHiding.init(); 7484 7485 if (Config.quotePreview) { 7486 QuotePreview.init(); 7487 } 7488 7489 Parser.init(); 7490 7491 if (Main.tid) { 7492 Main.threadClosed = !document.forms.post; 7493 Main.threadSticky = !!$.cls('stickyIcon', $.id('pi' + Main.tid))[0]; 7494 7495 if (Config.threadStats) { 7496 ThreadStats.init(); 7497 } 7498 7499 Parser.parseThread(Main.tid); 7500 7501 if (Config.threadUpdater) { 7502 ThreadUpdater.init(); 7503 } 7504 } 7505 else { 7506 if (!Main.page) { 7507 Depager.init(); 7508 } 7509 7510 if (Config.topPageNav) { 7511 Main.setPageNav(); 7512 } 7513 if (Config.threadHiding) { 7514 ThreadHiding.init(); 7515 Parser.parseBoard(); 7516 } 7517 else { 7518 Parser.parseBoard(); 7519 } 7520 } 7521 7522 if (Main.board === 'f') { 7523 SWFEmbed.init(); 7524 } 7525 7526 if (Config.quickReply) { 7527 QR.init(); 7528 } 7529 7530 ReplyHiding.purge(); 7531 }; 7532 7533 Main.isThreadClosed = function(tid) { 7534 return window.thread_archived || ((el = $.id('pi' + tid)) && $.cls('closedIcon', el)[0]) 7535 }; 7536 7537 Main.setThreadState = function(state, mode) { 7538 var cnt, el, ref, cap; 7539 7540 cap = state.charAt(0).toUpperCase() + state.slice(1); 7541 7542 if (mode) { 7543 cnt = $.cls('postNum', $.id('pi' + Main.tid))[0]; 7544 el = document.createElement('img'); 7545 el.className = state + 'Icon retina'; 7546 el.title = cap; 7547 el.src = Main.icons2[state]; 7548 if (state == 'sticky' && (ref = $.cls('closedIcon', cnt)[0])) { 7549 cnt.insertBefore(el, ref); 7550 cnt.insertBefore(document.createTextNode(' '), ref); 7551 } 7552 else { 7553 cnt.appendChild(document.createTextNode(' ')); 7554 cnt.appendChild(el); 7555 } 7556 } 7557 else { 7558 if (el = $.cls(state + 'Icon', $.id('pi' + Main.tid))[0]) { 7559 el.parentNode.removeChild(el.previousSibling); 7560 el.parentNode.removeChild(el); 7561 } 7562 } 7563 7564 Main['thread' + cap] = mode; 7565 }; 7566 7567 Main.icons = { 7568 up: 'arrow_up.png', 7569 down: 'arrow_down.png', 7570 right: 'arrow_right.png', 7571 download: 'arrow_down2.png', 7572 refresh: 'refresh.png', 7573 cross: 'cross.png', 7574 gis: 'gis.png', 7575 iqdb: 'iqdb.png', 7576 minus: 'post_expand_minus.png', 7577 plus: 'post_expand_plus.png', 7578 rotate: 'post_expand_rotate.gif', 7579 quote: 'quote.png', 7580 report: 'report.png', 7581 notwatched: 'watch_thread_off.png', 7582 watched: 'watch_thread_on.png', 7583 help: 'question.png' 7584 }; 7585 7586 Main.icons2 = { 7587 archived: 'archived.gif', 7588 closed: 'closed.gif', 7589 sticky: 'sticky.gif', 7590 trash: 'trash.gif' 7591 }, 7592 7593 Main.initIcons = function() { 7594 var key, paths, url; 7595 7596 paths = { 7597 yotsuba_new: 'futaba/', 7598 futaba_new: 'futaba/', 7599 yotsuba_b_new: 'burichan/', 7600 burichan_new: 'burichan/', 7601 tomorrow: 'tomorrow/', 7602 photon: 'photon/' 7603 }; 7604 7605 url = '//s.4cdn.org/image/' 7606 7607 if (window.devicePixelRatio >= 2) { 7608 for (key in Main.icons) { 7609 Main.icons[key] = Main.icons[key].replace('.', '@2x.'); 7610 } 7611 for (key in Main.icons2) { 7612 Main.icons2[key] = Main.icons2[key].replace('.', '@2x.'); 7613 } 7614 } 7615 7616 for (key in Main.icons2) { 7617 Main.icons2[key] = url + Main.icons2[key]; 7618 } 7619 7620 url += 'buttons/' + paths[Main.stylesheet]; 7621 for (key in Main.icons) { 7622 Main.icons[key] = url + Main.icons[key]; 7623 } 7624 }; 7625 7626 Main.setPageNav = function() { 7627 var el, cnt; 7628 7629 cnt = document.createElement('div'); 7630 cnt.setAttribute('data-shiftkey', '1'); 7631 cnt.setAttribute('data-trackpos', 'TN-position'); 7632 cnt.className = 'topPageNav'; 7633 7634 if (Config['TN-position']) { 7635 cnt.style.cssText = Config['TN-position']; 7636 } 7637 else { 7638 cnt.style.left = '10px'; 7639 cnt.style.top = '50px'; 7640 } 7641 7642 el = $.cls('pagelist')[0] 7643 7644 if (!el) { 7645 return; 7646 } 7647 7648 el = el.cloneNode(true); 7649 cnt.appendChild(el); 7650 Draggable.set(el); 7651 document.body.appendChild(cnt); 7652 }; 7653 7654 Main.initGlobalMessage = function() { 7655 var msg, btn, thisTs, oldTs; 7656 7657 if ((msg = $.id('globalMessage')) && msg.textContent) { 7658 msg.nextElementSibling.style.clear = 'both'; 7659 7660 btn = document.createElement('img'); 7661 btn.id = 'toggleMsgBtn'; 7662 btn.className = 'extButton'; 7663 btn.setAttribute('data-cmd', 'toggleMsg'); 7664 btn.alt = 'Toggle'; 7665 btn.title = 'Toggle announcement'; 7666 7667 oldTs = localStorage.getItem('4chan-global-msg'); 7668 thisTs = msg.getAttribute('data-utc'); 7669 7670 if (oldTs && thisTs <= oldTs) { 7671 msg.style.display = 'none'; 7672 btn.style.opacity = '0.5'; 7673 btn.src = Main.icons.plus; 7674 } 7675 else { 7676 btn.src = Main.icons.minus; 7677 } 7678 7679 msg.parentNode.insertBefore(btn, msg); 7680 } 7681 }; 7682 7683 Main.toggleGlobalMessage = function() { 7684 var msg, btn; 7685 7686 msg = $.id('globalMessage'); 7687 btn = $.id('toggleMsgBtn'); 7688 if (msg.style.display == 'none') { 7689 msg.style.display = ''; 7690 btn.src = Main.icons.minus; 7691 btn.style.opacity = '1'; 7692 localStorage.removeItem('4chan-global-msg'); 7693 } 7694 else { 7695 msg.style.display = 'none'; 7696 btn.src = Main.icons.plus; 7697 btn.style.opacity = '0.5'; 7698 localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); 7699 } 7700 }; 7701 7702 Main.setStickyNav = function() { 7703 var cnt, hdr; 7704 7705 cnt = document.createElement('div'); 7706 cnt.id = 'stickyNav'; 7707 cnt.className = 'extPanel reply'; 7708 cnt.setAttribute('data-shiftkey', '1'); 7709 cnt.setAttribute('data-trackpos', 'SN-position'); 7710 7711 if (Config['SN-position']) { 7712 cnt.style.cssText = Config['SN-position']; 7713 } 7714 else { 7715 cnt.style.right = '10px'; 7716 cnt.style.top = '50px'; 7717 } 7718 7719 hdr = document.createElement('div'); 7720 hdr.innerHTML = '<img class="pointer" src="' 7721 + Main.icons.up + '" data-cmd="totop" alt="โฒ" title="Top">' 7722 + '<img class="pointer" src="' + Main.icons.down 7723 + '" data-cmd="tobottom" alt="โผ" title="Bottom">'; 7724 Draggable.set(hdr); 7725 7726 cnt.appendChild(hdr); 7727 document.body.appendChild(cnt); 7728 }; 7729 7730 Main.getCookie = function(name) { 7731 var i, c, ca, key; 7732 7733 key = name + "="; 7734 ca = document.cookie.split(';'); 7735 7736 for (i = 0; c = ca[i]; ++i) { 7737 while (c.charAt(0) == ' ') { 7738 c = c.substring(1, c.length); 7739 } 7740 if (c.indexOf(key) == 0) { 7741 return decodeURIComponent(c.substring(key.length, c.length)); 7742 } 7743 } 7744 return null; 7745 }; 7746 7747 Main.setCookie = function(name, value) { 7748 var date = new Date(); 7749 7750 date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000)); 7751 7752 document.cookie = name + '=' + value 7753 + '; expires=' + date.toGMTString() 7754 + '; path=/; domain=boards.4chan.org'; 7755 }; 7756 7757 Main.removeCookie = function(name) { 7758 document.cookie = name + '=' 7759 + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;' 7760 + '; path=/; domain=boards.4chan.org'; 7761 }; 7762 7763 Main.onclick = function(e) { 7764 var t, cmd, tid; 7765 7766 if ((t = e.target) == document) { 7767 return; 7768 } 7769 7770 if (cmd = t.getAttribute('data-cmd')) { 7771 id = t.getAttribute('data-id'); 7772 switch (cmd) { 7773 case 'update': 7774 e.preventDefault(); 7775 ThreadUpdater.forceUpdate(); 7776 break; 7777 case 'post-menu': 7778 e.preventDefault(); 7779 PostMenu.open(t); 7780 break; 7781 case 'auto': 7782 ThreadUpdater.toggleAuto(); 7783 break; 7784 case 'totop': 7785 case 'tobottom': 7786 if (!e.shiftKey) { 7787 location.href = '#' + cmd.slice(2); 7788 } 7789 break; 7790 case 'hide': 7791 ThreadHiding.toggle(id); 7792 break; 7793 case 'watch': 7794 ThreadWatcher.toggle(id); 7795 break; 7796 case 'hide-r': 7797 ReplyHiding.toggle(id); 7798 break; 7799 case 'expand': 7800 ThreadExpansion.toggle(id); 7801 break; 7802 case 'open-qr': 7803 e.preventDefault(); 7804 QR.show(Main.tid); 7805 $.tag('textarea', document.forms.qrPost)[0].focus(); 7806 break; 7807 case 'depage': 7808 e.preventDefault(); 7809 Depager.toggle(); 7810 break; 7811 case 'report': 7812 Report.open(id, t.getAttribute('data-board')); 7813 break; 7814 case 'filter-sel': 7815 e.preventDefault(); 7816 Filter.addSelection(); 7817 break; 7818 case 'embed': 7819 Media.toggleEmbed(t); 7820 break 7821 case 'sound': 7822 ThreadUpdater.toggleSound(); 7823 break; 7824 case 'toggleMsg': 7825 Main.toggleGlobalMessage(); 7826 break; 7827 case 'settings-toggle': 7828 SettingsMenu.toggle(); 7829 break; 7830 case 'settings-save': 7831 SettingsMenu.save(); 7832 break; 7833 case 'keybinds-open': 7834 Keybinds.open(); 7835 break; 7836 case 'filters-open': 7837 Filter.open(); 7838 break; 7839 case 'thread-hiding-clear': 7840 ThreadHiding.clear(); 7841 break; 7842 case 'css-open': 7843 CustomCSS.open(); 7844 break; 7845 case 'settings-export': 7846 SettingsMenu.showExport(); 7847 break; 7848 case 'export-close': 7849 SettingsMenu.closeExport(); 7850 break; 7851 case 'custom-menu-edit': 7852 CustomMenu.showEditor(); 7853 break; 7854 } 7855 } 7856 else if (!Config.disableAll) { 7857 if (QR.enabled && t.title == 'Reply to this post') { 7858 e.preventDefault(); 7859 tid = Main.tid || t.previousElementSibling.getAttribute('href').split('#')[0].split('/')[1]; 7860 QR.quotePost(tid, !e.ctrlKey && t.textContent); 7861 } 7862 else if (Config.imageExpansion && e.which == 1 && t.parentNode 7863 && $.hasClass(t.parentNode, 'fileThumb') 7864 && t.parentNode.nodeName == 'A' 7865 && !$.hasClass(t.parentNode, 'deleted')) { 7866 7867 if (ImageExpansion.toggle(t)) { 7868 e.preventDefault(); 7869 } 7870 } 7871 else if (Config.inlineQuotes && e.which == 1 && $.hasClass(t, 'quotelink')) { 7872 if (!e.shiftKey) { 7873 QuoteInline.toggle(t, e); 7874 } 7875 else { 7876 e.preventDefault(); 7877 window.location = t.href; 7878 } 7879 } 7880 else if (Config.threadExpansion && t.parentNode && $.hasClass(t.parentNode, 'abbr')) { 7881 e.preventDefault(); 7882 ThreadExpansion.expandComment(t); 7883 } 7884 else if (Main.isMobileDevice && Config.quotePreview) { 7885 if ($.hasClass(t, 'quotelink') 7886 && (cmd = t.getAttribute('href').match(QuotePreview.regex)) 7887 && cmd[1] != 'rs') { 7888 e.preventDefault(); 7889 } 7890 } 7891 } 7892 }; 7893 7894 Main.onThreadMouseOver = function(e) { 7895 var t = e.target; 7896 7897 if (Config.quotePreview 7898 && $.hasClass(t, 'quotelink') 7899 && !$.hasClass(t, 'deadlink') 7900 && !$.hasClass(t, 'linkfade')) { 7901 QuotePreview.resolve(e.target); 7902 } 7903 else if (Config.imageHover && t.hasAttribute('data-md5') 7904 && !$.hasClass(t.parentNode, 'deleted')) { 7905 ImageHover.show(t); 7906 } 7907 else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { 7908 Media.showYTPreview(t); 7909 } 7910 else if (Config.filter && t.hasAttribute('data-filtered')) { 7911 QuotePreview.show(t, 7912 t.href ? t.parentNode.parentNode.parentNode : t.parentNode.parentNode); 7913 } 7914 }; 7915 7916 Main.onThreadMouseOut = function(e) { 7917 var t = e.target; 7918 7919 if (Config.quotePreview && $.hasClass(t, 'quotelink')) { 7920 QuotePreview.remove(t); 7921 } 7922 else if (Config.imageHover && t.hasAttribute('data-md5')) { 7923 ImageHover.hide(); 7924 } 7925 else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { 7926 Media.removeYTPreview(); 7927 } 7928 else if (Config.filter && t.hasAttribute('data-filtered')) { 7929 QuotePreview.remove(t); 7930 } 7931 }; 7932 7933 Main.linkToThread = function(tid, board, post) { 7934 return '//' + location.host + '/' 7935 + (board || Main.board) + '/thread/' 7936 + tid + (post > 0 ? ('#p' + post) : ''); 7937 }; 7938 7939 Main.addCSS = function() { 7940 var style, css = '\ 7941 body.hasDropDownNav {\ 7942 margin-top: 45px;\ 7943 }\ 7944 .extButton.threadHideButton {\ 7945 float: left;\ 7946 margin-right: 5px;\ 7947 margin-top: -1px;\ 7948 }\ 7949 .extButton.replyHideButton {\ 7950 margin-top: 1px;\ 7951 }\ 7952 div.op > span .postHideButtonCollapsed {\ 7953 margin-right: 1px;\ 7954 }\ 7955 .dropDownNav #boardNavMobile, {\ 7956 display: block !important;\ 7957 }\ 7958 .extPanel {\ 7959 border: 1px solid rgba(0, 0, 0, 0.20);\ 7960 }\ 7961 .tomorrow .extPanel {\ 7962 border: 1px solid #111;\ 7963 }\ 7964 .extButton,\ 7965 img.pointer {\ 7966 width: 18px;\ 7967 height: 18px;\ 7968 }\ 7969 .extControls {\ 7970 display: inline;\ 7971 margin-left: 5px;\ 7972 }\ 7973 .extButton {\ 7974 cursor: pointer;\ 7975 margin-bottom: -4px;\ 7976 }\ 7977 .trashIcon {\ 7978 width: 16px;\ 7979 height: 16px;\ 7980 margin-bottom: -2px;\ 7981 margin-left: 5px;\ 7982 }\ 7983 .threadUpdateStatus {\ 7984 margin-left: 0.5ex;\ 7985 }\ 7986 .futaba_new .stub,\ 7987 .burichan_new .stub {\ 7988 line-height: 1;\ 7989 padding-bottom: 1px;\ 7990 }\ 7991 .stub .extControls,\ 7992 .stub .wbtn,\ 7993 .stub input {\ 7994 display: none;\ 7995 }\ 7996 .stub .threadHideButton {\ 7997 float: none;\ 7998 margin-right: 2px;\ 7999 }\ 8000 div.post div.postInfo {\ 8001 width: auto;\ 8002 display: inline;\ 8003 }\ 8004 .right {\ 8005 float: right;\ 8006 }\ 8007 .center {\ 8008 display: block;\ 8009 margin: auto;\ 8010 }\ 8011 .pointer {\ 8012 cursor: pointer;\ 8013 }\ 8014 .drag {\ 8015 cursor: move !important;\ 8016 user-select: none !important;\ 8017 -moz-user-select: none !important;\ 8018 -webkit-user-select: none !important;\ 8019 }\ 8020 #quickReport,\ 8021 #quickReply {\ 8022 display: block;\ 8023 position: fixed;\ 8024 padding: 2px;\ 8025 font-size: 10pt;\ 8026 }\ 8027 #qrepHeader,\ 8028 #qrHeader {\ 8029 text-align: center;\ 8030 margin-bottom: 1px;\ 8031 padding: 0;\ 8032 height: 18px;\ 8033 line-height: 18px;\ 8034 }\ 8035 #qrepClose,\ 8036 #qrClose {\ 8037 float: right;\ 8038 }\ 8039 #quickReport iframe {\ 8040 overflow: hidden;\ 8041 }\ 8042 #quickReport {\ 8043 height: 190px;\ 8044 }\ 8045 #qrForm > div {\ 8046 clear: both;\ 8047 }\ 8048 #quickReply input[type="text"],\ 8049 #quickReply textarea,\ 8050 #quickReply #recaptcha_response_field {\ 8051 border: 1px solid #aaa;\ 8052 font-family: arial,helvetica,sans-serif;\ 8053 font-size: 10pt;\ 8054 outline: medium none;\ 8055 width: 296px;\ 8056 padding: 2px;\ 8057 margin: 0 0 1px 0;\ 8058 }\ 8059 #quickReply textarea {\ 8060 min-width: 296px;\ 8061 float: left;\ 8062 }\ 8063 #quickReply input::-moz-placeholder,\ 8064 #quickReply textarea::-moz-placeholder {\ 8065 color: #aaa !important;\ 8066 opacity: 1 !important;\ 8067 }\ 8068 #quickReply input[type="submit"] {\ 8069 width: 83px;\ 8070 margin: 0;\ 8071 font-size: 10pt;\ 8072 float: left;\ 8073 }\ 8074 #quickReply #qrCapField {\ 8075 display: block;\ 8076 margin-top: 1px;\ 8077 }\ 8078 #qrCaptcha {\ 8079 width: 300px;\ 8080 height: 53px;\ 8081 cursor: pointer;\ 8082 border: 1px solid #aaa;\ 8083 display: block;\ 8084 }\ 8085 #quickReply input.presubmit {\ 8086 margin-right: 1px;\ 8087 width: 212px;\ 8088 float: left;\ 8089 }\ 8090 #qrFile {\ 8091 width: 215px;\ 8092 margin-right: 5px;\ 8093 }\ 8094 .qrRealFile {\ 8095 position: absolute;\ 8096 left: 0;\ 8097 visibility: hidden;\ 8098 }\ 8099 .yotsuba_new #qrFile {\ 8100 color:black;\ 8101 }\ 8102 #qrSpoiler {\ 8103 display: inline;\ 8104 }\ 8105 #qrError {\ 8106 width: 292px;\ 8107 display: none;\ 8108 font-family: monospace;\ 8109 background-color: #E62020;\ 8110 font-size: 12px;\ 8111 color: white;\ 8112 padding: 3px 5px;\ 8113 text-shadow: 0 1px rgba(0, 0, 0, 0.20);\ 8114 clear: both;\ 8115 }\ 8116 #qrError a:hover,\ 8117 #qrError a {\ 8118 color: white !important;\ 8119 text-decoration: underline;\ 8120 }\ 8121 #twHeader {\ 8122 font-weight: bold;\ 8123 text-align: center;\ 8124 height: 17px;\ 8125 }\ 8126 .futaba_new #twHeader,\ 8127 .burichan_new #twHeader {\ 8128 line-height: 1;\ 8129 }\ 8130 #twPrune {\ 8131 margin-left: 3px;\ 8132 margin-top: -1px;\ 8133 }\ 8134 #twClose {\ 8135 float: left;\ 8136 margin-top: -1px;\ 8137 }\ 8138 #threadWatcher {\ 8139 max-width: 265px;\ 8140 display: block;\ 8141 position: absolute;\ 8142 padding: 3px;\ 8143 }\ 8144 #watchList {\ 8145 margin: 0;\ 8146 padding: 0;\ 8147 user-select: none;\ 8148 -moz-user-select: none;\ 8149 -webkit-user-select: none;\ 8150 }\ 8151 #watchList li:first-child {\ 8152 margin-top: 3px;\ 8153 padding-top: 2px;\ 8154 border-top: 1px solid rgba(0, 0, 0, 0.20);\ 8155 }\ 8156 .photon #watchList li:first-child {\ 8157 border-top: 1px solid #ccc;\ 8158 }\ 8159 .yotsuba_new #watchList li:first-child {\ 8160 border-top: 1px solid #d9bfb7;\ 8161 }\ 8162 .yotsuba_b_new #watchList li:first-child {\ 8163 border-top: 1px solid #b7c5d9;\ 8164 }\ 8165 .tomorrow #watchList li:first-child {\ 8166 border-top: 1px solid #111;\ 8167 }\ 8168 #watchList a {\ 8169 text-decoration: none;\ 8170 }\ 8171 #watchList li {\ 8172 overflow: hidden;\ 8173 white-space: nowrap;\ 8174 text-overflow: ellipsis;\ 8175 }\ 8176 div.post div.image-expanded {\ 8177 display: table;\ 8178 }\ 8179 div.op div.file .image-expanded-anti {\ 8180 margin-left: -3px;\ 8181 }\ 8182 #quote-preview {\ 8183 display: block;\ 8184 position: absolute;\ 8185 top: 0;\ 8186 padding: 3px 6px 6px 3px;\ 8187 margin: 0;\ 8188 }\ 8189 #quote-preview .dateTime {\ 8190 white-space: nowrap;\ 8191 }\ 8192 .yotsuba_new #quote-preview.highlight,\ 8193 .yotsuba_b_new #quote-preview.highlight {\ 8194 border-width: 1px 2px 2px 1px !important;\ 8195 border-style: solid !important;\ 8196 }\ 8197 .yotsuba_new #quote-preview.highlight {\ 8198 border-color: #D99F91 !important;\ 8199 }\ 8200 .yotsuba_b_new #quote-preview.highlight {\ 8201 border-color: #BA9DBF !important;\ 8202 }\ 8203 .yotsuba_b_new .highlight-anti,\ 8204 .burichan_new .highlight-anti {\ 8205 border-width: 1px !important;\ 8206 background-color: #bfa6ba !important;\ 8207 }\ 8208 .yotsuba_new .highlight-anti,\ 8209 .futaba_new .highlight-anti {\ 8210 background-color: #e8a690 !important;\ 8211 }\ 8212 .tomorrow .highlight-anti {\ 8213 background-color: #111 !important;\ 8214 border-color: #111;\ 8215 }\ 8216 .photon .highlight-anti {\ 8217 background-color: #bbb !important;\ 8218 }\ 8219 .op.inlined {\ 8220 display: block;\ 8221 }\ 8222 #quote-preview .inlined,\ 8223 #quote-preview .postMenuBtn,\ 8224 #quote-preview .extButton,\ 8225 #quote-preview .extControls {\ 8226 display: none;\ 8227 }\ 8228 .hasNewReplies {\ 8229 font-weight: bold;\ 8230 }\ 8231 .archivelink {\ 8232 opacity: 0.5;\ 8233 }\ 8234 .deadlink {\ 8235 text-decoration: line-through !important;\ 8236 }\ 8237 div.backlink {\ 8238 font-size: 0.8em !important;\ 8239 display: inline;\ 8240 padding: 0;\ 8241 padding-left: 5px;\ 8242 }\ 8243 .backlink.mobile {\ 8244 padding: 3px 5px;\ 8245 display: block;\ 8246 clear: both;\ 8247 line-height: 2;\ 8248 }\ 8249 .op .backlink.mobile,\ 8250 #quote-preview .backlink.mobile {\ 8251 display: none !important;\ 8252 }\ 8253 .backlink.mobile .quoteLink {\ 8254 padding-right: 2px;\ 8255 }\ 8256 .backlink span {\ 8257 padding: 0;\ 8258 }\ 8259 .burichan_new .backlink a,\ 8260 .yotsuba_b_new .backlink a {\ 8261 color: #34345C !important;\ 8262 }\ 8263 .burichan_new .backlink a:hover,\ 8264 .yotsuba_b_new .backlink a:hover {\ 8265 color: #dd0000 !important;\ 8266 }\ 8267 .expbtn {\ 8268 margin-right: 3px;\ 8269 margin-left: 2px;\ 8270 }\ 8271 .tCollapsed .rExpanded {\ 8272 display: none;\ 8273 }\ 8274 #stickyNav {\ 8275 position: fixed;\ 8276 font-size: 0;\ 8277 }\ 8278 #stickyNav img {\ 8279 vertical-align: middle;\ 8280 }\ 8281 .tu-error {\ 8282 color: red;\ 8283 }\ 8284 .topPageNav {\ 8285 position: absolute;\ 8286 }\ 8287 .yotsuba_b_new .topPageNav {\ 8288 border-top: 1px solid rgba(255, 255, 255, 0.25);\ 8289 border-left: 1px solid rgba(255, 255, 255, 0.25);\ 8290 }\ 8291 .newPostsMarker:not(#quote-preview) {\ 8292 box-shadow: 0 3px red;\ 8293 }\ 8294 #toggleMsgBtn {\ 8295 float: left;\ 8296 margin-bottom: 6px;\ 8297 }\ 8298 .panelHeader {\ 8299 font-weight: bold;\ 8300 font-size: 16px;\ 8301 text-align: center;\ 8302 margin-bottom: 5px;\ 8303 margin-top: 5px;\ 8304 padding-bottom: 5px;\ 8305 border-bottom: 1px solid rgba(0, 0, 0, 0.20);\ 8306 }\ 8307 .yotsuba_new .panelHeader {\ 8308 border-bottom: 1px solid #d9bfb7;\ 8309 }\ 8310 .yotsuba_b_new .panelHeader {\ 8311 border-bottom: 1px solid #b7c5d9;\ 8312 }\ 8313 .tomorrow .panelHeader {\ 8314 border-bottom: 1px solid #111;\ 8315 }\ 8316 .panelHeader span {\ 8317 position: absolute;\ 8318 right: 5px;\ 8319 top: 5px;\ 8320 }\ 8321 .UIMenu,\ 8322 .UIPanel {\ 8323 position: fixed;\ 8324 width: 100%;\ 8325 height: 100%;\ 8326 z-index: 9002;\ 8327 top: 0;\ 8328 left: 0;\ 8329 }\ 8330 .UIPanel {\ 8331 line-height: 14px;\ 8332 font-size: 14px;\ 8333 background-color: rgba(0, 0, 0, 0.25);\ 8334 }\ 8335 .UIPanel:after {\ 8336 display: inline-block;\ 8337 height: 100%;\ 8338 vertical-align: middle;\ 8339 content: "";\ 8340 }\ 8341 .UIPanel > div {\ 8342 -moz-box-sizing: border-box;\ 8343 box-sizing: border-box;\ 8344 display: inline-block;\ 8345 height: auto;\ 8346 max-height: 100%;\ 8347 position: relative;\ 8348 width: 400px;\ 8349 left: 50%;\ 8350 margin-left: -200px;\ 8351 overflow: auto;\ 8352 box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ 8353 vertical-align: middle;\ 8354 }\ 8355 #settingsMenu > div {\ 8356 top: 25px;;\ 8357 vertical-align: top;\ 8358 max-height: 85%;\ 8359 }\ 8360 .extPanel input[type="text"],\ 8361 .extPanel textarea {\ 8362 border: 1px solid #AAA;\ 8363 outline: none;\ 8364 }\ 8365 .UIPanel .center {\ 8366 margin-bottom: 5px;\ 8367 }\ 8368 .UIPanel button {\ 8369 display: inline-block;\ 8370 margin-right: 5px;\ 8371 }\ 8372 .UIPanel code {\ 8373 background-color: #eee;\ 8374 color: #000000;\ 8375 padding: 1px 4px;\ 8376 font-size: 12px;\ 8377 }\ 8378 .UIPanel ul {\ 8379 list-style: none;\ 8380 padding: 0;\ 8381 margin: 0 0 10px;\ 8382 }\ 8383 .UIPanel .export-field {\ 8384 width: 385px;\ 8385 }\ 8386 #settingsMenu label input {\ 8387 margin-right: 5px;\ 8388 }\ 8389 .tomorrow #settingsMenu ul {\ 8390 border-bottom: 1px solid #282a2e;\ 8391 }\ 8392 .settings-off {\ 8393 padding-left: 3px;\ 8394 }\ 8395 .settings-cat-lbl {\ 8396 font-weight: bold;\ 8397 margin: 10px 0 5px;\ 8398 padding-left: 5px;\ 8399 }\ 8400 .settings-cat-lbl img {\ 8401 vertical-align: text-bottom;\ 8402 margin-right: 5px;\ 8403 cursor: pointer;\ 8404 width: 18px;\ 8405 height: 18px;\ 8406 }\ 8407 .settings-tip {\ 8408 font-size: 0.85em;\ 8409 margin: 2px 0 5px 0;\ 8410 padding-left: 23px;\ 8411 }\ 8412 #settings-exp-all {\ 8413 padding-left: 7px;\ 8414 text-align: center;\ 8415 }\ 8416 #settingsMenu .settings-cat {\ 8417 display: none;\ 8418 margin-left: 3px;\ 8419 }\ 8420 #customCSSMenu textarea {\ 8421 display: block;\ 8422 max-width: 100%;\ 8423 min-width: 100%;\ 8424 -moz-box-sizing: border-box;\ 8425 box-sizing: border-box;\ 8426 height: 200px;\ 8427 margin: 0 0 5px;\ 8428 font-family: monospace;\ 8429 }\ 8430 #customCSSMenu .right,\ 8431 #settingsMenu .right {\ 8432 margin-top: 2px;\ 8433 }\ 8434 #settingsMenu label {\ 8435 display: inline-block;\ 8436 user-select: none;\ 8437 -moz-user-select: none;\ 8438 -webkit-user-select: none;\ 8439 }\ 8440 #filtersHelp > div {\ 8441 width: 600px;\ 8442 left: 50%;\ 8443 margin-left: -300px;\ 8444 }\ 8445 #filtersHelp h4 {\ 8446 font-size: 15px;\ 8447 margin: 20px 0 0 10px;\ 8448 }\ 8449 #filtersHelp h4:before {\ 8450 content: "ยป";\ 8451 margin-right: 3px;\ 8452 }\ 8453 #filtersHelp ul {\ 8454 padding: 0;\ 8455 margin: 10px;\ 8456 }\ 8457 #filtersHelp li {\ 8458 padding: 3px 0;\ 8459 list-style: none;\ 8460 }\ 8461 #filtersMenu table {\ 8462 width: 100%;\ 8463 }\ 8464 #filtersMenu th {\ 8465 font-size: 12px;\ 8466 }\ 8467 #filtersMenu tbody {\ 8468 text-align: center;\ 8469 }\ 8470 #filtersMenu select,\ 8471 #filtersMenu .fPattern,\ 8472 #filtersMenu .fBoards,\ 8473 #palette-custom-input {\ 8474 padding: 1px;\ 8475 font-size: 11px;\ 8476 }\ 8477 #filtersMenu select {\ 8478 width: 75px;\ 8479 }\ 8480 #filtersMenu tfoot td {\ 8481 padding-top: 10px;\ 8482 }\ 8483 #keybindsHelp li {\ 8484 padding: 3px 5px;\ 8485 }\ 8486 .fPattern {\ 8487 width: 110px;\ 8488 }\ 8489 .fBoards {\ 8490 width: 25px;\ 8491 }\ 8492 .fColor {\ 8493 width: 60px;\ 8494 }\ 8495 .fDel {\ 8496 font-size: 16px;\ 8497 }\ 8498 .filter-preview {\ 8499 cursor: default;\ 8500 margin-left: 3px;\ 8501 }\ 8502 #quote-preview iframe,\ 8503 #quote-preview .filter-preview {\ 8504 display: none;\ 8505 }\ 8506 .post-hidden .extButton,\ 8507 .post-hidden:not(#quote-preview) .postInfo {\ 8508 opacity: 0.5;\ 8509 }\ 8510 .post-hidden:not(.thread) .postInfo {\ 8511 padding-left: 5px;\ 8512 }\ 8513 .post-hidden:not(#quote-preview) input,\ 8514 .post-hidden:not(#quote-preview) .replyContainer,\ 8515 .post-hidden:not(#quote-preview) .summary,\ 8516 .post-hidden:not(#quote-preview) .op .file,\ 8517 .post-hidden:not(#quote-preview) .file,\ 8518 .post-hidden .wbtn,\ 8519 .post-hidden .postNum span,\ 8520 .post-hidden:not(#quote-preview) .backlink,\ 8521 div.post-hidden:not(#quote-preview) div.file,\ 8522 div.post-hidden:not(#quote-preview) blockquote.postMessage {\ 8523 display: none;\ 8524 }\ 8525 .click-me {\ 8526 border-radius: 5px;\ 8527 margin-top: 5px;\ 8528 padding: 2px 5px;\ 8529 position: absolute;\ 8530 font-weight: bold;\ 8531 z-index: 2;\ 8532 white-space: nowrap;\ 8533 }\ 8534 .yotsuba_new .click-me,\ 8535 .futaba_new .click-me {\ 8536 color: #800000;\ 8537 background-color: #F0E0D6;\ 8538 border: 2px solid #D9BFB7;\ 8539 }\ 8540 .yotsuba_b_new .click-me,\ 8541 .burichan_new .click-me {\ 8542 color: #000;\ 8543 background-color: #D6DAF0;\ 8544 border: 2px solid #B7C5D9;\ 8545 }\ 8546 .tomorrow .click-me {\ 8547 color: #C5C8C6;\ 8548 background-color: #282A2E;\ 8549 border: 2px solid #111;\ 8550 }\ 8551 .photon .click-me {\ 8552 color: #333;\ 8553 background-color: #ddd;\ 8554 border: 2px solid #ccc;\ 8555 }\ 8556 .click-me:before {\ 8557 content: "";\ 8558 border-width: 0 6px 6px;\ 8559 border-style: solid;\ 8560 left: 50%;\ 8561 margin-left: -6px;\ 8562 position: absolute;\ 8563 width: 0;\ 8564 height: 0;\ 8565 top: -6px;\ 8566 }\ 8567 .yotsuba_new .click-me:before,\ 8568 .futaba_new .click-me:before {\ 8569 border-color: #D9BFB7 transparent;\ 8570 }\ 8571 .yotsuba_b_new .click-me:before,\ 8572 .burichan_new .click-me:before {\ 8573 border-color: #B7C5D9 transparent;\ 8574 }\ 8575 .tomorrow .click-me:before {\ 8576 border-color: #111 transparent;\ 8577 }\ 8578 .photon .click-me:before {\ 8579 border-color: #ccc transparent;\ 8580 }\ 8581 .click-me:after {\ 8582 content: "";\ 8583 border-width: 0 4px 4px;\ 8584 top: -4px;\ 8585 display: block;\ 8586 left: 50%;\ 8587 margin-left: -4px;\ 8588 position: absolute;\ 8589 width: 0;\ 8590 height: 0;\ 8591 }\ 8592 .yotsuba_new .click-me:after,\ 8593 .futaba_new .click-me:after {\ 8594 border-color: #F0E0D6 transparent;\ 8595 border-style: solid;\ 8596 }\ 8597 .yotsuba_b_new .click-me:after,\ 8598 .burichan_new .click-me:after {\ 8599 border-color: #D6DAF0 transparent;\ 8600 border-style: solid;\ 8601 }\ 8602 .tomorrow .click-me:after {\ 8603 border-color: #282A2E transparent;\ 8604 border-style: solid;\ 8605 }\ 8606 .photon .click-me:after {\ 8607 border-color: #DDD transparent;\ 8608 border-style: solid;\ 8609 }\ 8610 #image-hover {\ 8611 position: fixed;\ 8612 max-width: 100%;\ 8613 max-height: 100%;\ 8614 top: 0px;\ 8615 right: 0px;\ 8616 z-index: 9002;\ 8617 }\ 8618 .thread-stats {\ 8619 float: right;\ 8620 margin-right: 5px;\ 8621 cursor: default;\ 8622 }\ 8623 .compact .thread {\ 8624 max-width: 75%;\ 8625 }\ 8626 .dotted {\ 8627 text-decoration: none;\ 8628 border-bottom: 1px dashed;\ 8629 }\ 8630 .linkfade {\ 8631 opacity: 0.5;\ 8632 }\ 8633 #quote-preview .linkfade {\ 8634 opacity: 1.0;\ 8635 }\ 8636 kbd {\ 8637 background-color: #f7f7f7;\ 8638 color: black;\ 8639 border: 1px solid #ccc;\ 8640 border-radius: 3px 3px 3px 3px;\ 8641 box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset;\ 8642 font-family: monospace;\ 8643 font-size: 11px;\ 8644 line-height: 1.4;\ 8645 padding: 0 5px;\ 8646 }\ 8647 .deleted {\ 8648 opacity: 0.66;\ 8649 }\ 8650 .noPictures a.fileThumb img:not(.expanded-thumb) {\ 8651 opacity: 0;\ 8652 }\ 8653 .noPictures.futaba_new a.fileThumb,\ 8654 .noPictures.yotsuba_new a.fileThumb {\ 8655 border: 1px solid #800;\ 8656 }\ 8657 .noPictures.burichan_new a.fileThumb,\ 8658 .noPictures.yotsuba_b_new a.fileThumb {\ 8659 border: 1px solid #34345C;\ 8660 }\ 8661 .noPictures.tomorrow a.fileThumb:not(.expanded-thumb) {\ 8662 border: 1px solid #C5C8C6;\ 8663 }\ 8664 .noPictures.photon a.fileThumb:not(.expanded-thumb) {\ 8665 border: 1px solid #004A99;\ 8666 }\ 8667 .spinner {\ 8668 margin-top: 2px;\ 8669 padding: 3px;\ 8670 display: table;\ 8671 }\ 8672 #settings-presets {\ 8673 position: relative;\ 8674 top: -1px;\ 8675 }\ 8676 #colorpicker { \ 8677 position: fixed;\ 8678 text-align: center;\ 8679 }\ 8680 .colorbox {\ 8681 font-size: 10px;\ 8682 width: 16px;\ 8683 height: 16px;\ 8684 line-height: 17px;\ 8685 display: inline-block;\ 8686 text-align: center;\ 8687 background-color: #fff;\ 8688 border: 1px solid #aaa;\ 8689 text-decoration: none;\ 8690 color: #000;\ 8691 cursor: pointer;\ 8692 vertical-align: top;\ 8693 }\ 8694 #palette-custom-input {\ 8695 vertical-align: top;\ 8696 width: 45px;\ 8697 margin-right: 2px;\ 8698 }\ 8699 #qrDummyFile {\ 8700 float: left;\ 8701 margin-right: 5px;\ 8702 width: 220px;\ 8703 cursor: default;\ 8704 -moz-user-select: none;\ 8705 -webkit-user-select: none;\ 8706 -ms-user-select: none;\ 8707 user-select: none;\ 8708 white-space: nowrap;\ 8709 text-overflow: ellipsis;\ 8710 overflow: hidden;\ 8711 }\ 8712 #qrDummyFileLabel {\ 8713 margin-left: 3px;\ 8714 }\ 8715 .depageNumber {\ 8716 position: absolute;\ 8717 right: 5px;\ 8718 }\ 8719 .depagerEnabled .depagelink {\ 8720 font-weight: bold;\ 8721 }\ 8722 .depagerEnabled strong {\ 8723 font-weight: normal;\ 8724 }\ 8725 .depagelink {\ 8726 display: inline-block;\ 8727 padding: 4px 0;\ 8728 cursor: pointer;\ 8729 text-decoration: none;\ 8730 }\ 8731 .burichan_new .depagelink,\ 8732 .futaba_new .depagelink {\ 8733 text-decoration: underline;\ 8734 }\ 8735 #customMenuBox {\ 8736 margin: 0 auto 5px auto;\ 8737 width: 385px;\ 8738 display: block;\ 8739 }\ 8740 .preview-summary {\ 8741 display: block;\ 8742 }\ 8743 #swf-embed-header {\ 8744 padding: 0 0 0 3px;\ 8745 font-weight: normal;\ 8746 height: 20px;\ 8747 line-height: 20px;\ 8748 }\ 8749 .yotsuba_new #swf-embed-header,\ 8750 .yotsuba_b_new #swf-embed-header {\ 8751 height: 18px;\ 8752 line-height: 18px;\ 8753 }\ 8754 #swf-embed-close {\ 8755 position: absolute;\ 8756 right: 0;\ 8757 top: 1px;\ 8758 }\ 8759 .open-qr-wrap {\ 8760 text-align: center;\ 8761 width: 200px;\ 8762 position: absolute;\ 8763 margin-left: 50%;\ 8764 left: -100px;\ 8765 }\ 8766 .postMenuBtn {\ 8767 margin-left: 5px;\ 8768 text-decoration: none;\ 8769 line-height: 1em;\ 8770 display: inline-block;\ 8771 -webkit-transition: -webkit-transform 0.1s;\ 8772 -moz-transition: -moz-transform 0.1s;\ 8773 transition: transform 0.1s;\ 8774 width: 1em;\ 8775 height: 1em;\ 8776 text-align: center;\ 8777 outline: none;\ 8778 opacity: 0.8;\ 8779 }\ 8780 .postMenuBtn:hover{\ 8781 opacity: 1;\ 8782 }\ 8783 .yotsuba_new .postMenuBtn,\ 8784 .futaba_new .postMenuBtn {\ 8785 color: #000080;\ 8786 }\ 8787 .tomorrow .postMenuBtn {\ 8788 color: #5F89AC !important;\ 8789 }\ 8790 .tomorrow .postMenuBtn:hover {\ 8791 color: #81a2be !important;\ 8792 }\ 8793 .photon .postMenuBtn {\ 8794 color: #FF6600 !important;\ 8795 }\ 8796 .photon .postMenuBtn:hover {\ 8797 color: #FF3300 !important;\ 8798 }\ 8799 .menuOpen {\ 8800 -webkit-transform: rotate(90deg);\ 8801 -moz-transform: rotate(90deg);\ 8802 -ms-transform: rotate(90deg);\ 8803 transform: rotate(90deg);\ 8804 }\ 8805 .settings-sub label:before {\ 8806 border-bottom: 1px solid;\ 8807 border-left: 1px solid;\ 8808 content: " ";\ 8809 display: inline-block;\ 8810 height: 8px;\ 8811 margin-bottom: 5px;\ 8812 width: 8px;\ 8813 }\ 8814 .settings-sub {\ 8815 margin-left: 25px;\ 8816 }\ 8817 .settings-tip.settings-sub {\ 8818 padding-left: 32px;\ 8819 }\ 8820 .centeredThreads .opContainer {\ 8821 display: block;\ 8822 }\ 8823 .centeredThreads .postContainer {\ 8824 margin: auto;\ 8825 width: 75%;\ 8826 }\ 8827 .centeredThreads .sideArrows {\ 8828 display: none;\ 8829 }\ 8830 .centre-exp {\ 8831 width: auto !important;\ 8832 clear: both;\ 8833 }\ 8834 .centeredThreads .expandedWebm {\ 8835 float: none;\ 8836 }\ 8837 .centeredThreads .summary {\ 8838 margin-left: 12.5%;\ 8839 display: block;\ 8840 }\ 8841 .centre-exp div.op{\ 8842 display: table;\ 8843 }\ 8844 #yt-preview { position: absolute; }\ 8845 #yt-preview img { display: block; }\ 8846 \ 8847 @media only screen and (max-width: 480px) {\ 8848 #threadWatcher {\ 8849 max-width: none;\ 8850 padding: 3px 0;\ 8851 left: 0;\ 8852 width: 100%;\ 8853 border-left: none;\ 8854 border-right: none;\ 8855 }\ 8856 #watchList {\ 8857 padding: 0 10px;\ 8858 }\ 8859 .btn-row {\ 8860 margin-top: 5px;\ 8861 }\ 8862 .image-expanded .mFileInfo {\ 8863 display: none !important;\ 8864 }\ 8865 .mobile-report {\ 8866 float: right;\ 8867 font-size: 11px;\ 8868 margin-bottom: 3px;\ 8869 margin-left: 10px;\ 8870 }\ 8871 .mobile-report:after {\ 8872 content: "]";\ 8873 }\ 8874 .mobile-report:before {\ 8875 content: "[";\ 8876 }\ 8877 .nws .mobile-report:after {\ 8878 color: #800000;\ 8879 }\ 8880 .nws .mobile-report:before {\ 8881 color: #800000;\ 8882 }\ 8883 .ws .mobile-report {\ 8884 color: #34345C;\ 8885 }\ 8886 .nws .mobile-report {\ 8887 color:#0000EE;\ 8888 }\ 8889 .reply .mobile-report {\ 8890 margin: 5px 5px 0 5px;\ 8891 }\ 8892 .postLink .mobileHideButton {\ 8893 margin-right: 3px;\ 8894 }\ 8895 .board .mobile-hr-hidden {\ 8896 margin-top: 10px !important;\ 8897 }\ 8898 .board > .mobileHideButton {\ 8899 margin-top: -20px !important;\ 8900 }\ 8901 .board > .mobileHideButton:first-child {\ 8902 margin-top: 10px !important;\ 8903 }\ 8904 .extButton.threadHideButton {\ 8905 float: none;\ 8906 margin: 0;\ 8907 margin-bottom: 5px;\ 8908 }\ 8909 .mobile-post-hidden {\ 8910 display: none;\ 8911 }\ 8912 #toggleMsgBtn {\ 8913 display: none;\ 8914 }\ 8915 .mobile-tu-status {\ 8916 height: 20px;\ 8917 line-height: 20px;\ 8918 }\ 8919 .mobile-tu-show {\ 8920 width: 150px;\ 8921 margin: auto;\ 8922 display: block;\ 8923 text-align: center;\ 8924 }\ 8925 .button input {\ 8926 margin: 0 3px 0 0;\ 8927 position: relative;\ 8928 top: -2px;\ 8929 border-radius: 0;\ 8930 height: 10px;\ 8931 width: 10px;\ 8932 }\ 8933 .UIPanel > div {\ 8934 width: 320px;\ 8935 margin-left: -160px;\ 8936 }\ 8937 .UIPanel .export-field {\ 8938 width: 300px;\ 8939 }\ 8940 .yotsuba_new #quote-preview.highlight,\ 8941 #quote-preview {\ 8942 border-width: 1px !important;\ 8943 }\ 8944 .yotsuba_new #quote-preview.highlight {\ 8945 border-color: #D9BFB7 !important;\ 8946 }\ 8947 #quickReply input[type="text"],\ 8948 #quickReply textarea,\ 8949 .extPanel input[type="text"],\ 8950 .extPanel textarea {\ 8951 font-size: 16px;\ 8952 }\ 8953 #quickReply {\ 8954 position: absolute;\ 8955 left: 50%;\ 8956 margin-left: -154px;\ 8957 }\ 8958 }\ 8959 '; 8960 8961 style = document.createElement('style'); 8962 style.setAttribute('type', 'text/css'); 8963 style.textContent = css; 8964 document.head.appendChild(style); 8965 }; 8966 8967 Main.init();