MediaWiki:Follow.js

/* Update recent news about a favorite title (function initFollower {

// Add link switch (mw.config.get("skin")) { case "wikia": // oasis case "oasis": $("#my-tools-menu").append('Theo dõi bộ truyện'); break; case "monobook": case "vector": $("#p-tb ul").append('Theo dõi bộ truyện'); break; }

// Core object var follower = { fn: { loadGlobal: function { }, saveGlobal: function { }, loadLocal: function { }, saveLocal: function { }, setData: function { }, follow: function { }, hasFollowed: function { }, target: function { }, untarget: function { }, unfollow: function { }, unfollowAll: function { }, getFeeds: function { }, getAvailableTitles: function { }, clearFeeds: function { }, vitalizeInterface: function { }, genInteractiveItem: function { }, genInteractiveFeed: function { } },       settings: { scriptURL: mw.config.get("wgServer") + "/" + mw.config.get("wgScriptPath") + "/api.php", // domain keyOfTitles: "watchlistTitles", deleteIconURL: "https://i.imgur.com/toc40DW.png", username: mw.config.get("wgUserName"), storageTitle: "User:" + mw.config.get("wgUserName") + "/FollowTitles.css", feedsPerReq: 4, availableTitles: [], from: {}, to: {}, noMore: {} },       data: { titles: [], targetTitles: [] },       newfeeds: {}, markup: { html: "", css: "" }   };

// Decorate html follower.markup.html = ' \n' + '   \n' + '       \n' + '           To the light of Holy, to the glory of Dark, let us pray, the faith of Sonako.\n' + '       \n' + '       \n' + '           Hãy đăng nhập để đồng bộ hóa danh sách theo dõi trên mọi thiết bị và trình duyệt nhé! Nếu danh sách truyện\n' + '           bị thiếu, hãy thử bấm cập nhật trước khi tải lại trang.\n' + '       \n' + '        \n' + '   \n' + '   \n' + '       Bạn muốn thêm truyện nào vào danh sách theo dõi? \n' + '       \n' + '       \n' + '           \n' + '       \n' + '       \n' + '       \n' + '   \n' + '   \n' + '   \n' + '       Hãy tích chọn những truyện bạn muốn cập nhật tin tức. \n' + '       Những truyện bạn đang theo dõi: \n' + '       \n' + '        \n' + '       \n' + '           \n' + '           \n' + '       \n' + '   \n' + '   \n' + '   \n' + '       <input id="getFeed" type="button" value="Dạo này có chương mới chưa?" />\n' + '       \n' + '       \n' + '       <p id="feedStatus" class="info"> \n' + '       \n' + '           <input id="getMoreFeed" type="button" value="Tải thêm tí nữa" />\n' + '           <input id="clearFeed" type="button" value="Xóa hết tìm lại" />\n' + '       \n' + '   \n' + ' \n' ;

// Decorate css follower.markup.css = 'img.delete {\n' + '   margin: 0 4px;\n' + '   cursor: pointer;\n' + '}\n' + '\n' + '.content.list {\n' + '   display: flex;\n' + '   flex-direction: column;\n' + '}\n' + '\n' + '.inactive {\n' + '   display: none;\n' + '}\n' + '\n' + '.subheader.subtitle {\n' + '   font-size: xx-large;\n' + '   font-style: italic;\n' + '   font-weight: bolder;\n' + '}\n' + '\n' + '.interactiveItemWrapper {\n' + '   display: inline-flex;\n' + '}\n' + '\n' + '.interactiveFeedWrapper {\n' + '   display: inline-block;\n' + '   margin: 10px;\n' + '   border: 2px black solid;\n' + '   padding: 10px;\n' + '   flex-grow: 1;\n' + '   width: 20%;\n' + '}\n' + '\n' + '.feeds.list {\n' + '   display: block;\n' + '   width: 100%;\n' + '}\n' + '\n' + '.info {\n' + '   font-style: italic;\n' + '}\n' + '\n' + '.titles div.autocompleteTitles {\n' + '   width: 300px;\n' + '   position: relative;\n' + '   display: inline-block;\n' + '}\n' + '\n' + '.titles div.autocompleteTitles input {\n' + '   width: 90%;\n' + '}\n' + '\n' + '.autocompleteTitles-items {\n' + '   position: absolute;\n' + '   border: 1px solid #d4d4d4;\n' + '   border-bottom: none;\n' + '   border-top: none;\n' + '   z-index: 99;\n' + '   /*position the autocomplete items to be the same width as the container:*/\n' + '   top: 100%;\n' + '   left: 0;\n' + '   right: 0;\n' + '}\n' + '\n' + '.autocompleteTitles-items div {\n' + '   padding: 10px;\n' + '   background-color: #fff;\n' + '   border-bottom: 1px solid #d4d4d4;\n' + '}\n' + '\n' + '.autocompleteTitles-items div:hover {\n' + '   /*when hovering an item:*/\n' + '   background-color: #e9e9e9;\n' + '}\n' + '\n' + '.autocompleteTitles-active {\n' + '   /*when navigating through the items using the arrow keys:*/\n' + '   background-color: DodgerBlue !important;\n' + '   color: #ffffff;\n' + '}\n' ;

// Define functionalities follower.fn.loadLocal = function { follower.fn.setData(JSON.parse(window.localStorage.getItem(follower.settings.keyOfTitles)) || {}); }

follower.fn.saveLocal = function { window.localStorage.setItem(follower.settings.keyOfTitles, JSON.stringify(follower.data)); }

follower.fn.saveGlobal = function (cbSuccess, cbFailure) { // Check identification if (follower.settings.username === null) { cbFailure("Bạn chưa đăng nhập nên không thể lưu dữ liệu lên wikia."); return; }       // Check token permission if (mw.user.tokens.values.editToken === "+\\") { cbFailure("editToken của bạn không hợp lệ."); return; }       // Check storage existence $.ajax({           url: follower.settings.scriptURL,            type: "POST",            dataType: "json",            data: {                action: "edit",                title: follower.settings.storageTitle,                summary: "Save following titles",                text: JSON.stringify(follower.data),                token: mw.user.tokens.values.editToken,                format: "json"            }        }).then(function onsavefulfilled(data) {            console.debug("onsavefulfilled");            console.debug(data);            cbSuccess("Đã lưu danh sách theo dõi tại <a target='_blank' class='title' href='/wiki/" + follower.settings.storageTitle + "'>" + follower.settings.storageTitle + "</a>");        }).fail(function oneditrejected(jqXHR, textStatus) {            console.debug("oneditrejected " + textStatus);            console.debug(jqXHR);            cbFailure(textStatus); });   }

follower.fn.setData = function (newState) { if (newState instanceof Object) { for (var key in newState) { if (newState.hasOwnProperty(key) && follower.data.hasOwnProperty(key)) { follower.data[key] = newState[key]; }           }        }        follower.fn.saveLocal; // Always save local after data changes }

follower.fn.hasFollowed = function (title) { return follower.data.titles.includes(title); }

follower.fn.follow = function (title) { follower.fn.setData({           titles: follower.data.titles.concat(title),            targetTitles: follower.data.targetTitles.concat(title)        }); }

follower.fn.target = function (title) { console.debug("+ " + title); follower.fn.setData({           targetTitles: follower.data.targetTitles.concat(title)        }); }

follower.fn.untarget = function (title) { follower.fn.setData({           targetTitles: follower.data.targetTitles.splice(follower.data.targetTitles.indexOf(title), 1)        }); }

follower.fn.unfollow = function (title) { console.debug("- " + title); follower.fn.setData({           targetTitles: follower.data.targetTitles.splice(follower.data.targetTitles.indexOf(title), 1),            titles: follower.data.titles.splice(follower.data.titles.indexOf(title), 1)        }); follower.settings.noMore[title] = undefined; follower.settings.from[title] = undefined; follower.settings.to[title] = undefined; }

follower.fn.unfollowAll = function { follower.fn.setData({           targetTitles: [],            titles: []        }); follower.settings.noMore = {}; follower.settings.from = {}; follower.settings.to = {}; }

follower.fn.getAvailableTitles = function { $.ajax({           url: follower.settings.scriptURL,            data: {                action: "query",                list: "allcategories",                acmin: 1,                aclimit: "max",                format: "json"            },            type: "GET",            dataType: "json",            success: function (data) {                follower.settings.availableTitles = data.query.allcategories.map(function (e) { return e["*"]; });           }        });    }

follower.fn.genInteractiveItem = function (title, checked) { item = document.createElement("div"); item.className = "interactiveItemWrapper"; item.value = title;

input = document.createElement("input"); item.appendChild(input); input.setAttribute("type", "checkbox"); input.setAttribute("id", "feed"); input.setAttribute("name", title); input.setAttribute("value", title); input.checked = checked; input.addEventListener("click", function (e) {           if (e.target.checked) {                follower.fn.target(e.target.value);            } else {                follower.fn.untarget(e.target.value);            }        })

dlt = document.createElement("img"); item.appendChild(dlt); dlt.setAttribute("src", follower.settings.deleteIconURL); dlt.className = "delete"; dlt.setAttribute("value", title); dlt.addEventListener("click", function {            follower.fn.unfollow($(this).attr("value"));            $(this).parent.remove;        });

lbl = document.createElement("label"); item.appendChild(lbl); lbl.setAttribute("for", "feed"); lbl.innerText = title;

return item; }

follower.fn.genInteractiveFeed = function (feed) { feedWrapper = document.createElement("div"); feedWrapper.className = "activity-type-new activity-ns-0 interactiveFeedWrapper"; console.debug(feed);

bullet = document.createElement("img"); feedWrapper.appendChild(bullet); bullet.className = "new sprite"; bullet.setAttribute("src", "data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"); bullet.setAttribute("alt", "Trang mới"); bullet.setAttribute("title", "Trang mới"); bullet.setAttribute("width", 16); bullet.setAttribute("height", 16);

title = document.createElement("strong"); feedWrapper.appendChild(title); a = document.createElement("a"); title.appendChild(a); a.className = "title"; a.setAttribute("href", "/wiki/" + encodeURIComponent(feed.title)); a.setAttribute("target", "_blank"); a.innerText = feed.title;

cite = document.createElement("cite"); feedWrapper.appendChild(cite); p = document.createElement("p"); cite.appendChild(p); p.className = "subtle"; span = document.createElement("span"); p.appendChild(span); span.innerText = " cập nhật lần cuối bởi "; a = document.createElement("a"); p.appendChild(a); a.setAttribute("href", "/wiki/User:" + encodeURIComponent(feed.byUser)); a.setAttribute("rel", "nofollow"); a.setAttribute("target", "_blank"); a.className = "userlink"; a.innerText = feed.byUser; span = document.createElement("span"); p.appendChild(span); span.innerText = " vào " + (new Date(Date.parse(feed.lastedit))).toLocaleString;

return feedWrapper; }

follower.fn.getFeeds = function (cbSuccess, cbFailures) { console.debug("getting feeds") currentDate = (new Date).toISOString; follower.data.targetTitles.forEach(function (title) {           if (!follower.settings.to[title]) {                follower.settings.from[title] = currentDate;                follower.settings.to[title] = currentDate;                follower.settings.noMore[title] = false;            }        });

$(follower.data.targetTitles).each(function (_, title) {           $.ajax({ url: follower.settings.scriptURL, data: { action: "query", generator: "categorymembers", gcmtitle: "Category:" + title, gcmnamespace: 0, gcmprop: "ids|title|timestamp", gcmsort: "timestamp", gcmdir: "desc", gcmlimit: follower.settings.feedsPerReq, gcmstart: follower.settings.to[title], format: "json", prop: "revisions|pageprops", rvprop: "timestamp|user", indexpageids: true },               type: "GET", dataType: "json", success: function (data) { console.debug(data); feeds = data.query.pageids.map(function (id) {                       return {                            pageid: id,                            title: data.query.pages[id].title,                            lastedit: data.query.pages[id].revisions[0].timestamp,                            byUser: data.query.pages[id].revisions[0].user                        }                    }); if (data["query-continue"]) { follower.settings.to[title] = data["query-continue"].categorymembers.gcmstart; follower.settings.noMore[title] = false; } else { follower.settings.to[title] = feeds[feeds.length - 1].lastedit; follower.settings.noMore[title] = true; }                   follower.newfeeds[title] = feeds;

cbSuccess(title); },               error: function (req, status, err) { console.debug(req); cbFailures(title, status, err); }           });        });    }

follower.fn.clearFeeds = function { follower.newfeeds = {}; follower.settings.from = {}; follower.settings.to = {}; follower.settings.noMore = {}; }

follower.fn.vitalizeInterface = function { // Set title $(".WikiaPage h1, h1#firstHeading").html("Theo Dõi Nâng cao"); $("head title").html("Theo Dõi Nâng cao | Sonako Light Novel Wiki");

// Load global and local data follower.fn.loadLocal; follower.fn.loadGlobal;

// Load available titles follower.fn.getAvailableTitles;

// Initial check of watchlist if (follower.data.titles.length === 0) { $(".watchlist .instruction").addClass("inactive"); $(".watchlist .control").addClass("inactive"); $(".watchlist #watchstatus").html("Trống vắng quá :(");           $(".feedRetriever").addClass("inactive");        } else {            $(".watchlist #watchstatus").empty;            $(".watchlist .content.list").append( follower.data.titles.map(function (title) {                   return follower.fn.genInteractiveItem(title, follower.data.targetTitles.includes(title));                }) );       }

// Autocomplete field // Sauce: https://www.w3schools.com/howto/howto_js_autocomplete.asp (function autocomplete(inp) {           function addActive(x) {                /*a function to classify an item as "active":*/                if (!x) return false;                /*start by removing the "active" class on all items:*/                removeActive(x);                if (currentFocus >= x.length) currentFocus = 0;                if (currentFocus < 0) currentFocus = (x.length - 1);                /*add class "autocomplete-active":*/                x[currentFocus].classList.add("autocompleteTitles-active");            }            function removeActive(x) {                /*a function to remove the "active" class from all autocomplete items:*/                for (var i = 0; i < x.length; i++) {                    x[i].classList.remove("autocompleteTitles-active");                }            }            function closeAllLists(elmnt) {                /*close all autocomplete lists in the document, except the one passed as an argument:*/ var x = document.getElementsByClassName("autocompleteTitles-items"); for (var i = 0; i < x.length; i++) { if (elmnt != x[i] && elmnt != inp) { x[i].parentNode.removeChild(x[i]); }               }            }            /*execute a function when someone clicks in the document:*/ document.addEventListener("click", function (e) {               closeAllLists(e.target);            }); inp.addEventListener("input", function (e) {               val = this.value;                /*close any already open lists of autocompleted values*/                closeAllLists;                if (!val) { return false; }                currentFocus = -1;                /*create a DIV element that will contain the items (values):*/                a = document.createElement("div");                a.setAttribute("id", this.id + "autocompleteTitles-list");                a.setAttribute("class", "autocompleteTitles-items");                /*append the DIV element as a child of the autocomplete container:*/                this.parentNode.appendChild(a);                /*for each item in the array...*/                for (i = 0; i < follower.settings.availableTitles.length; i++) {                    /*check if the item starts with the same letters as the text field value:*/                    t = follower.settings.availableTitles[i]; if (t.toLowerCase.includes(val.toLowerCase)) { /*create a DIV element for each matching element:*/ b = document.createElement("DIV"); /*make the matching letters bold:*/ idx = t.indexOf(val.toLowerCase); b.innerHTML = t.replace(new RegExp(val, "i"), " $& "); /*insert a input field that will hold the current array item's value:*/ b.innerHTML += "<input type='hidden' value='" + t + "'>"; /*execute a function when someone clicks on the item value (DIV element):*/ b.addEventListener("click", function (e) {                           /*insert the value for the autocomplete text field:*/                            inp.value = this.getElementsByTagName("input")[0].value;                            /*close the list of autocompleted values,                            (or any other open lists of autocompleted values:*/ closeAllLists; });                       a.appendChild(b);                    }                }            }); /*execute a function presses a key on the keyboard:*/ inp.addEventListener("keydown", function (e) {               var x = document.getElementById(this.id + "autocompleteTitles-list");                if (x) x = x.getElementsByTagName("div");                if (e.keyCode == 40) {                    /*If the arrow DOWN key is pressed,                    increase the currentFocus variable:*/                    currentFocus++;                    /*and and make the current item more visible:*/                    addActive(x);                } else if (e.keyCode == 38) { //up                    /*If the arrow UP key is pressed,                    decrease the currentFocus variable:*/                    currentFocus--;                    /*and and make the current item more visible:*/                    addActive(x);                } else if (e.keyCode == 13) {                    /*If the ENTER key is pressed, prevent the form from being submitted,*/ e.preventDefault; if (currentFocus > -1) { /*and simulate a click on the "active" item:*/ if (x) x[currentFocus].click; }               }            });        })(document.querySelector("#inputTitle"));

// Listener to register new title $(".titles #follow").click(function followCb {           title = $("#inputTitle").val;            if (!title) return;            console.debug(title);            $("#inputTitle option:selected").prop("selected", false); // Unselect            $(".watchlist #watchstatus").html("Trống vắng quá :("); $(".watchlist .instruction").removeClass("inactive"); $(".watchlist .control").removeClass("inactive"); $(".feedRetriever").removeClass("inactive"); if (!follower.fn.hasFollowed(title)) { follower.fn.follow(title); $(".watchlist .content.list").append(follower.fn.genInteractiveItem(title, true)); }       });

// Listener to clear titles $(".watchlist #clearTitles").click(function {            if (window.confirm("Bạn có chắc là muốn bỏ theo dõi những LN hấp dẫn này chứ?")) {                follower.fn.unfollowAll;                $(".watchlist .content.list").empty;                $(".watchlist .instruction").addClass("inactive");                $(".watchlist .control").addClass("inactive");                $(".watchlist #watchstatus").html("Trống vắng quá :("); }       });

// Listener to save titles to global $(".watchlist #saveTitles").click(function {            follower.fn.saveGlobal( function saveGlobalSuccessCb(resp) { $("#watchstatus").html(resp); },               function saveGlobalFailureCb(err) { $("#watchstatus").html(err); }           );        });

// Listener to retrieve feed $(".feedRetriever #getFeed").click(function {            $("#feedStatus").empty;

follower.fn.getFeeds(function getFeedCbSuccess(title) {               console.debug("getFeedCbSuccess " + title);

// Show control buttons $(".feedRetriever .control").removeClass("inactive");

if (follower.settings.noMore[title]) { $("#feedsStatus").append($(" ").text("Không thể đào thêm " + title + " nữa đâu. ")); }

// Show feeds $(".feeds.list").append(                   follower.newfeeds[title].map(follower.fn.genInteractiveFeed)                ); }, function getFeedCbFailure(title, status, err) { console.debug("getFeedCbFailure " + title + " " + status + " " + err); $("#feedsStatus").append($(" ").text("Xảy ra lỗi khi tải  " + title + ": " + status)); });       });        $(".feedRetriever #getMoreFeed").click(function  {            $(".feedRetriever #getFeed").click;        });

// Listener to clear current feeds $(".feedRetriever #clearFeed").click(function {            follower.fn.clearFeeds;            // Hide            $(".feedRetriever .feeds.list").empty;            $("#feedStatus").empty;            $(".feedRetriever .control").addClass("inactive");        });

window.follower = follower; }

// Implementation // Only show on Special:Follow if (mw.config.get("wgNamespaceNumber") === -1 && mw.config.get("wgTitle") === "Follow") { /* css */ mw.util.addCSS(follower.markup.css);

/* markup */ $("#mw-content-text").html(follower.markup.html);

/* add listeners */ follower.fn.vitalizeInterface; } });