GitHub RTL Comment Blocks

A userscript that adds a button to insert RTL text blocks in comments

当前为 2016-06-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub RTL Comment Blocks
  3. // @version 1.0.0
  4. // @description A userscript that adds a button to insert RTL text blocks in comments
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace http://github.com/Mottie
  7. // @include https://github.com/*
  8. // @run-at document-idle
  9. // @grant GM_addStyle
  10. // @connect github.com
  11. // @author Rob Garrison
  12. // ==/UserScript==
  13. /*jshint unused:true, esnext:true */
  14. /* global GM_addStyle */
  15. (function() {
  16. "use strict";
  17.  
  18. let targets,
  19. busy = false;
  20.  
  21. // ${text} is not an es6 placeholder!
  22. const rtlBlock = '<div dir="rtl" align="right">${text}</div>',
  23.  
  24. icon = `
  25. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
  26. <path d="M14 3v8l-4-4m-7 7V6C1 6 0 5 0 3s1-3 3-3h7v2H9v12H7V2H5v12H3z"/>
  27. </svg>
  28. `;
  29.  
  30. // delegated binding; ignore clicks on svg & path
  31. GM_addStyle('.ghu-rtl > * { pointer-events:none; }');
  32.  
  33. // Add monospace font toggle
  34. function addRtlButton() {
  35. busy = true;
  36. var el, button,
  37. toolbars = document.querySelectorAll(".toolbar-commenting"),
  38. indx = toolbars.length;
  39. while (indx--) {
  40. el = toolbars[indx];
  41. if (!el.querySelector(".ghu-rtl")) {
  42. button = document.querySelector("button");
  43. button.type = "button";
  44. button.className = "ghu-rtl toolbar-item tooltipped tooltipped-n";
  45. button.setAttribute("aria-label", "RTL");
  46. button.setAttribute("tabindex", "-1");
  47. button.innerHTML = icon;
  48. el.insertBefore(button, el.childNodes[0]);
  49. }
  50. }
  51. busy = false;
  52. }
  53.  
  54. function closest(el, selector) {
  55. while (el && el.nodeName !== 'BODY' && !el.matches(selector)) {
  56. el = el.parentNode;
  57. }
  58. return el && el.matches(selector) ? el : [];
  59. }
  60.  
  61. function addBindings() {
  62. document.querySelector('body').addEventListener('click', function(event) {
  63. var textarea, str,
  64. target = event.target;
  65. if (target && target.classList.contains('ghu-rtl')) {
  66. textarea = closest(target, '.previewable-comment-form');
  67. textarea = textarea.querySelector('.comment-form-textarea');
  68. textarea.focus();
  69. str = rtlBlock.split('${text}');
  70. // insert text - before: "<div dir='rtl' align='right'>", after: "</div>"
  71. surroundSelectedText(textarea, str[0], str[1]);
  72. return false;
  73. }
  74. });
  75. }
  76.  
  77. targets = document.querySelectorAll([
  78. '#js-repo-pjax-container',
  79. '#js-pjax-container',
  80. '.js-preview-body'
  81. ].join(','));
  82.  
  83. Array.prototype.forEach.call(targets, function(target) {
  84. new MutationObserver(function(mutations) {
  85. mutations.forEach(function(mutation) {
  86. // preform checks before adding code wrap to minimize function calls
  87. if (!busy && mutation.target === target) {
  88. addRtlButton();
  89. }
  90. });
  91. }).observe(target, {
  92. childList: true,
  93. subtree: true
  94. });
  95. });
  96.  
  97. addBindings();
  98. addRtlButton();
  99.  
  100. /* HEAVILY MODIFIED from https://github.com/timdown/rangyinputs
  101. code was unwrapped & unneeded code was removed
  102. */
  103. /**
  104. * @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation within textareas and text inputs.
  105. *
  106. * https://github.com/timdown/rangyinputs
  107. *
  108. * For range and selection features for contenteditable, see Rangy.
  109. * http://code.google.com/p/rangy/
  110. *
  111. * Depends on jQuery 1.0 or later.
  112. *
  113. * Copyright 2014, Tim Down
  114. * Licensed under the MIT license.
  115. * Version: 1.2.0
  116. * Build date: 30 November 2014
  117. */
  118. var UNDEF = "undefined";
  119. var getSelection, setSelection, surroundSelectedText;
  120.  
  121. // Trio of isHost* functions taken from Peter Michaux's article:
  122. // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
  123. function isHostMethod(object, property) {
  124. var t = typeof object[property];
  125. return t === "function" || (!!(t == "object" && object[property])) || t == "unknown";
  126. }
  127. function isHostProperty(object, property) {
  128. return typeof(object[property]) != UNDEF;
  129. }
  130. function isHostObject(object, property) {
  131. return !!(typeof(object[property]) == "object" && object[property]);
  132. }
  133. function fail(reason) {
  134. if (window.console && window.console.log) {
  135. window.console.log("RangyInputs not supported in your browser. Reason: " + reason);
  136. }
  137. }
  138.  
  139. function adjustOffsets(el, start, end) {
  140. if (start < 0) {
  141. start += el.value.length;
  142. }
  143. if (typeof end == UNDEF) {
  144. end = start;
  145. }
  146. if (end < 0) {
  147. end += el.value.length;
  148. }
  149. return { start: start, end: end };
  150. }
  151.  
  152. function makeSelection(el, start, end) {
  153. return {
  154. start: start,
  155. end: end,
  156. length: end - start,
  157. text: el.value.slice(start, end)
  158. };
  159. }
  160.  
  161. function getBody() {
  162. return isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
  163. }
  164.  
  165. var testTextArea = document.createElement("textarea");
  166. getBody().appendChild(testTextArea);
  167.  
  168. if (isHostProperty(testTextArea, "selectionStart") && isHostProperty(testTextArea, "selectionEnd")) {
  169. getSelection = function(el) {
  170. var start = el.selectionStart, end = el.selectionEnd;
  171. return makeSelection(el, start, end);
  172. };
  173.  
  174. setSelection = function(el, startOffset, endOffset) {
  175. var offsets = adjustOffsets(el, startOffset, endOffset);
  176. el.selectionStart = offsets.start;
  177. el.selectionEnd = offsets.end;
  178. };
  179. } else if (isHostMethod(testTextArea, "createTextRange") && isHostObject(document, "selection") &&
  180. isHostMethod(document.selection, "createRange")) {
  181.  
  182. getSelection = function(el) {
  183. var start = 0, end = 0, normalizedValue, textInputRange, len, endRange;
  184. var range = document.selection.createRange();
  185.  
  186. if (range && range.parentElement() == el) {
  187. len = el.value.length;
  188.  
  189. normalizedValue = el.value.replace(/\r\n/g, "\n");
  190. textInputRange = el.createTextRange();
  191. textInputRange.moveToBookmark(range.getBookmark());
  192. endRange = el.createTextRange();
  193. endRange.collapse(false);
  194. if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
  195. start = end = len;
  196. } else {
  197. start = -textInputRange.moveStart("character", -len);
  198. start += normalizedValue.slice(0, start).split("\n").length - 1;
  199. if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
  200. end = len;
  201. } else {
  202. end = -textInputRange.moveEnd("character", -len);
  203. end += normalizedValue.slice(0, end).split("\n").length - 1;
  204. }
  205. }
  206. }
  207.  
  208. return makeSelection(el, start, end);
  209. };
  210.  
  211. // Moving across a line break only counts as moving one character in a TextRange, whereas a line break in
  212. // the textarea value is two characters. This function corrects for that by converting a text offset into a
  213. // range character offset by subtracting one character for every line break in the textarea prior to the
  214. // offset
  215. var offsetToRangeCharacterMove = function(el, offset) {
  216. return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
  217. };
  218.  
  219. setSelection = function(el, startOffset, endOffset) {
  220. var offsets = adjustOffsets(el, startOffset, endOffset);
  221. var range = el.createTextRange();
  222. var startCharMove = offsetToRangeCharacterMove(el, offsets.start);
  223. range.collapse(true);
  224. if (offsets.start == offsets.end) {
  225. range.move("character", startCharMove);
  226. } else {
  227. range.moveEnd("character", offsetToRangeCharacterMove(el, offsets.end));
  228. range.moveStart("character", startCharMove);
  229. }
  230. range.select();
  231. };
  232. } else {
  233. getBody().removeChild(testTextArea);
  234. fail("No means of finding text input caret position");
  235. return;
  236. }
  237. // Clean up
  238. getBody().removeChild(testTextArea);
  239.  
  240. function getValueAfterPaste(el, text) {
  241. var val = el.value, sel = getSelection(el), selStart = sel.start;
  242. return {
  243. value: val.slice(0, selStart) + text + val.slice(sel.end),
  244. index: selStart,
  245. replaced: sel.text
  246. };
  247. }
  248.  
  249. function pasteTextWithCommand(el, text) {
  250. el.focus();
  251. var sel = getSelection(el);
  252.  
  253. // Hack to work around incorrect delete command when deleting the last word on a line
  254. setSelection(el, sel.start, sel.end);
  255. if (text === "") {
  256. document.execCommand("delete", false, null);
  257. } else {
  258. document.execCommand("insertText", false, text);
  259. }
  260.  
  261. return {
  262. replaced: sel.text,
  263. index: sel.start
  264. };
  265. }
  266.  
  267. function pasteTextWithValueChange(el, text) {
  268. el.focus();
  269. var valueAfterPaste = getValueAfterPaste(el, text);
  270. el.value = valueAfterPaste.value;
  271. return valueAfterPaste;
  272. }
  273.  
  274. var pasteText = function(el, text) {
  275. var valueAfterPaste = getValueAfterPaste(el, text);
  276. try {
  277. var pasteInfo = pasteTextWithCommand(el, text);
  278. if (el.value == valueAfterPaste.value) {
  279. pasteText = pasteTextWithCommand;
  280. return pasteInfo;
  281. }
  282. } catch (ex) {
  283. // Do nothing and fall back to changing the value manually
  284. }
  285. pasteText = pasteTextWithValueChange;
  286. el.value = valueAfterPaste.value;
  287. return valueAfterPaste;
  288. };
  289.  
  290. var updateSelectionAfterInsert = function(el, startIndex, text, selectionBehaviour) {
  291. var endIndex = startIndex + text.length;
  292.  
  293. selectionBehaviour = (typeof selectionBehaviour == "string") ?
  294. selectionBehaviour.toLowerCase() : "";
  295.  
  296. if ((selectionBehaviour == "collapsetoend" || selectionBehaviour == "select") && /[\r\n]/.test(text)) {
  297. // Find the length of the actual text inserted, which could vary
  298. // depending on how the browser deals with line breaks
  299. var normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  300. endIndex = startIndex + normalizedText.length;
  301. var firstLineBreakIndex = startIndex + normalizedText.indexOf("\n");
  302.  
  303. if (el.value.slice(firstLineBreakIndex, firstLineBreakIndex + 2) == "\r\n") {
  304. // Browser uses \r\n, so we need to account for extra \r characters
  305. endIndex += normalizedText.match(/\n/g).length;
  306. }
  307. }
  308.  
  309. switch (selectionBehaviour) {
  310. case "collapsetostart":
  311. setSelection(el, startIndex, startIndex);
  312. break;
  313. case "collapsetoend":
  314. setSelection(el, endIndex, endIndex);
  315. break;
  316. case "select":
  317. setSelection(el, startIndex, endIndex);
  318. break;
  319. }
  320. };
  321.  
  322. surroundSelectedText = function(el, before, after, selectionBehaviour) {
  323. if (typeof after == UNDEF) {
  324. after = before;
  325. }
  326. var sel = getSelection(el);
  327. var pasteInfo = pasteText(el, before + sel.text + after);
  328. updateSelectionAfterInsert(el, pasteInfo.index + before.length, sel.text, selectionBehaviour || "select");
  329. };
  330.  
  331. })();