v2exBetterReply

better reply experience for v2ex

当前为 2017-10-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name v2exBetterReply
  3. // @author dbw9580
  4. // @namespace v2ex.com
  5. // @description better reply experience for v2ex
  6. // @include /^https?:\/\/(\w+\.)?v2ex\.com\/t\//
  7. // @version 2017-10-20
  8. // @grant GM_log
  9. // @grant GM_addStyle
  10. // @run-at document-end
  11. // @require https://code.jquery.com/jquery-2.2.4.min.js
  12. // @supportURL https://github.com/dbw9580/v2exBetterReply/
  13. // ==/UserScript==
  14.  
  15. "use strict";
  16. //===========================
  17. // Configuration Section
  18. //
  19. // Set this to true to enable display of comments by blocked users.
  20. // This now only takes effect on comments referenced on the same page,
  21. // due to API restrictions, in multi-page threads, a comment referenced
  22. // on a different page that should be blocked, will still be displayed.
  23. // This may be fixed in future releases.
  24. var SHOW_BLOCKED_REF = false;
  25.  
  26. // Set this to your preferred max width of the reference preview floating block.
  27. var REF_PREVIEW_WIDTH = "600px";
  28.  
  29. // End of Configuration Section
  30. //===========================
  31.  
  32. GM_addStyle(".v2exBR-reply-no-target{background-color: #AAAAAA; color: black !important; cursor: pointer; font-weight:bold;}");
  33. GM_addStyle(".v2exBR-cited-comment-view{background-color: white; position: absolute; display: none; max-width: "+REF_PREVIEW_WIDTH+";}");
  34. GM_addStyle(".v2exBR-reply-citation{color: #778087; cursor: pointer;} .v2exBR-reply-citation:hover{color: #4d5256; text-decoration: underline;}");
  35. GM_addStyle(".v2exBR-cited-comment-view .fr{display: none;}");
  36.  
  37. /* insert preview block */
  38. $(document.body).append($("<div class=\"v2exBR-cited-comment-view cell\" id=\"v2exBR_citation_div\"></div>"));
  39.  
  40. var API = {};
  41. API.URL = {};
  42. API.URL.topicReply = "https://www.v2ex.com/api/replies/show.json?topic_id=";
  43. API.getTopicReplies = function (topicId) {
  44. var url = API.URL.topicReply + topicId.toString();
  45. var result;
  46. $.ajax({
  47. type: "GET",
  48. url: url,
  49. dataType: "json",
  50. success: function (data) { result = data },
  51. error: function () { return },
  52. async: false,
  53. // important! to prevent browser caching results returned by api which is vollatile
  54. cache: false
  55. });
  56.  
  57. return result;
  58. };
  59. API.getTopicReplyIdsInPostedOrder = function (repliesList) {
  60. var thisReply, i;
  61. var replyOrderIdMap = [];
  62.  
  63. //assume that replies returned by API are already in the order of them being posted
  64. //simply walk through the array.
  65. for (i = 0; i < repliesList.length; i++){
  66. replyOrderIdMap.push(repliesList[i].id);
  67. }
  68.  
  69. return replyOrderIdMap;
  70. };
  71.  
  72. function markReplyTrueOrder(replyOrderIdMap, repliesDivList) {
  73. var lastReplyIndex = parseInt($(repliesDivList).find(".no").eq(0).text()) - 1;
  74. var thisReplyId;
  75. $(repliesDivList).each(function (index) {
  76. thisReplyId = this.id.match(/^r_(\d+)/)[1];
  77. while (thisReplyId != replyOrderIdMap[lastReplyIndex].toString()) {
  78. if(lastReplyIndex < replyOrderIdMap.length){
  79. lastReplyIndex++;
  80. }
  81. else{
  82. return true;
  83. }
  84. }
  85. $(this).attr("v2exBR-true-order", 1 + lastReplyIndex++);
  86. });
  87. }
  88.  
  89. function adjustFloorNo(repliesDivList) {
  90. $(repliesDivList).each(function(){
  91. var thisReplyTrueOrder = $(this).attr("v2exBR-true-order");
  92. $(this).find(".no").text(thisReplyTrueOrder);
  93. });
  94. }
  95.  
  96. function inflatePreviewBlock(reply, previewDiv) {
  97. var cc = $(commentCells).eq(0).clone();
  98. $(cc).find("img.avatar").attr("src", reply.member.avatar_normal);
  99. $(cc).find("strong>a.dark").attr("href", "/member/" + reply.member.username).text(reply.member.username);
  100. $(cc).find("strong+span.fade.small").remove();
  101. $(cc).find("strong").after("&nbsp;&nbsp;<span class=\"fade small\">" + getRelativeTime(reply.last_modified) + "</span>&nbsp;&nbsp;<span class=\"small fade\">" + (reply.thanks != 0 ? `♥ ${reply.thanks}` : "") + "</span>");
  102. $(cc).find(".reply_content").html(reply.content_rendered);
  103. $(previewDiv).html($(cc).html());
  104. return $(previewDiv);
  105. }
  106.  
  107. function getRelativeTime(absTime) {
  108. var now = parseInt(Date.now() / 1000);
  109. var then = parseInt(absTime);
  110. var days = Math.floor((now - then) / (3600 * 24));
  111. var hours = Math.floor((now - then) / 3600) - days * 24;
  112. var mins = Math.floor((now - then) / 60) - days * 24 * 60 - hours * 60;
  113.  
  114. if (days > 0) {
  115. return days + " 天前";
  116. }
  117. else if (hours > 0) {
  118. return hours + " 小时 " + mins + " 分钟前";
  119. }
  120. else if (mins > 0) {
  121. return mins + " 分钟前";
  122. }
  123. else {
  124. return "几秒前";
  125. }
  126. }
  127.  
  128. var numCurrentPage = Math.ceil(parseInt($(".no").eq(0).text()) / 100);
  129. var threadUrl = window.location.href.match(/^.+\/t\/\d+/)[0];
  130. var commentCells = $("div.cell, div.inner").filter(function(){
  131. return this.id.startsWith("r");
  132. });
  133. var topicId = window.location.href.match(/^.+\/t\/(\d+)/)[1];
  134. var repliesList = API.getTopicReplies(topicId);
  135. var replyOrderIdMap = API.getTopicReplyIdsInPostedOrder(repliesList);
  136.  
  137. var startId = parseInt(commentCells.eq(0).get(0).id.substring(2));
  138. var endId = parseInt(commentCells.eq(-1).get(0).id.substring(2));
  139. var startNo = replyOrderIdMap.indexOf(startId);
  140. var endNo = replyOrderIdMap.indexOf(endId);
  141. var hiddenReplyIds = [];
  142. for (var i = startNo + 1; i < endNo; i++){
  143. var thisReplyId = replyOrderIdMap[i];
  144. if ($("#r_" + thisReplyId).length == 0) {
  145. hiddenReplyIds.push(thisReplyId);
  146. }
  147. }
  148.  
  149. /* parse reference */
  150. commentCells.find("div.reply_content")
  151. .each(function(index){
  152. var content = $(this).html();
  153. var replacementSpan = "<span class=\"v2exBR-reply-citation\" v2exBR-commentCellId=\"null\" v2exBR-citedPage=\"0\">";
  154. content = content.replace(/&gt;&gt;\d+(?=\s|<br)/g, replacementSpan + "$&" + "</span>");
  155. $(this).html(content);
  156. });
  157.  
  158. markReplyTrueOrder(replyOrderIdMap, commentCells);
  159. bindCitationElements(replyOrderIdMap);
  160. adjustFloorNo(commentCells);
  161.  
  162. /* register floor number functions */
  163. $(".no").hover(function(){
  164. $(this).addClass("v2exBR-reply-no-target");
  165. }, function(){
  166. $(this).removeClass("v2exBR-reply-no-target");
  167. }).click(function(e){
  168. var username = $(this).parent().next().next().children("a").text();
  169. var commentNo = $(this).text();
  170. makeCitedReply(username, commentNo);
  171. //to prevent the vanilla feature provided by v2ex.js to scroll up to the reply
  172. e.stopImmediatePropagation();
  173. });
  174.  
  175.  
  176. $(".v2exBR-reply-citation").hover(function(){
  177. var self = this;
  178. var commentCellId = $(self).attr("v2exBR-commentCellId");
  179. var numCitedPage = parseInt($(self).attr("v2exBR-citedPage"));
  180. var replyNo = parseInt($(self).attr("v2exBR-order"));
  181.  
  182. if (commentCellId === "null") return;
  183. if (commentCellId === "blocked") {
  184. $("#v2exBR_citation_div").html("引用的回复被隐藏或来自已屏蔽的用户。")
  185. .css({
  186. top:$(self).offset().top,
  187. left:$(self).offset().left + $(self).width()
  188. })
  189. .fadeIn(100);
  190.  
  191. return;
  192. }
  193.  
  194. var divPosTopOffset = window.getComputedStyle(self).getPropertyValue("font-size").match(/(\d+)px/)[1];
  195.  
  196. inflatePreviewBlock(repliesList[replyNo - 1], $("#v2exBR_citation_div"))
  197. .css({
  198. top:$(self).offset().top,
  199. left:$(self).offset().left + $(self).width()
  200. })
  201. .fadeIn(100);
  202. }, function(){
  203. $("#v2exBR_citation_div").fadeOut(100);
  204. });
  205.  
  206.  
  207. $(".v2exBR-reply-citation").click(function(){
  208. var commentCellId = $(this).attr("v2exBR-commentCellId");
  209. var numCitedPage = parseInt($(this).attr("v2exBR-citedPage"));
  210. if (commentCellId === "null" || commentCellId === "blocked") return;
  211.  
  212. if(numCitedPage == numCurrentPage){
  213. $("html, body").animate({
  214. scrollTop: $("#r_" + commentCellId).offset().top
  215. }, 500);
  216. }
  217. else{
  218. window.location.href = threadUrl + "?p=" + numCitedPage + "&v2exBR_commentCellId=" + commentCellId;
  219. }
  220.  
  221. });
  222.  
  223. (function(){
  224. var commentCellId = window.location.href.match(/v2exBR_commentCellId=(\d+)/);
  225. if (commentCellId != null){
  226. commentCellId = commentCellId[1];
  227. $("html, body").animate({
  228. scrollTop: $("#r_" + commentCellId).offset().top
  229. }, 500);
  230. }
  231. })();
  232.  
  233. function bindCitationElements(replyOrderIdMap){
  234. $("span.v2exBR-reply-citation").each(function(){
  235. var replyNo = parseInt($(this).text().match(/>>(\d+)/)[1]);
  236. var citedCommentCellId = "";
  237. var numCitedPage = Math.ceil(replyNo / 100);
  238.  
  239. citedCommentCellId = replyOrderIdMap[replyNo - 1];
  240. if (hiddenReplyIds.indexOf(citedCommentCellId) < 0) {
  241. registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
  242. }
  243. else if (SHOW_BLOCKED_REF) {
  244. registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
  245. }
  246. else {
  247. registerCitation(this, "blocked", numCitedPage, replyNo);
  248. }
  249.  
  250. });
  251. }
  252.  
  253.  
  254. function getCommentCellIdFromReplyNo(documentRoot, replyNo){
  255. var thisReplyNo = documentRoot.find(".no").filter(function () {
  256. return parseInt($(this).text()) == replyNo;
  257. });
  258. if (thisReplyNo.length > 0) {
  259. return thisReplyNo.parents("div.cell").get(0).id;
  260. }
  261. else {
  262. return "null";
  263. }
  264. }
  265.  
  266. function registerCitation(elem, id, numPage, order){
  267. $(elem).attr("v2exBR-commentCellId", id);
  268. $(elem).attr("v2exBR-citedPage", numPage);
  269. $(elem).attr("v2exBR-order", order);
  270. }
  271.  
  272. function makeCitedReply(username, commentNo){
  273. var replyContent = $("#reply_content");
  274. var oldContent = replyContent.val();
  275.  
  276. var userTag = "@" + username + " ";
  277. var commentTag = ">>" + commentNo + " \n";
  278.  
  279. var newContent = commentTag + userTag;
  280. if(oldContent.length > 0){
  281. if (oldContent != commentTag + userTag) {
  282. newContent = oldContent + "\n" + commentTag + userTag;
  283. }
  284. } else {
  285. newContent = commentTag + userTag;
  286. }
  287.  
  288. replyContent.focus();
  289. replyContent.val(newContent);
  290. moveEnd($("#reply_content"));
  291. }
  292.  
  293. //copied from v2ex.js in case this script gets executed before v2ex.js
  294. //is loaded
  295. var moveEnd = function (obj) {
  296. obj.focus();
  297. obj = obj.get(0);
  298. var len = obj.value.length;
  299. if (document.selection) {
  300. var sel = obj.createTextRange();
  301. sel.moveStart('character', len);
  302. sel.collapse();
  303. sel.select();
  304. } else if (typeof obj.selectionStart == 'number' && typeof obj.selectionEnd == 'number') {
  305. obj.selectionStart = obj.selectionEnd = len;
  306. }
  307. }
  308.  
  309.