GitHub Indent-NewLine Comments

A userscript that allows you to indent & outdent blocks and auto indent for new line in the comment editor

当前为 2020-03-10 提交的版本,查看 最新版本

  1. //
  2. // ==UserScript==
  3. // @name GitHub Indent-NewLine Comments
  4. // @version 0.0.1
  5. // @description A userscript that allows you to indent & outdent blocks and auto indent for new line in the comment editor
  6. // @license MIT
  7. // @author ly525
  8. // @namespace https://github.com/ly525
  9. // @include https://github.com/*
  10. // @include https://gist.github.com/*
  11. // @run-at document-idle
  12. // @grant GM_addStyle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_registerMenuCommand
  16. // @connect github.com
  17. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=666427
  18. // @icon https://github.githubassets.com/pinned-octocat.svg
  19. // ==/UserScript==
  20.  
  21. /*
  22. * ly525 says:
  23. * HEAVILY MODIFIED 3/8/2020 from https://greasyfork.org/zh-CN/scripts/28176-github-indent-comments
  24. * Thanks a lot!
  25. */
  26.  
  27. /* HEAVILY MODIFIED 3/17/2017 from https://github.com/timdown/rangyinputs
  28. * - The code was unwrapped
  29. * - jQuery elements removed, updated to ES2015
  30. * - Unneeded code removed
  31. * - Added global variable "rangyInput"
  32. */
  33.  
  34.  
  35. /**
  36. * @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation
  37. * within textareas and text inputs.
  38. *
  39. * https://github.com/timdown/rangyinputs
  40. *
  41. * For range and selection features for contenteditable, see Rangy.
  42. * http://code.google.com/p/rangy/
  43. *
  44. * xxxx Depends on jQuery 1.0 or later. xxxxx
  45. *
  46. * Copyright 2014, Tim Down
  47. * Licensed under the MIT license.
  48. * Version: 1.2.0
  49. * Build date: 30 November 2014
  50. */
  51. /* jshint esnext:true */
  52. (() => {
  53.  
  54. window.rangyInput = {};
  55.  
  56. const UNDEF = "undefined";
  57. let getSelection, setSelection;
  58.  
  59. // Trio of isHost* functions taken from Peter Michaux's article:
  60. // State of the art browser scripting (https://goo.gl/w6HPyE)
  61. function isHostMethod(object, property) {
  62. var t = typeof object[property];
  63. return t === "function" ||
  64. (!!(t == "object" && object[property])) ||
  65. t == "unknown";
  66. }
  67. function isHostProperty(object, property) {
  68. return typeof(object[property]) != UNDEF;
  69. }
  70. function isHostObject(object, property) {
  71. return !!(typeof(object[property]) == "object" && object[property]);
  72. }
  73. function fail(reason) {
  74. if (window.console && window.console.log) {
  75. window.console.log(
  76. `RangyInputs not supported in your browser. Reason: ${reason}`
  77. );
  78. }
  79. }
  80.  
  81. function adjustOffsets(el, start, end) {
  82. if (start < 0) {
  83. start += el.value.length;
  84. }
  85. if (typeof end == UNDEF) {
  86. end = start;
  87. }
  88. if (end < 0) {
  89. end += el.value.length;
  90. }
  91. return { start: start, end: end };
  92. }
  93.  
  94. function makeSelection(el, start, end) {
  95. return {
  96. start: start,
  97. end: end,
  98. length: end - start,
  99. text: el.value.slice(start, end)
  100. };
  101. }
  102.  
  103. function getBody() {
  104. return isHostObject(document, "body") ?
  105. document.body :
  106. document.querySelector("body");
  107. }
  108.  
  109. window.rangyInput.init = () => {
  110. const testTextArea = document.createElement("textarea");
  111. getBody().appendChild(testTextArea);
  112.  
  113. if (
  114. isHostProperty(testTextArea, "selectionStart") &&
  115. isHostProperty(testTextArea, "selectionEnd")
  116. ) {
  117.  
  118. getSelection = el => {
  119. return makeSelection(el, el.selectionStart, el.selectionEnd);
  120. };
  121.  
  122. setSelection = (el, startOffset, endOffset) => {
  123. var offsets = adjustOffsets(el, startOffset, endOffset);
  124. el.selectionStart = offsets.start;
  125. el.selectionEnd = offsets.end;
  126. };
  127.  
  128. } else if (
  129. isHostMethod(testTextArea, "createTextRange") &&
  130. isHostObject(document, "selection") &&
  131. isHostMethod(document.selection, "createRange")
  132. ) {
  133.  
  134. getSelection = el => {
  135. let normalizedValue, textInputRange, len, endRange,
  136. start = 0,
  137. end = 0;
  138. const range = document.selection.createRange();
  139.  
  140. if (range && range.parentElement() == el) {
  141. len = el.value.length;
  142. normalizedValue = el.value.replace(/\r\n/g, "\n");
  143. textInputRange = el.createTextRange();
  144. textInputRange.moveToBookmark(range.getBookmark());
  145. endRange = el.createTextRange();
  146. endRange.collapse(false);
  147. if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
  148. start = end = len;
  149. } else {
  150. start = -textInputRange.moveStart("character", -len);
  151. start += normalizedValue.slice(0, start).split("\n").length - 1;
  152. if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
  153. end = len;
  154. } else {
  155. end = -textInputRange.moveEnd("character", -len);
  156. end += normalizedValue.slice(0, end).split("\n").length - 1;
  157. }
  158. }
  159. }
  160. return makeSelection(el, start, end);
  161. };
  162.  
  163. // Moving across a line break only counts as moving one character in a
  164. // TextRange, whereas a line break in the textarea value is two
  165. // characters. This function corrects for that by converting a text offset
  166. // into a range character offset by subtracting one character for every
  167. // line break in the textarea prior to the offset
  168. const offsetToRangeCharacterMove = function(el, offset) {
  169. return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
  170. };
  171.  
  172. setSelection = (el, startOffset, endOffset) => {
  173. const offsets = adjustOffsets(el, startOffset, endOffset),
  174. range = el.createTextRange(),
  175. startCharMove = offsetToRangeCharacterMove(el, offsets.start);
  176. range.collapse(true);
  177. if (offsets.start == offsets.end) {
  178. range.move("character", startCharMove);
  179. } else {
  180. range.moveEnd(
  181. "character",
  182. offsetToRangeCharacterMove(el, offsets.end)
  183. );
  184. range.moveStart("character", startCharMove);
  185. }
  186. range.select();
  187. };
  188.  
  189. } else {
  190. getBody().removeChild(testTextArea);
  191. fail("No means of finding text input caret position");
  192. return;
  193. }
  194.  
  195. // Clean up
  196. getBody().removeChild(testTextArea);
  197.  
  198. function getValueAfterPaste(el, text) {
  199. const val = el.value,
  200. sel = getSelection(el),
  201. selStart = sel.start;
  202. return {
  203. value: val.slice(0, selStart) + text + val.slice(sel.end),
  204. index: selStart,
  205. replaced: sel.text
  206. };
  207. }
  208.  
  209. function pasteTextWithCommand(el, text) {
  210. el.focus();
  211. const sel = getSelection(el);
  212.  
  213. // Hack to work around incorrect delete command when deleting the last
  214. // word on a line
  215. setSelection(el, sel.start, sel.end);
  216. if (text === "") {
  217. document.execCommand("delete", false, null);
  218. } else {
  219. document.execCommand("insertText", false, text);
  220. }
  221.  
  222. return {
  223. replaced: sel.text,
  224. index: sel.start
  225. };
  226. }
  227.  
  228. function pasteTextWithValueChange(el, text) {
  229. el.focus();
  230. const valueAfterPaste = getValueAfterPaste(el, text);
  231. el.value = valueAfterPaste.value;
  232. return valueAfterPaste;
  233. }
  234.  
  235. let pasteText = (el, text) => {
  236. const valueAfterPaste = getValueAfterPaste(el, text);
  237. try {
  238. const pasteInfo = pasteTextWithCommand(el, text);
  239. if (el.value == valueAfterPaste.value) {
  240. pasteText = pasteTextWithCommand;
  241. return pasteInfo;
  242. }
  243. } catch (ex) {
  244. // Do nothing and fall back to changing the value manually
  245. }
  246. pasteText = pasteTextWithValueChange;
  247. el.value = valueAfterPaste.value;
  248. return valueAfterPaste;
  249. };
  250.  
  251. function updateSelectionAfterInsert(el, startIndex, text, selBehaviour) {
  252. let endIndex = startIndex + text.length;
  253. selBehaviour = (typeof selBehaviour == "string") ?
  254. selBehaviour.toLowerCase() :
  255. "";
  256.  
  257. if (
  258. (selBehaviour == "collapsetoend" || selBehaviour == "select") &&
  259. /[\r\n]/.test(text)
  260. ) {
  261. // Find the length of the actual text inserted, which could vary
  262. // depending on how the browser deals with line breaks
  263. const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  264. endIndex = startIndex + normalizedText.length;
  265. const firstLineBreakIndex = startIndex + normalizedText.indexOf("\n");
  266.  
  267. if (
  268. el.value.slice(firstLineBreakIndex, firstLineBreakIndex + 2) == "\r\n"
  269. ) {
  270. // Browser uses \r\n, so we need to account for extra \r characters
  271. endIndex += normalizedText.match(/\n/g).length;
  272. }
  273. }
  274.  
  275. switch (selBehaviour) {
  276. case "collapsetostart":
  277. setSelection(el, startIndex, startIndex);
  278. break;
  279. case "collapsetoend":
  280. setSelection(el, endIndex, endIndex);
  281. break;
  282. case "select":
  283. setSelection(el, startIndex, endIndex);
  284. break;
  285. }
  286. }
  287.  
  288. window.rangyInput.surroundSelectedText = (el, before, after) => {
  289. if (typeof after == UNDEF) {
  290. after = before;
  291. }
  292. const sel = getSelection(el),
  293. pasteInfo = pasteText(el, before + sel.text + after);
  294. updateSelectionAfterInsert(
  295. el,
  296. pasteInfo.index + before.length,
  297. sel.text,
  298. "select"
  299. );
  300. };
  301.  
  302. window.rangyInput.indentSelectedText = (el, callback) => {
  303. const sel = getSelection(el),
  304. result = callback(sel.text),
  305. pasteInfo = pasteText(el, result);
  306. //「修改1」主要修改了这里,将updateSelectionAfterInsert的值由「select」改为「collapsetoend」
  307. updateSelectionAfterInsert(el, pasteInfo.index, result, "collapsetoend");
  308. };
  309. };
  310. })();
  311.  
  312. (() => {
  313. "use strict";
  314.  
  315. let spaceSize = GM_getValue("space-size", 2);
  316.  
  317. const icons = {
  318. indent: `
  319. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16">
  320. <path d="M12 13c0 .6 0 1-.9 1H.9c-.9 0-.9-.4-.9-1s0-1 .9-1h10.2c.88 0 .88.4.88 1zM.92 4h10.2C12 4 12 3.6 12 3s0-1-.9-1H.92c-.9 0-.9.4-.9 1s0 1 .9 1zM11.5 7h-5C6 7 6 7.4 6 8s0 1 .5 1h5c.5 0 .5-.4.5-1s0-1-.5-1zm-7 1L0 5v6z"/>
  321. </svg>`,
  322. outdent: `
  323. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16">
  324. <path d="M12 13c0 .6 0 1-.9 1H.9c-.9 0-.9-.4-.9-1s0-1 .9-1h10.2c.88 0 .88.4.88 1zM.92 4h10.2C12 4 12 3.6 12 3s0-1-.9-1H.92c-.9 0-.9.4-.9 1s0 1 .9 1zm10.7 3H6.4c-.46 0-.4.4-.4 1s-.06 1 .4 1h5.2c.47 0 .4-.4.4-1s.07-1-.4-1zM0 8l4.5-3v6z"/>
  325. </svg>`
  326. };
  327.  
  328. GM_addStyle(".ghio-in-outdent * { pointer-events:none; }");
  329.  
  330. // Add indent & outdent buttons
  331. function addButtons() {
  332. createButton("Outdent");
  333. createButton("Indent");
  334. }
  335.  
  336. function createButton(name) {
  337. const toolbars = $$(".toolbar-commenting"),
  338. nam = name.toLowerCase(),
  339. button = document.createElement("button");
  340. let el,
  341. indx = toolbars.length;
  342. if (indx) {
  343. button.type = "button";
  344. button.className = `ghio-${nam.toLowerCase()} ghio-in-outdent toolbar-item tooltipped tooltipped-n`;
  345. button.setAttribute("aria-label", `${name} Selected Text`);
  346. button.setAttribute("tabindex", "-1");
  347. button.innerHTML = icons[nam.toLowerCase()];
  348. while (indx--) {
  349. el = toolbars[indx];
  350. if (!$(`.ghio-${nam.toLowerCase()}`, el)) {
  351. el.insertBefore(button.cloneNode(true), el.childNodes[0]);
  352. }
  353. }
  354. }
  355. }
  356.  
  357. function indent(text) {
  358. let result = [],
  359. block = new Array(parseInt(spaceSize, 10) + 1).join(" ");
  360. (text || "").split(/\r*\n/).forEach(line => {
  361. result.push(block + line);
  362. });
  363. return result.join("\n");
  364. }
  365.  
  366. function outdent(text) {
  367. let regex = new RegExp(`^(\x20{1,${spaceSize}}|\xA0{1,${spaceSize}}|\x09)`),
  368. result = [];
  369. (text || "").split(/\r*\n/).forEach(line => {
  370. result.push(line.replace(regex, ""));
  371. });
  372. return result.join("\n");
  373. }
  374.  
  375. function addBindings() {
  376. window.rangyInput.init();
  377. saveTabSize();
  378. $("body").addEventListener("click", event => {
  379. let textarea,
  380. target = event.target;
  381. if (target && target.classList.contains("ghio-in-outdent")) {
  382. textarea = closest(".previewable-comment-form", target);
  383. textarea = $(".comment-form-textarea", textarea);
  384. textarea.focus();
  385. setTimeout(() => {
  386. window.rangyInput.indentSelectedText(
  387. textarea,
  388. target.classList.contains("ghio-indent") ? indent : outdent
  389. );
  390. }, 100);
  391. return false;
  392. }
  393. });
  394. // Add Tab & Shift + Tab
  395. $("body").addEventListener("keydown", event => {
  396. if (event.key === "Tab") {
  397. let target = event.target;
  398. if (target && target.classList.contains("comment-form-textarea")) {
  399. event.preventDefault();
  400. target.focus();
  401. setTimeout(() => {
  402. window.rangyInput.indentSelectedText(
  403. target,
  404. // shift + tab = outdent
  405. event.getModifierState("Shift") ? outdent : indent
  406. );
  407. }, 100);
  408. }
  409. }
  410. // 「修改2」增加 New Line 自动对齐行为
  411. // https://stackoverflow.com/questions/5743916/how-to-add-autoindent-to-html-textarea
  412. else if (event.key === 'Enter') {
  413. let target = event.target;
  414. if (target && target.classList.contains("comment-form-textarea")) {
  415. // event.preventDefault();
  416. setTimeout((that) => {
  417. var start = that.selectionStart;
  418. var v = that.value;
  419. var thisLine = "";
  420. var indentation = 0;
  421. for (let i = start - 2; i >= 0 && v[i] != "\n"; i--) {
  422. thisLine = v[i] + thisLine;
  423. }
  424. for (let i = 0; i < thisLine.length && thisLine[i] == " "; i++) {
  425.  
  426. indentation++;
  427. }
  428. that.value = v.slice(0, start) + " ".repeat(indentation) + v.slice(start);
  429. that.selectionStart = start + indentation;
  430. that.selectionEnd = start + indentation;
  431. }, 0.01, target);
  432. }
  433. }
  434. });
  435. }
  436.  
  437. function saveTabSize() {
  438. let $el = $(".gh-indent-size");
  439. if (!$el) {
  440. $el = document.createElement("style");
  441. $el.setAttribute("rel", "stylesheet");
  442. $el.className = "gh-indent-size";
  443. document.querySelector("head").appendChild($el);
  444. }
  445. $el.innerHTML = `.comment-form-textarea { tab-size:${spaceSize}; }`;
  446. }
  447.  
  448. function $(selector, el) {
  449. return (el || document).querySelector(selector);
  450. }
  451.  
  452. function $$(selector, el) {
  453. return Array.from((el || document).querySelectorAll(selector));
  454. }
  455.  
  456. function closest(selector, el) {
  457. while (el && el.nodeType === 1) {
  458. if (el.matches(selector)) {
  459. return el;
  460. }
  461. el = el.parentNode;
  462. }
  463. return null;
  464. }
  465.  
  466. // Add GM options
  467. GM_registerMenuCommand(
  468. "Indent or outdent size",
  469. () => {
  470. const spaces = GM_getValue("indentOutdentSize", spaceSize);
  471. let val = prompt("Enter number of spaces to indent or outdent:", spaces);
  472. if (val !== null && typeof val === "string") {
  473. spaceSize = val;
  474. GM_setValue("space-size", val);
  475. saveTabSize();
  476. }
  477. }
  478. );
  479.  
  480. document.addEventListener("ghmo:container", addButtons);
  481. addBindings();
  482. addButtons();
  483. })();