github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/public/global.js (about) 1 'use strict'; 2 var formVars={}; 3 var alertMapping={}; 4 var alertList=[]; 5 var alertCount=0; 6 var moreTopicCount=0; 7 var conn=false; 8 var selectedTopics=[]; 9 var attachItemCallback=()=>{} 10 var quoteItemCallback=()=>{} 11 var baseTitle=document.title; 12 var wsBackoff=0; 13 var noAlerts=false; 14 15 // Topic move 16 var forumToMoveTo=0; 17 18 function pushNotice(msg) { 19 let aBox = document.getElementsByClassName("alertbox")[0]; 20 let n = document.createElement('div'); 21 n.innerHTML = Tmpl_notice(msg).trim(); 22 aBox.appendChild(n); 23 runInitHook("after_notice"); 24 } 25 26 // TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts 27 function ajaxError(xhr,status,e) { 28 log("The AJAX request failed"); 29 log("xhr",xhr); 30 log("status",status); 31 log("e",e); 32 if(status=="parsererror") log("The server didn't respond with a valid JSON response"); 33 console.trace(); 34 } 35 36 function postLink(ev) { 37 ev.preventDefault(); 38 let formAction = $(ev.target).closest('a').attr("href"); 39 $.ajax({ url:formAction, type:"POST", dataType:"json", error: ajaxError, data: {js: 1} }); 40 } 41 42 function bindToAlerts() { 43 log("bindToAlerts"); 44 $(".alertItem.withAvatar a").unbind("click"); 45 $(".alertItem.withAvatar a").click(function(ev) { 46 ev.stopPropagation(); 47 ev.preventDefault(); 48 $.ajax({ 49 url: "/api/?a=set&m=dismiss-alert", 50 type: "POST", 51 dataType: "json", 52 data: { id: $(this).attr("data-asid") }, 53 //error: ajaxError, 54 success: () => { 55 window.location.href = this.getAttribute("href"); 56 } 57 }); 58 }); 59 } 60 61 function addAlert(msg,notice=false) { 62 var mmsg = msg.msg; 63 if(mmsg[0]==".") mmsg = phraseBox["alerts"]["alerts"+mmsg]; 64 if("sub" in msg) { 65 for(var i=0; i<msg.sub.length; i++) mmsg = mmsg.replace("\{"+i+"\}",msg.sub[i]); 66 } 67 68 let aItem = Tmpl_alert({ 69 ASID: msg.id, 70 Path: msg.path, 71 Avatar: msg.img || "", 72 Message: mmsg 73 }) 74 //alertMapping[msg.id] = aItem; 75 let div = document.createElement('div'); 76 div.innerHTML = aItem.trim(); 77 alertMapping[msg.id] = div.firstChild; 78 alertList.push(msg.id); 79 80 if(notice) { 81 // TODO: Add some sort of notification queue to avoid flooding the end-user with notices? 82 // TODO: Use the site name instead of "Something Happened" 83 if(Notification.permission==="granted") { 84 var n = new Notification("Something Happened",{ 85 body: mmsg, 86 icon: msg.img, 87 }); 88 setTimeout(n.close.bind(n),8000); 89 } 90 } 91 92 runInitHook("after_add_alert"); 93 } 94 95 function updateAlertList(menuAlerts) { 96 log("enter updateAlertList"); 97 log("alertList:",alertList); 98 log("alertMapping:",alertMapping); 99 log("alertCount:",alertCount); 100 let alertListNode = menuAlerts.getElementsByClassName("alertList")[0]; 101 let alertCounterNode = menuAlerts.getElementsByClassName("alert_counter")[0]; 102 alertCounterNode.textContent = "0"; 103 104 alertListNode.innerHTML = ""; 105 let any = false; 106 let j = 0; 107 for(var i=0; i<alertList.length && j<8; i++) { 108 any = true; 109 alertListNode.appendChild(alertMapping[alertList[i]]); 110 //outList += alertMapping[alertList[i]]; 111 j++; 112 } 113 if(!any) alertListNode.innerHTML = "<div class='alertItem'>"+phraseBox["alerts"]["alerts.no_alerts"]+"</div>"; 114 115 if(alertCount!=0) { 116 alertCounterNode.textContent = alertCount; 117 menuAlerts.classList.add("has_alerts"); 118 let nTitle = "("+alertCount+") "+baseTitle; 119 if(document.title!=nTitle) document.title = nTitle; 120 } else { 121 menuAlerts.classList.remove("has_alerts"); 122 if(document.title!=baseTitle) document.title = baseTitle; 123 } 124 125 bindToAlerts(); 126 log("alertCount",alertCount) 127 runInitHook("after_update_alert_list",alertCount); 128 } 129 130 function setAlertError(menuAlerts,msg) { 131 let n = menuAlerts.getElementsByClassName("alertList")[0]; 132 n.innerHTML = "<div class='alertItem'>"+msg+"</div>"; 133 } 134 135 var alertsInitted = false; 136 var lastTc = 0; 137 function loadAlerts(menuAlerts,eTc=false) { 138 if(!alertsInitted) return; 139 let tc = ""; 140 if(eTc && lastTc!=0) tc = "&t="+lastTc+"&c="+alertCount; 141 $.ajax({ 142 type:'get', 143 dataType:'json', 144 url:'/api/?m=alerts'+tc, 145 success: data => { 146 if("errmsg" in data) { 147 setAlertError(menuAlerts,data.errmsg) 148 return; 149 } 150 if(!eTc) { 151 alertList=[]; 152 alertMapping={}; 153 } 154 if(!data.hasOwnProperty("msgs")) data = {"msgs":[],"count":alertCount,"tc":lastTc}; 155 /*else if(data.count != (alertCount + data.msgs.length)) tc = false; 156 if(eTc && lastTc!=0) { 157 for(var i in data.msgs) wsAlertEvent(data.msgs[i]); 158 } else {*/ 159 log("data",data); 160 for(var i in data.msgs) addAlert(data.msgs[i]); 161 alertCount = data.count; 162 updateAlertList(menuAlerts); 163 try { 164 localStorage.setItem("alertList",JSON.stringify(alertList)); 165 localStorage.setItem("alertMapping",JSON.stringify(alertMapping)); 166 localStorage.setItem("alertCount",alertCount); 167 } catch(e) { 168 localStorage.clear(); 169 } 170 //} 171 lastTc = data.tc; 172 }, 173 error: (magic,status,er) => { 174 let errtxt = "Unable to get the alerts"; 175 try { 176 let dat = JSON.parse(magic.responseText); 177 if("errmsg" in dat) errtxt = dat.errmsg; 178 } catch(e) { 179 log(magic.responseText); 180 log(e); 181 } 182 log("er",er); 183 setAlertError(menuAlerts,errtxt); 184 } 185 }); 186 } 187 188 function SplitN(data,ch,n) { 189 var o = []; 190 if(data.length===0) return o; 191 192 var lastI = 0; 193 var j = 0; 194 var lastN = 1; 195 for(let i=0; i<data.length; i++) { 196 if(data[i]===ch) { 197 o[j++] = data.substring(lastI,i); 198 lastI = i; 199 if(lastN===n) break; 200 lastN++; 201 } 202 } 203 if(data.length > lastI) o[o.length-1] += data.substring(lastI); 204 return o; 205 } 206 207 function wsAlertEvent(dat) { 208 log("wsAlertEvent",dat) 209 addAlert(dat,true); 210 alertCount++; 211 212 let aTmp = alertList; 213 alertList = [alertList[alertList.length-1]]; 214 aTmp = aTmp.slice(0,-1); 215 for(let i=0; i<aTmp.length; i++) alertList.push(aTmp[i]); 216 // TODO: Add support for other alert feeds like PM Alerts 217 let n = document.getElementById("general_alerts"); 218 // TODO: Make sure we update alertCount here 219 lastTc = 0; 220 updateAlertList(n/*,alist*/); 221 } 222 223 function runWebSockets(resume=false) { 224 let s = ""; 225 if(window.location.protocol == "https:") s = "s"; 226 conn = new WebSocket("ws"+s+"://" + document.location.host + "/ws/"); 227 228 conn.onerror = e => { 229 log(e); 230 } 231 232 // TODO: Sync alerts, topic list, etc. 233 conn.onopen = () => { 234 log("The WebSockets connection was opened"); 235 if(resume) conn.send("resume " + document.location.pathname + " " + Math.round((new Date()).getTime() / 1000) + '\r'); 236 else conn.send("page " + document.location.pathname + '\r'); 237 // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on 238 if(me.User.ID > 0) Notification.requestPermission(); 239 } 240 241 conn.onclose = () => { 242 conn = false; 243 log("The WebSockets connection was closed"); 244 let backoff = 0.8; 245 if(wsBackoff < 0) wsBackoff = 0; 246 else if(wsBackoff > 12) backoff = 11; 247 else if(wsBackoff > 5) backoff = 5; 248 wsBackoff++; 249 250 setTimeout(() => { 251 if(!noAlerts) { 252 let nl = document.getElementsByClassName("menu_alerts"); 253 for(var i=0; i < nl.length; i++) loadAlerts(nl[i],true); 254 } 255 runWebSockets(true); 256 }, backoff * 60 * 1000); 257 258 if(wsBackoff > 0) { 259 if(wsBackoff <= 5) setTimeout(() => wsBackoff--, 5.5 * 60 * 1000); 260 else if(wsBackoff <= 12) setTimeout(() => wsBackoff--, 11.5 * 60 * 1000); 261 else setTimeout(() => wsBackoff--, 20 * 60 * 1000); 262 } 263 } 264 265 conn.onmessage = (event) => { 266 if(!noAlerts && event.data[0] == "{") { 267 log("json message"); 268 let data = ""; 269 try { 270 data = JSON.parse(event.data); 271 } catch(e) { 272 log(e); 273 return; 274 } 275 276 if("msg" in data) wsAlertEvent(data); 277 else if("event" in data) { 278 if(data.event=="dismiss-alert"){ 279 Object.keys(alertMapping).forEach((key) => { 280 if(key!=data.id) return; 281 alertCount--; 282 let index = -1; 283 for(var i=0; i < alertList.length; i++) { 284 if(alertList[i]==key) { 285 alertList[i] = 0; 286 index = i; 287 } 288 } 289 if(index==-1) return; 290 291 for(var i = index; (i+1) < alertList.length; i++) alertList[i] = alertList[i+1]; 292 alertList.splice(alertList.length-1,1); 293 delete alertMapping[key]; 294 295 // TODO: Add support for other alert feeds like PM Alerts 296 let generalAlerts = document.getElementById("general_alerts"); 297 if(alertList.length < 8) loadAlerts(generalAlerts,true); 298 else updateAlertList(generalAlerts); 299 }); 300 } 301 } else if("Topics" in data) { 302 log("topic in data"); 303 log("data",data); 304 // TODO: Handle desyncs more gracefully? 305 // TODO: Send less unneccessary data? 306 let topic = data.Topics[0]; 307 if(topic===undefined){ 308 log("empty topic list"); 309 return; 310 } 311 if("mod" in data) { 312 topic.CanMod = data.mod==1 || data.mod[0]==1; 313 if(data.lock==1) { 314 $(".val_lock").each(function(){ 315 this.classList.remove("auto_hide"); 316 }); 317 } 318 if(data.move==1) { 319 $(".val_move").each(function(){ 320 this.classList.remove("auto_hide"); 321 }); 322 } 323 } 324 // TODO: Fix the data race where the function hasn't been loaded yet 325 let renTopic = Tmpl_topics_topic(topic); 326 $(".topic_row[data-tid='"+topic.ID+"']").addClass("ajax_topic_dupe"); 327 328 let node = $(renTopic); 329 node.addClass("new_item hide_ajax_topic"); 330 log("Prepending to topic list"); 331 $(".topic_list").prepend(node); 332 moreTopicCount++; 333 334 let blocks = document.getElementsByClassName("more_topic_block_initial"); 335 for(let i=0; i<blocks.length; i++) { 336 let block = blocks[i]; 337 block.classList.remove("more_topic_block_initial"); 338 block.classList.add("more_topic_block_active"); 339 340 log("phraseBox",phraseBox); 341 let msgBox = block.getElementsByClassName("more_topics")[0]; 342 msgBox.innerText = phraseBox["topic_list"]["topic_list.changed_topics"].replace("%d",moreTopicCount); 343 } 344 } else log("unknown message",data); 345 } 346 347 let messages = event.data.split('\r'); 348 for(var i=0; i<messages.length; i++) { 349 let message = messages[i]; 350 //log("message",message); 351 let msgblocks = SplitN(message," ",3); 352 if(msgblocks.length < 3) continue; 353 if(message.startsWith("set ")) { 354 let oldInnerHTML = document.querySelector(msgblocks[1]).innerHTML; 355 if(msgblocks[2]==oldInnerHTML) continue; 356 document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]; 357 } else if(message.startsWith("set-class ")) { 358 // Fix to stop the inspector from getting all jittery 359 let oldClassName = document.querySelector(msgblocks[1]).className; 360 if(msgblocks[2]==oldClassName) continue; 361 document.querySelector(msgblocks[1]).className = msgblocks[2]; 362 } 363 } 364 } 365 } 366 367 // TODO: Surely, there's a prettier and more elegant way of doing this? 368 function getExt(name) { 369 if(!(name.indexOf('.') > -1)) throw("This file doesn't have an extension"); 370 return name.split('.').pop(); 371 } 372 373 (() => { 374 addInitHook("pre_init", () => { 375 runInitHook("pre_global"); 376 log("before notify on alert") 377 // We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :( 378 log("noAlerts:",noAlerts); 379 if(!noAlerts) { 380 notifyOnScriptW("tmpl_alert", e => { 381 if(e!=undefined) log("failed alert? why?",e) 382 }, () => { 383 if(!Tmpl_alert) throw("tmpl func not found"); 384 addInitHook("after_phrases", () => { 385 // TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred 386 $(document).ready(() => { 387 log("checking local storage cache"); 388 alertsInitted = true; 389 let al = document.getElementsByClassName("menu_alerts"); 390 let sAlertList = localStorage.getItem("alertList"); 391 let sAlertMapping = localStorage.getItem("alertMapping"); 392 let sAlertCount = localStorage.getItem("alertCount"); 393 if(sAlertList!=null && sAlertList!="" && 394 sAlertMapping!=null && sAlertMapping!="" &&sAlertCount!=null && sAlertCount!="" && sAlertCount!="0" 395 ) { 396 log("sAlertList",sAlertList) 397 log("sAlertMapping",sAlertMapping) 398 log("sAlertCount",sAlertCount) 399 alertList = JSON.parse(sAlertList) 400 alertMapping = JSON.parse(sAlertMapping) 401 alertCount = parseInt(sAlertCount) 402 log("alertList",alertList) 403 log("alertMapping",alertMapping) 404 log("alertCount",alertCount) 405 for(var i=0; i<al.length; i++) loadAlerts(al[i],true); 406 } else for(var i=0; i<al.length; i++) loadAlerts(al[i]); 407 if(window["WebSocket"]) runWebSockets(); 408 }); 409 }); 410 }); 411 } else { 412 addInitHook("after_phrases", () => { 413 $(document).ready(() => { 414 if(window["WebSocket"]) runWebSockets(); 415 }); 416 }); 417 } 418 419 $(document).ready(mainInit); 420 }); 421 })(); 422 423 // TODO: Use these in .filter_item and pass back an item count from the backend to work with here 424 // Ported from common/parser.go 425 function PageOffset(count,page,perPage) { 426 let offset = 0; 427 let lastPage = LastPage(count, perPage) 428 if(page > 1) offset = (perPage * page) - perPage; 429 else if (page == -1) { 430 page = lastPage; 431 offset = (perPage * page) - perPage; 432 } else page = 1; 433 434 // We don't want the offset to overflow the slices, if everything's in memory 435 //if(offset >= (count - 1)) offset = 0; 436 return {Offset:offset,Page:page,LastPage:lastPage}; 437 } 438 function LastPage(count,perPage) { 439 return (count / perPage) + 1 440 } 441 function Paginate(currentPage,lastPage,maxPages) { 442 let diff = lastPage - currentPage; 443 let pre = 3; 444 if(diff < 3) pre = maxPages - diff; 445 446 let page = currentPage - pre; 447 if(page < 0) page = 0; 448 let o = []; 449 while(o.length < maxPages && page < lastPage){ 450 page++; 451 o.push(page); 452 } 453 return o; 454 } 455 456 function mainInit(){ 457 log("enter mainInit"); 458 runInitHook("start_init"); 459 460 $(".more_topics").click(ev => { 461 ev.preventDefault(); 462 let blocks = document.getElementsByClassName("more_topic_block_active"); 463 for(let i=0; i<blocks.length; i++) { 464 let bl = blocks[i]; 465 bl.classList.remove("more_topic_block_active"); 466 bl.classList.add("more_topic_block_initial"); 467 } 468 $(".ajax_topic_dupe").fadeOut("slow", function(){ 469 $(this).remove(); 470 }); 471 $(".hide_ajax_topic").removeClass("hide_ajax_topic"); // TODO: Do Fade 472 moreTopicCount = 0; 473 }) 474 475 $(".add_like,.remove_like").click(function(ev) { 476 ev.preventDefault(); 477 //$(this).unbind("click"); 478 let target = this.closest("a").getAttribute("href"); 479 log("target",target); 480 481 let controls = this.closest(".controls"); 482 let hadLikes = controls.classList.contains("has_likes"); 483 let likeCountNode = controls.getElementsByClassName("like_count")[0]; 484 log("likeCountNode",likeCountNode); 485 if(this.classList.contains("add_like")) { 486 this.classList.remove("add_like"); 487 this.classList.add("remove_like"); 488 if(!hadLikes) controls.classList.add("has_likes"); 489 this.closest("a").setAttribute("href",target.replace("like","unlike")); 490 likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) + 1; 491 } else { 492 this.classList.remove("remove_like"); 493 this.classList.add("add_like"); 494 let likeCount = parseInt(likeCountNode.innerHTML); 495 if(likeCount==1) controls.classList.remove("has_likes"); 496 this.closest("a").setAttribute("href",target.replace("unlike","like")); 497 likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1; 498 } 499 500 //let likeButton = this; 501 $.ajax({ 502 url:target, 503 type:"POST", 504 dataType:"json", 505 data: { js: 1 }, 506 error: ajaxError, 507 success: function (dat,status,xhr) { 508 if("success" in dat && dat["success"] == "1") return; 509 // addNotice("Failed to add a like: {err}") 510 //likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML)-1; 511 log("data",dat); 512 log("status",status); 513 log("xhr",xhr); 514 } 515 }); 516 }); 517 518 $(".link_label").click(function(ev) { 519 ev.preventDefault(); 520 let linkSel = $('#'+$(this).attr("data-for")); 521 if(!linkSel.hasClass("link_opened")) { 522 ev.stopPropagation(); 523 linkSel.addClass("link_opened"); 524 } 525 }); 526 527 function rebuildPaginator(lastPage) { 528 let urlParams = new URLSearchParams(window.location.search); 529 let page = urlParams.get('page'); 530 if(page=="") page = 1; 531 532 let pageList = Paginate(page,lastPage,5) 533 //$(".pageset").html(Tmpl_paginator({PageList:pageList,Page:page,LastPage:lastPage})); 534 let ok = false; 535 $(".pageset").each(function(){ 536 this.outerHTML = Tmpl_paginator({PageList:pageList,Page:page,LastPage:lastPage}); 537 ok = true; 538 }); 539 if(!ok) $(Tmpl_paginator({PageList:pageList,Page:page,LastPage:lastPage})).insertAfter("#topic_list"); 540 } 541 542 function rebindPaginator() { 543 // TODO: Take mostviewed into account 544 // TODO: Get this to work with topics too 545 $(".pageitem a").unbind("click"); 546 $(".pageitem a").click(function(ev) { 547 ev.preventDefault(); 548 let url = "//"+window.location.host+window.location.pathname; 549 let urlParams = new URLSearchParams(window.location.search); 550 urlParams.set("page",new URLSearchParams(this.getAttribute("href")).get("page")); 551 let q = "?"; 552 for(let item of urlParams.entries()) q += item[0]+"="+item[1]+"&"; 553 if(q.length>1) q = q.slice(0,-1); 554 555 // TODO: Try to de-duplicate some of these fetch calls 556 fetch(url+q+"&js=1",{credentials:"same-origin"}) 557 .then(r => { 558 if(!r.ok) throw(url+q+"&js=1 failed to load"); 559 return r.json(); 560 }).then(d => { 561 if(!"Topics" in d) throw("no Topics in data"); 562 let topics = d["Topics"]; 563 log("ajax navigated to different page"); 564 565 // TODO: Fix the data race where the function hasn't been loaded yet 566 let out = ""; 567 for(let i=0;i<topics.length;i++) out += Tmpl_topics_topic(topics[i]); 568 $(".topic_list").html(out); 569 570 let obj = {Title:document.title,Url:url+q}; 571 history.pushState(obj,obj.Title,obj.Url); 572 rebuildPaginator(d.LastPage); 573 rebindPaginator(); 574 }).catch(e => { 575 log("Unable to get script "+url+q+"&js=1",e); 576 console.trace(); 577 }); 578 }); 579 } 580 581 // TODO: Render a headless topics.html instead of the individual topic rows and a bit of JS glue 582 $(".filter_item").click(function(ev) { 583 if(!window.location.pathname.startsWith("/topics/")) return 584 ev.preventDefault(); 585 let that = this; 586 let fid = this.getAttribute("data-fid"); 587 // TODO: Take mostviewed into account 588 let url = "//"+window.location.host+"/topics/?fids="+fid; 589 590 fetch(url+"&js=1",{credentials:"same-origin"}) 591 .then(r => { 592 if(!r.ok) throw(url+"&js=1 failed to load"); 593 return r.json(); 594 }).then(d => { 595 log("data",d); 596 if(!"Topics" in d) throw("no Topics in data"); 597 let topics = d["Topics"]; 598 log("ajax navigated to "+that.innerText); 599 600 // TODO: Fix the data race where the function hasn't been loaded yet 601 let out = ""; 602 for(let i=0;i<topics.length;i++) out += Tmpl_topics_topic(topics[i]); 603 $(".topic_list").html(out); 604 //$(".topic_list").addClass("single_forum"); 605 606 baseTitle = that.innerText; 607 if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle; 608 else document.title = baseTitle; 609 let obj = {Title:document.title,Url:url}; 610 history.pushState(obj,obj.Title,obj.Url); 611 rebuildPaginator(d.LastPage) 612 rebindPaginator(); 613 614 $(".filter_item").each(function(){ 615 this.classList.remove("filter_selected"); 616 }); 617 that.classList.add("filter_selected"); 618 $(".topic_list_title h1").text(that.innerText); 619 $(".link_select .link_option .link_recent").attr("href","//"+window.location.host+"/topics/?fids="+fid); 620 $(".link_select .link_option .link_most_viewed").attr("href","//"+window.location.host+"/topics/most-viewed/?fids="+fid); 621 unbindPage(); 622 bindPage(); 623 }).catch(e => { 624 log("Unable to get script "+url+"&js=1",e); 625 console.trace(); 626 }); 627 }); 628 629 if(document.getElementById("topicsItemList")!==null) rebindPaginator(); 630 if(document.getElementById("forumItemList")!==null) rebindPaginator(); 631 632 // TODO: Show a search button when JS is disabled? 633 $(".widget_search_input").keypress(function(e) { 634 if(e.keyCode!='13') return; 635 // TODO: Only fire on /topics/ 636 event.preventDefault(); 637 // TODO: Take mostviewed into account 638 let url = "//"+window.location.host+window.location.pathname; 639 let urlParams = new URLSearchParams(window.location.search); 640 urlParams.set("q",this.value); 641 let q = "?"; 642 for(let item of urlParams.entries()) q += item[0]+"="+item[1]+"&"; 643 if(q.length>1) q = q.slice(0,-1); 644 645 // TODO: Try to de-duplicate some of these fetch calls 646 fetch(url+q+"&js=1",{credentials:"same-origin"}) 647 .then(r => { 648 if(!r.ok) throw(url+q+"&js=1 failed to load"); 649 return r.json(); 650 }).then(d => { 651 if(!"Topics" in d) throw("no Topics in data"); 652 let topics = d["Topics"]; 653 log("ajax navigated to search page"); 654 655 // TODO: Fix the data race where the function hasn't been loaded yet 656 let out = ""; 657 for(let i=0;i<topics.length;i++) out += Tmpl_topics_topic(topics[i]); 658 $(".topic_list").html(out); 659 660 baseTitle = phraseBox["topic_list"]["topic_list.search_head"]; 661 $(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]); 662 if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle; 663 else document.title = baseTitle; 664 let obj = {Title: document.title, Url: url+q}; 665 history.pushState(obj,obj.Title,obj.Url); 666 rebuildPaginator(d.LastPage); 667 rebindPaginator(); 668 $(".link_select .link_option .link_recent").attr("href",url+q); 669 $(".link_select .link_option .link_most_viewed").attr("href",url+q); 670 }).catch(e => { 671 log("Unable to get script "+url+q+"&js=1",e); 672 console.trace(); 673 }); 674 }); 675 676 runInitHook("before_init_bind_page"); 677 bindPage(); 678 runInitHook("after_init_bind_page"); 679 680 $(".edit_field").click(function(ev) { 681 ev.preventDefault(); 682 let bp = $(this).closest('.editable_parent'); 683 let block = bp.find('.editable_block').eq(0); 684 block.html("<input name='edit_field'value='"+block.text()+"'type='text'><a href='"+$(this).closest('a').attr("href")+"'><button class='submit_edit'type='submit'>Update</button></a>"); 685 686 $(".submit_edit").click(function(ev) { 687 ev.preventDefault(); 688 let bp = $(this).closest('.editable_parent'); 689 let bl = bp.find('.editable_block').eq(0); 690 let content = bl.find('input').eq(0).val(); 691 bl.html(content); 692 693 let formAction = $(this).closest('a').attr("href"); 694 $.ajax({ 695 url: formAction+"?s="+me.User.S, 696 type:"POST", 697 dataType:"json", 698 error: ajaxError, 699 data: { js: 1, edit_item: content } 700 }); 701 }); 702 }); 703 704 $(".edit_fields").click(function(ev) { 705 ev.preventDefault(); 706 if($(this).find("input").length!==0) return; 707 //log("clicked .edit_fields"); 708 var bp = $(this).closest('.editable_parent'); 709 bp.find('.hide_on_edit').addClass("edit_opened"); 710 bp.find('.show_on_edit').addClass("edit_opened"); 711 bp.find('.editable_block').show(); 712 bp.find('.editable_block').each(function(){ 713 var fieldName = this.getAttribute("data-field"); 714 var fieldType = this.getAttribute("data-type"); 715 if(fieldType=="list") { 716 var fieldValue = this.getAttribute("data-value"); 717 if(fieldName in formVars) var it = formVars[fieldName]; 718 else var it = ['No','Yes']; 719 var itLen = it.length; 720 var out = ""; 721 for (var i=0; i<itLen; i++) { 722 var sel = ""; 723 if(fieldValue == i || fieldValue == it[i]) { 724 sel = "selected "; 725 this.classList.remove(fieldName+'_'+it[i]); 726 this.innerHTML = ""; 727 } 728 out += "<option "+sel+"value='"+i+"'>"+it[i]+"</option>"; 729 } 730 this.innerHTML = "<select data-field='"+fieldName+"'name='"+fieldName+"'>"+out+"</select>"; 731 } 732 else if(fieldType=="hidden") {} 733 else this.innerHTML = "<input name='"+fieldName+"'value='"+this.textContent+"'type='text'>"; 734 }); 735 736 // Remove any handlers already attached to the submitter 737 $(".submit_edit").unbind("click"); 738 739 $(".submit_edit").click(function(ev) { 740 ev.preventDefault(); 741 var outData = {js: 1} 742 var bp = $(this).closest('.editable_parent'); 743 bp.find('.editable_block').each(function() { 744 var fieldName = this.getAttribute("data-field"); 745 var fieldType = this.getAttribute("data-type"); 746 if(fieldType=="list") { 747 var newContent = $(this).find('select :selected').text(); 748 this.classList.add(fieldName+'_'+newContent); 749 this.innerHTML = ""; 750 } else if(fieldType=="hidden") { 751 var newContent = $(this).val(); 752 } else { 753 var newContent = $(this).find('input').eq(0).val(); 754 this.innerHTML = newContent; 755 } 756 this.setAttribute("data-value",newContent); 757 outData[fieldName] = newContent; 758 }); 759 760 let href = $(this).closest('a').attr("href"); 761 //log("href",href); 762 //log(outData); 763 $.ajax({ url: href+"?s="+me.User.S, type:"POST", dataType:"json", data: outData, error: ajaxError }); 764 bp.find('.hide_on_edit').removeClass("edit_opened"); 765 bp.find('.show_on_edit').removeClass("edit_opened"); 766 }); 767 }); 768 769 $(this).click(() => { 770 $(".selectedAlert").removeClass("selectedAlert"); 771 $("#back").removeClass("alertActive"); 772 $(".link_select").removeClass("link_opened"); 773 }); 774 775 $(".alert_bell").click(function(){ 776 let menuAlerts = $(this).parent(); 777 if(menuAlerts.hasClass("selectedAlert")) { 778 event.stopPropagation(); 779 menuAlerts.removeClass("selectedAlert"); 780 $("#back").removeClass("alertActive"); 781 } 782 }); 783 $(".menu_alerts").click(function(ev) { 784 ev.stopPropagation(); 785 if($(this).hasClass("selectedAlert")) return; 786 if(!conn) loadAlerts(this,true); 787 this.className += " selectedAlert"; 788 document.getElementById("back").className += " alertActive" 789 }); 790 $(".link_select").click(ev => ev.stopPropagation()); 791 792 $("input,textarea,select,option").keyup(ev => ev.stopPropagation()) 793 794 $("#themeSelectorSelect").change(function(){ 795 log("Changing the theme to "+this.options[this.selectedIndex].getAttribute("value")); 796 $.ajax({ 797 url: this.form.getAttribute("action")+"?s="+me.User.S, 798 type:"POST", 799 dataType:"json", 800 data: { "theme": this.options[this.selectedIndex].getAttribute("value"), js: 1 }, 801 error: ajaxError, 802 success: function (dat,status,xhr) { 803 log("Theme successfully switched"); 804 log("dat",dat); 805 log("status",status); 806 log("xhr",xhr); 807 window.location.reload(); 808 } 809 }); 810 }); 811 812 // The time range selector for the time graphs in the Control Panel 813 $(".autoSubmitRedirect").change(function(){ 814 let els = this.form.elements; 815 let s = ""; 816 for(let i=0; i<els.length; i++) { 817 let el = els[i]; 818 if(el.nodeName=="SELECT") { 819 s += el.name+"="+el.options[el.selectedIndex].getAttribute("value")+"&"; 820 } 821 // TODO: Implement other element types... 822 } 823 if(s.length > 0) s = "?"+s.substr(0, s.length-1); 824 825 window.location = this.form.getAttribute("action")+s; // Do a redirect as a form submission refuses to work properly 826 }); 827 828 $(".unix_to_24_hour_time").each(function(){ 829 let unixTime = this.innerText; 830 let date = new Date(unixTime*1000); 831 log("date",date); 832 let mins = "0"+date.getMinutes(); 833 let formattedTime = date.getHours()+":"+mins.substr(-2); 834 log("formattedTime",formattedTime); 835 this.innerText = formattedTime; 836 }); 837 838 $(".unix_to_date").each(function(){ 839 // TODO: Localise this 840 let monthList = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 841 let date = new Date(this.innerText * 1000); 842 log("date",date); 843 let day = "0"+date.getDate(); 844 let formattedTime = monthList[date.getMonth()]+" "+day.substr(-2)+" "+date.getFullYear(); 845 log("formattedTime",formattedTime); 846 this.innerText = formattedTime; 847 }); 848 849 $("spoiler").addClass("hide_spoil"); 850 $(".hide_spoil").click(function(ev) { 851 ev.stopPropagation(); 852 ev.preventDefault(); 853 $(this).removeClass("hide_spoil"); 854 $(this).unbind("click"); 855 }); 856 857 this.onkeyup = function(ev) { 858 if(ev.which==37) this.querySelectorAll("#prevFloat a")[0].click(); 859 if(ev.which==39) this.querySelectorAll("#nextFloat a")[0].click(); 860 }; 861 862 function asyncGetSheet(src) { 863 return new Promise((resolve,reject) => { 864 let res = document.createElement('link'); 865 res.async = true; 866 867 const onloadHandler = (e,isAbort) => { 868 if (isAbort || !res.readyState || /loaded|complete/.test(res.readyState)) { 869 res.onload = null; 870 res.onreadystatechange = null; 871 res = undefined; 872 873 isAbort ? reject(e) : resolve(); 874 } 875 } 876 877 res.onerror = (e) => { 878 reject(e); 879 }; 880 res.onload = onloadHandler; 881 res.onreadystatechange = onloadHandler; 882 res.href = src; 883 res.rel = "stylesheet"; 884 res.type = "text/css"; 885 886 const prior = document.getElementsByTagName('link')[0]; 887 prior.parentNode.insertBefore(res,prior); 888 }); 889 } 890 891 function stripQ(name) { 892 return name.split('?')[0]; 893 } 894 895 function loadArb(base,href,h=null) { 896 fetch(href,{credentials:"same-origin"}) 897 .then(resp => { 898 if(!resp.ok) throw(href+" failed to load"); 899 let xr = resp.headers.get("x-res") 900 if(xr!=null) { 901 for(let res of xr.split(",")) { 902 let pro; 903 if(stripQ(getExt(res))=="css") pro = asyncGetSheet(pre+res) 904 else pro = asyncGetScript(pre+res) 905 pro.then(() => log("Loaded "+res)) 906 .catch(e => { 907 log("Unable to get "+res,e); 908 console.trace(); 909 }); 910 } 911 } 912 return resp.text(); 913 }).then(d => { 914 document.querySelector("#back").outerHTML = d; 915 if(h!==null) h(d); 916 $(".elapsed").remove(); 917 let obj = {Title:document.title,Url:base}; 918 history.pushState(obj,obj.Title,obj.Url); 919 }).catch(e => { 920 log("Unable to get script "+href,e); 921 console.trace(); 922 }); 923 } 924 925 /*$(".rowtopic a,a.rowtopic,a.forum_poster").click(function(ev) { 926 let base = this.getAttribute("href"); 927 loadArb(base,base+"?i=1", () => { 928 unbindTopic(); 929 bindTopic(); 930 }); 931 ev.stopPropagation(); 932 ev.preventDefault(); 933 })*/ 934 $("a").click(function(ev) { 935 let base = this.getAttribute("href"); 936 if(base!="/topics/") return; 937 loadArb(base,base+"?i=1", () => { 938 unbindPage(); 939 bindPage(); 940 }); 941 ev.stopPropagation(); 942 ev.preventDefault(); 943 }) 944 945 runInitHook("almost_end_init"); 946 runInitHook("end_init"); 947 } 948 949 function bindPage() { 950 log("enter bindPage"); 951 $(".create_topic_link").click(ev => { 952 ev.preventDefault(); 953 $(".topic_create_form").removeClass("auto_hide"); 954 }); 955 $(".topic_create_form .close_form").click(ev => { 956 ev.preventDefault(); 957 $(".topic_create_form").addClass("auto_hide"); 958 }); 959 960 bindTopic(); 961 runInitHook("end_bind_page") 962 } 963 964 function unbindPage() { 965 log("enter unbindPage"); 966 $(".create_topic_link").unbind("click"); 967 $(".topic_create_form .close_form").unbind("click"); 968 unbindTopic(); 969 runHook("end_unbind_page") 970 } 971 972 function bindTopic() { 973 log("enter bindTopic"); 974 $(".open_edit").click(ev => { 975 ev.preventDefault(); 976 $('.hide_on_edit').addClass("edit_opened"); 977 $('.show_on_edit').addClass("edit_opened"); 978 runHook("open_edit"); 979 }); 980 981 $(".topic_item .submit_edit").click(function(ev){ 982 ev.preventDefault(); 983 let nameInput = $(".topic_name_input").val(); 984 $(".topic_name").html(nameInput); 985 $(".topic_name").attr(nameInput); 986 let contentInput = $('.topic_content_input').val(); 987 $(".topic_content").html(quickParse(contentInput)); 988 let statusInput = $('.topic_status_input').val(); 989 $(".topic_status_e:not(.open_edit)").html(statusInput); 990 991 $('.hide_on_edit').removeClass("edit_opened"); 992 $('.show_on_edit').removeClass("edit_opened"); 993 runHook("close_edit"); 994 995 $.ajax({ 996 url: this.form.getAttribute("action"), 997 type:"POST", 998 dataType:"json", 999 data: { 1000 name: nameInput, 1001 status: statusInput, 1002 content: contentInput, 1003 js: 1 1004 }, 1005 error: ajaxError, 1006 success: (dat,status,xhr) => { 1007 if("Content" in dat) $(".topic_content").html(dat["Content"]); 1008 } 1009 }); 1010 }); 1011 1012 $(".delete_item").click(function(ev) { 1013 postLink(ev); 1014 $(this).closest('.deletable_block').remove(); 1015 }); 1016 1017 // Miniature implementation of the parser to avoid sending as much data back and forth 1018 function quickParse(m) { 1019 const r = (o,n) => { 1020 m = m.replace(o,n) 1021 } 1022 r(":)", "😀") 1023 r(":(", "😞") 1024 r(":D", "😃") 1025 r(":P", "😛") 1026 r(":O", "😲") 1027 r(":p", "😛") 1028 r(":o", "😲") 1029 r(";)", "😉") 1030 r("\n","<br>") 1031 return m 1032 } 1033 1034 $(".edit_item").click(function(ev){ 1035 ev.preventDefault(); 1036 1037 let bp = this.closest('.editable_parent'); 1038 $(bp).find('.hide_on_edit').addClass("edit_opened"); 1039 $(bp).find('.show_on_edit').addClass("edit_opened"); 1040 $(bp).find('.hide_on_block_edit').addClass("edit_opened"); 1041 $(bp).find('.show_on_block_edit').addClass("edit_opened"); 1042 let srcNode = bp.querySelector(".edit_source"); 1043 let block = bp.querySelector('.editable_block'); 1044 block.classList.add("in_edit"); 1045 1046 let src = ""; 1047 if(srcNode!=null) src = srcNode.innerText; 1048 else src = block.innerHTML; 1049 block.innerHTML = Tmpl_topic_c_edit_post({ 1050 ID: bp.getAttribute("id").slice("post-".length), 1051 Source: src, 1052 Ref: this.closest('a').getAttribute("href") 1053 }) 1054 runHook("edit_item_pre_bind"); 1055 1056 $(".submit_edit").click(function(ev){ 1057 ev.preventDefault(); 1058 $(bp).find('.hide_on_edit').removeClass("edit_opened"); 1059 $(bp).find('.show_on_edit').removeClass("edit_opened"); 1060 $(bp).find('.hide_on_block_edit').removeClass("edit_opened"); 1061 $(bp).find('.show_on_block_edit').removeClass("edit_opened"); 1062 block.classList.remove("in_edit"); 1063 let con = block.querySelector('textarea').value; 1064 block.innerHTML = quickParse(con); 1065 if(srcNode!=null) srcNode.innerText = con; 1066 1067 let formAction = this.closest('a').getAttribute("href"); 1068 // TODO: Bounce the parsed post back and set innerHTML to it? 1069 $.ajax({ 1070 url: formAction, 1071 type:"POST", 1072 dataType:"json", 1073 data: { js: 1, edit_item: con }, 1074 error: ajaxError, 1075 success: (dat,status,xhr) => { 1076 if("Content" in dat) block.innerHTML = dat["Content"]; 1077 } 1078 }); 1079 }); 1080 }); 1081 1082 $(".quote_item").click(function(ev){ 1083 ev.preventDefault(); 1084 ev.stopPropagation(); 1085 let src = this.closest(".post_item").getElementsByClassName("edit_source")[0]; 1086 let con = document.getElementById("input_content") 1087 log("con.value",con.value); 1088 1089 let item; 1090 if(con.value=="") item = "<blockquote>"+src.innerHTML+"</blockquote>" 1091 else item = "\r\n<blockquote>"+src.innerHTML+"</blockquote>"; 1092 con.value = con.value+item; 1093 log("con.value",con.value); 1094 1095 // For custom / third party text editors 1096 quoteItemCallback(src.innerHTML,item); 1097 }); 1098 1099 //id="poll_results_{pollid}" class="poll_results auto_hide" 1100 $(".poll_results_button").click(function(){ 1101 let pollID = $(this).attr("data-poll-id"); 1102 $("#poll_results_"+pollID).removeClass("auto_hide"); 1103 fetch("/poll/results/"+pollID, { 1104 credentials: 'same-origin' 1105 }).then(resp => resp.text()).catch(e => console.error("e",e)).then(rawData => { 1106 // TODO: Make sure the received data is actually a list of integers 1107 let data = JSON.parse(rawData); 1108 let allZero = true; 1109 for(let i=0; i<data.length; i++) { 1110 if(data[i]!="0") allZero = false; 1111 } 1112 if(allZero) { 1113 $("#poll_results_"+pollID+" .poll_no_results").removeClass("auto_hide"); 1114 log("all zero") 1115 return; 1116 } 1117 1118 $("#poll_results_"+pollID+" .user_content").html("<div id='poll_results_chart_"+pollID+"'></div>"); 1119 log("rawData",rawData); 1120 log("series",data); 1121 Chartist.Pie('#poll_results_chart_'+pollID, { 1122 series: data, 1123 }, { 1124 height: '120px', 1125 }); 1126 }) 1127 }); 1128 1129 runInitHook("end_bind_topic"); 1130 } 1131 1132 function unbindTopic() { 1133 log("enter unbindTopic"); 1134 $(".open_edit").unbind("click"); 1135 $(".topic_item .submit_edit").unbind("click"); 1136 $(".delete_item").unbind("click"); 1137 $(".edit_item").unbind("click"); 1138 $(".submit_edit").unbind("click"); 1139 $(".quote_item").unbind("click"); 1140 $(".poll_results_button").unbind("click"); 1141 runHook("end_unbind_topic"); 1142 }