rangy-inputs-mod.js

Modified rangyInputs plugin for selection & caret manipulation

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greatest.deepsurf.us/scripts/28239/181769/rangy-inputs-modjs.js

  1. /* HEAVILY MODIFIED 3/17/2017 from https://github.com/timdown/rangyinputs
  2. * - The code was unwrapped
  3. * - jQuery elements removed, updated to ES2015
  4. * - Unneeded code removed
  5. * - Added global variable "rangyInput"
  6. */
  7. /**
  8. * @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation
  9. * within textareas and text inputs.
  10. *
  11. * https://github.com/timdown/rangyinputs
  12. *
  13. * For range and selection features for contenteditable, see Rangy.
  14. * http://code.google.com/p/rangy/
  15. *
  16. * xxxx Depends on jQuery 1.0 or later. xxxxx
  17. *
  18. * Copyright 2014, Tim Down
  19. * Licensed under the MIT license.
  20. * Version: 1.2.0
  21. * Build date: 30 November 2014
  22. */
  23. /* jshint esnext:true */
  24. (() => {
  25.  
  26. window.rangyInput = {};
  27.  
  28. const UNDEF = "undefined";
  29. let getSelection, setSelection;
  30.  
  31. // Trio of isHost* functions taken from Peter Michaux's article:
  32. // State of the art browser scripting (https://goo.gl/w6HPyE)
  33. function isHostMethod(object, property) {
  34. var t = typeof object[property];
  35. return t === "function" ||
  36. (!!(t == "object" && object[property])) ||
  37. t == "unknown";
  38. }
  39. function isHostProperty(object, property) {
  40. return typeof(object[property]) != UNDEF;
  41. }
  42. function isHostObject(object, property) {
  43. return !!(typeof(object[property]) == "object" && object[property]);
  44. }
  45. function fail(reason) {
  46. if (window.console && window.console.log) {
  47. window.console.log(
  48. `RangyInputs not supported in your browser. Reason: ${reason}`
  49. );
  50. }
  51. }
  52.  
  53. function adjustOffsets(el, start, end) {
  54. if (start < 0) {
  55. start += el.value.length;
  56. }
  57. if (typeof end == UNDEF) {
  58. end = start;
  59. }
  60. if (end < 0) {
  61. end += el.value.length;
  62. }
  63. return { start: start, end: end };
  64. }
  65.  
  66. function makeSelection(el, start, end) {
  67. return {
  68. start: start,
  69. end: end,
  70. length: end - start,
  71. text: el.value.slice(start, end)
  72. };
  73. }
  74.  
  75. function getBody() {
  76. return isHostObject(document, "body") ?
  77. document.body :
  78. document.querySelector("body");
  79. }
  80.  
  81. window.rangyInput.init = () => {
  82. const testTextArea = document.createElement("textarea");
  83. getBody().appendChild(testTextArea);
  84.  
  85. if (
  86. isHostProperty(testTextArea, "selectionStart") &&
  87. isHostProperty(testTextArea, "selectionEnd")
  88. ) {
  89.  
  90. getSelection = el => {
  91. return makeSelection(el, el.selectionStart, el.selectionEnd);
  92. };
  93.  
  94. setSelection = (el, startOffset, endOffset) => {
  95. var offsets = adjustOffsets(el, startOffset, endOffset);
  96. el.selectionStart = offsets.start;
  97. el.selectionEnd = offsets.end;
  98. };
  99.  
  100. } else if (
  101. isHostMethod(testTextArea, "createTextRange") &&
  102. isHostObject(document, "selection") &&
  103. isHostMethod(document.selection, "createRange")
  104. ) {
  105.  
  106. getSelection = el => {
  107. let normalizedValue, textInputRange, len, endRange,
  108. start = 0,
  109. end = 0;
  110. const range = document.selection.createRange();
  111.  
  112. if (range && range.parentElement() == el) {
  113. len = el.value.length;
  114. normalizedValue = el.value.replace(/\r\n/g, "\n");
  115. textInputRange = el.createTextRange();
  116. textInputRange.moveToBookmark(range.getBookmark());
  117. endRange = el.createTextRange();
  118. endRange.collapse(false);
  119. if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
  120. start = end = len;
  121. } else {
  122. start = -textInputRange.moveStart("character", -len);
  123. start += normalizedValue.slice(0, start).split("\n").length - 1;
  124. if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
  125. end = len;
  126. } else {
  127. end = -textInputRange.moveEnd("character", -len);
  128. end += normalizedValue.slice(0, end).split("\n").length - 1;
  129. }
  130. }
  131. }
  132. return makeSelection(el, start, end);
  133. };
  134.  
  135. // Moving across a line break only counts as moving one character in a
  136. // TextRange, whereas a line break in the textarea value is two
  137. // characters. This function corrects for that by converting a text offset
  138. // into a range character offset by subtracting one character for every
  139. // line break in the textarea prior to the offset
  140. const offsetToRangeCharacterMove = function(el, offset) {
  141. return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
  142. };
  143.  
  144. setSelection = (el, startOffset, endOffset) => {
  145. const offsets = adjustOffsets(el, startOffset, endOffset),
  146. range = el.createTextRange(),
  147. startCharMove = offsetToRangeCharacterMove(el, offsets.start);
  148. range.collapse(true);
  149. if (offsets.start == offsets.end) {
  150. range.move("character", startCharMove);
  151. } else {
  152. range.moveEnd(
  153. "character",
  154. offsetToRangeCharacterMove(el, offsets.end)
  155. );
  156. range.moveStart("character", startCharMove);
  157. }
  158. range.select();
  159. };
  160.  
  161. } else {
  162. getBody().removeChild(testTextArea);
  163. fail("No means of finding text input caret position");
  164. return;
  165. }
  166.  
  167. // Clean up
  168. getBody().removeChild(testTextArea);
  169.  
  170. function getValueAfterPaste(el, text) {
  171. const val = el.value,
  172. sel = getSelection(el),
  173. selStart = sel.start;
  174. return {
  175. value: val.slice(0, selStart) + text + val.slice(sel.end),
  176. index: selStart,
  177. replaced: sel.text
  178. };
  179. }
  180.  
  181. function pasteTextWithCommand(el, text) {
  182. el.focus();
  183. const sel = getSelection(el);
  184.  
  185. // Hack to work around incorrect delete command when deleting the last
  186. // word on a line
  187. setSelection(el, sel.start, sel.end);
  188. if (text === "") {
  189. document.execCommand("delete", false, null);
  190. } else {
  191. document.execCommand("insertText", false, text);
  192. }
  193.  
  194. return {
  195. replaced: sel.text,
  196. index: sel.start
  197. };
  198. }
  199.  
  200. function pasteTextWithValueChange(el, text) {
  201. el.focus();
  202. const valueAfterPaste = getValueAfterPaste(el, text);
  203. el.value = valueAfterPaste.value;
  204. return valueAfterPaste;
  205. }
  206.  
  207. let pasteText = (el, text) => {
  208. const valueAfterPaste = getValueAfterPaste(el, text);
  209. try {
  210. const pasteInfo = pasteTextWithCommand(el, text);
  211. if (el.value == valueAfterPaste.value) {
  212. pasteText = pasteTextWithCommand;
  213. return pasteInfo;
  214. }
  215. } catch (ex) {
  216. // Do nothing and fall back to changing the value manually
  217. }
  218. pasteText = pasteTextWithValueChange;
  219. el.value = valueAfterPaste.value;
  220. return valueAfterPaste;
  221. };
  222.  
  223. function updateSelectionAfterInsert(el, startIndex, text, selBehaviour) {
  224. let endIndex = startIndex + text.length;
  225. selBehaviour = (typeof selBehaviour == "string") ?
  226. selBehaviour.toLowerCase() :
  227. "";
  228.  
  229. if (
  230. (selBehaviour == "collapsetoend" || selBehaviour == "select") &&
  231. /[\r\n]/.test(text)
  232. ) {
  233. // Find the length of the actual text inserted, which could vary
  234. // depending on how the browser deals with line breaks
  235. const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  236. endIndex = startIndex + normalizedText.length;
  237. const firstLineBreakIndex = startIndex + normalizedText.indexOf("\n");
  238.  
  239. if (
  240. el.value.slice(firstLineBreakIndex, firstLineBreakIndex + 2) == "\r\n"
  241. ) {
  242. // Browser uses \r\n, so we need to account for extra \r characters
  243. endIndex += normalizedText.match(/\n/g).length;
  244. }
  245. }
  246.  
  247. switch (selBehaviour) {
  248. case "collapsetostart":
  249. setSelection(el, startIndex, startIndex);
  250. break;
  251. case "collapsetoend":
  252. setSelection(el, endIndex, endIndex);
  253. break;
  254. case "select":
  255. setSelection(el, startIndex, endIndex);
  256. break;
  257. }
  258. }
  259.  
  260. window.rangyInput.surroundSelectedText = (el, before, after) => {
  261. if (typeof after == UNDEF) {
  262. after = before;
  263. }
  264. const sel = getSelection(el),
  265. pasteInfo = pasteText(el, before + sel.text + after);
  266. updateSelectionAfterInsert(
  267. el,
  268. pasteInfo.index + before.length,
  269. sel.text,
  270. "select"
  271. );
  272. };
  273.  
  274. window.rangyInput.indentSelectedText = (el, callback) => {
  275. const sel = getSelection(el),
  276. result = callback(sel.text),
  277. pasteInfo = pasteText(el, result);
  278. updateSelectionAfterInsert(el, pasteInfo.index, result, "select");
  279. };
  280. };
  281. })();