Advertisement
Guest User

bililiteRange backup

a guest
Mar 15th, 2023
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.11 KB | Source Code | 0 0
  1. // bililiteRange.js 2023-03-05
  2.  
  3. // bililiteRange.js commit ef1c276
  4. 'use strict';
  5.  
  6. let bililiteRange; // create one global variable
  7.  
  8. (function(){
  9.  
  10. const datakey = Symbol(); // use as the key to modify elements.
  11.  
  12. bililiteRange = function(el){
  13. var ret;
  14. if (el.setSelectionRange){
  15. // Element is an input or textarea
  16. // note that some input elements do not allow selections
  17. try{
  18. el.selectionStart = el.selectionStart;
  19. ret = new InputRange();
  20. }catch(e){
  21. ret = new NothingRange();
  22. }
  23. }else{
  24. // Standards, with any other kind of element
  25. ret = new W3CRange();
  26. }
  27. ret._el = el;
  28. // determine parent document, as implemented by John McLear <john@mclear.co.uk>
  29. ret._doc = el.ownerDocument;
  30. ret._win = ret._doc.defaultView;
  31. ret._bounds = [0, ret.length];
  32.  
  33.  
  34. if (!(el[datakey])){ // we haven't processed this element yet
  35. const data = createDataObject (el);
  36. startupHooks.forEach ( hook => hook (el, ret, data) );
  37. }
  38. return ret;
  39. }
  40.  
  41. bililiteRange.version = 3.2;
  42.  
  43. const startupHooks = new Set();
  44. bililiteRange.addStartupHook = fn => startupHooks.add(fn);
  45. startupHooks.add (trackSelection);
  46. startupHooks.add (fixInputEvents);
  47. startupHooks.add (correctNewlines);
  48.  
  49. // selection tracking. We want clicks to set the selection to the clicked location but tabbing in or element.focus() should restore
  50. // the selection to what it was.
  51. // There's no good way to do this. I just assume that a mousedown (or a drag and drop
  52. // into the element) within 100 ms of the focus event must have caused the focus, and
  53. // therefore we should not restore the selection.
  54. function trackSelection (element, range, data){
  55. data.selection = [0,0];
  56. range.listen('focusout', evt => data.selection = range._nativeSelection() );
  57. range.listen('mousedown', evt => data.mousetime = evt.timeStamp );
  58. range.listen('drop', evt => data.mousetime = evt.timeStamp );
  59. range.listen('focus', evt => {
  60. if ('mousetime' in data && evt.timeStamp - data.mousetime < 100) return;
  61. range._nativeSelect(range._nativeRange(data.selection))
  62. });
  63. }
  64.  
  65. function fixInputEvents (element, range, data){
  66. // DOM 3 input events, https://www.w3.org/TR/input-events-1/
  67. // have a data field with the text inserted, but that isn't enough to fully describe the change;
  68. // we need to know the old text (or at least its length)
  69. // and *where* the new text was inserted.
  70. // So we enhance input events with that information.
  71. // the "newText" should always be the same as the 'data' field, if it is defined
  72. data.oldText = range.all();
  73. data.liveRanges = new Set();
  74. range.listen('input', evt => {
  75. const newText = range.all();
  76. if (!evt.bililiteRange){
  77. evt.bililiteRange = diff (data.oldText, newText);
  78. if (evt.bililiteRange.unchanged){
  79. // no change. Assume that whatever happened, happened at the selection point (and use whatever data the browser gives us).
  80. evt.bililiteRange.start = range.clone().bounds('selection')[1] - (evt.data || '').length;
  81. }
  82. }
  83. data.oldText = newText;
  84.  
  85. // Also update live ranges on this element
  86. data.liveRanges.forEach( rng => {
  87. const start = evt.bililiteRange.start;
  88. const oldend = start + evt.bililiteRange.oldText.length;
  89. const newend = start + evt.bililiteRange.newText.length;
  90. // adjust bounds; this tries to emulate the algorithm that Microsoft Word uses for bookmarks
  91. let [b0, b1] = rng.bounds();
  92. if (b0 <= start){
  93. // no change
  94. }else if (b0 > oldend){
  95. b0 += newend - oldend;
  96. }else{
  97. b0 = newend;
  98. }
  99. if (b1 < start){
  100. // no change
  101. }else if (b1 >= oldend){
  102. b1 += newend - oldend;
  103. }else{
  104. b1 = start;
  105. }
  106. rng.bounds([b0, b1]);
  107. })
  108. });
  109. }
  110.  
  111. function diff (oldText, newText){
  112. // Try to find the changed text, assuming it was a continuous change
  113. if (oldText == newText){
  114. return {
  115. unchanged: true,
  116. start: 0,
  117. oldText,
  118. newText
  119. }
  120. }
  121.  
  122. const oldlen = oldText.length;
  123. const newlen = newText.length;
  124. for (var i = 0; i < newlen && i < oldlen; ++i){
  125. if (newText.charAt(i) != oldText.charAt(i)) break;
  126. }
  127. const start = i;
  128. for (i = 0; i < newlen && i < oldlen; ++i){
  129. let newpos = newlen-i-1, oldpos = oldlen-i-1;
  130. if (newpos < start || oldpos < start) break;
  131. if (newText.charAt(newpos) != oldText.charAt(oldpos)) break;
  132. }
  133. const oldend = oldlen-i;
  134. const newend = newlen-i;
  135. return {
  136. start,
  137. oldText: oldText.slice(start, oldend),
  138. newText: newText.slice(start, newend)
  139. }
  140. };
  141. bililiteRange.diff = diff; // expose
  142.  
  143. function correctNewlines (element, range, data){
  144. // we need to insert newlines rather than create new elements, so character-based calculations work
  145. range.listen('paste', evt => {
  146. if (evt.defaultPrevented) return;
  147. // windows adds \r's to clipboard!
  148. range.clone().bounds('selection').
  149. text(evt.clipboardData.getData("text/plain").replace(/\r/g,''), {inputType: 'insertFromPaste'}).
  150. bounds('endbounds').
  151. select().
  152. scrollIntoView();
  153. evt.preventDefault();
  154. });
  155. range.listen('keydown', function(evt){
  156. if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
  157. if (evt.defaultPrevented) return;
  158. if (evt.key == 'Enter'){
  159. range.clone().bounds('selection').
  160. text('\n', {inputType: 'insertLineBreak'}).
  161. bounds('endbounds').
  162. select().
  163. scrollIntoView();
  164. evt.preventDefault();
  165. }
  166. });
  167. }
  168.  
  169. // convenience function for defining input events
  170. function inputEventInit(type, oldText, newText, start, inputType){
  171. return {
  172. type,
  173. inputType,
  174. data: newText,
  175. bubbles: true,
  176. bililiteRange: {
  177. unchanged: (oldText == newText),
  178. start,
  179. oldText,
  180. newText
  181. }
  182. };
  183. }
  184.  
  185. // base class
  186. function Range(){}
  187. Range.prototype = {
  188. // allow use of range[0] and range[1] for start and end of bounds
  189. get 0(){
  190. return this.bounds()[0];
  191. },
  192. set 0(x){
  193. this.bounds([x, this[1]]);
  194. return x;
  195. },
  196. get 1(){
  197. return this.bounds()[1];
  198. },
  199. set 1(x){
  200. this.bounds([this[0], x]);
  201. return x;
  202. },
  203. all: function(text){
  204. if (arguments.length){
  205. return this.bounds('all').text(text, {inputType: 'insertReplacementText'});
  206. }else{
  207. return this._el[this._textProp];
  208. }
  209. },
  210. bounds: function(s){
  211. if (typeof s === 'number'){
  212. this._bounds = [s,s];
  213. }else if (bililiteRange.bounds[s]){
  214. this.bounds(bililiteRange.bounds[s].apply(this, arguments));
  215. }else if (s && s.bounds){
  216. this._bounds = s.bounds(); // copy bounds from an existing range
  217. }else if (s){
  218. this._bounds = s; // don't do error checking now; things may change at a moment's notice
  219. }else{
  220. // constrain bounds now
  221. var b = [
  222. Math.max(0, Math.min (this.length, this._bounds[0])),
  223. Math.max(0, Math.min (this.length, this._bounds[1]))
  224. ];
  225. b[1] = Math.max(b[0], b[1]);
  226. return b;
  227. }
  228. return this; // allow for chaining
  229. },
  230. clone: function(){
  231. return bililiteRange(this._el).bounds(this.bounds());
  232. },
  233. get data(){
  234. return this._el[datakey];
  235. },
  236. dispatch: function(opts = {}){
  237. var event = new Event (opts.type, opts);
  238. event.view = this._win;
  239. for (let prop in opts) try { event[prop] = opts[prop] } catch(e){}; // ignore read-only errors for properties that were copied in the previous line
  240. this._el.dispatchEvent(event); // note that the event handlers will be called synchronously, before the "return this;"
  241. return this;
  242. },
  243. get document() {
  244. return this._doc;
  245. },
  246. dontlisten: function (type, func = console.log, target){
  247. target ??= this._el;
  248. target.removeEventListener(type, func);
  249. return this;
  250. },
  251. get element() {
  252. return this._el
  253. },
  254. get length() {
  255. return this._el[this._textProp].length;
  256. },
  257. live (on = true){
  258. this.data.liveRanges[on ? 'add' : 'delete'](this);
  259. return this;
  260. },
  261. listen: function (type, func = console.log, target){
  262. target ??= this._el;
  263. target.addEventListener(type, func);
  264. return this;
  265. },
  266. scrollIntoView() {
  267. var top = this.top();
  268. // note that for TEXTAREA's, this.top() will do the scrolling and the following is irrelevant.
  269. // scroll into position if necessary
  270. if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){
  271. this._el.scrollTop = top;
  272. }
  273. return this;
  274. },
  275. select: function(){
  276. var b = this.data.selection = this.bounds();
  277. if (this._el === this._doc.activeElement){
  278. // only actually select if this element is active!
  279. this._nativeSelect(this._nativeRange(b));
  280. }
  281. this.dispatch({type: 'select', bubbles: true});
  282. return this; // allow for chaining
  283. },
  284. selection: function(text){
  285. if (arguments.length){
  286. return this.bounds('selection').text(text).bounds('endbounds').select();
  287. }else{
  288. return this.bounds('selection').text();
  289. }
  290. },
  291. sendkeys: function (text){
  292. this.data.sendkeysOriginalText = this.text();
  293. this.data.sendkeysBounds = undefined;
  294. function simplechar (rng, c){
  295. if (/^{[^}]*}$/.test(c)) c = c.slice(1,-1); // deal with unknown {key}s
  296. rng.text(c).bounds('endbounds');
  297. }
  298. text.replace(/{[^}]*}|[^{]+|{/g, part => (bililiteRange.sendkeys[part] || simplechar)(this, part, simplechar) );
  299. this.bounds(this.data.sendkeysBounds);
  300. this.dispatch({type: 'sendkeys', detail: text});
  301. return this;
  302. },
  303. text: function(text, {inputType = 'insertText'} = {}){
  304. if ( text !== undefined ){
  305. let eventparams = [this.text(), text, this[0], inputType];
  306. this.dispatch (inputEventInit('beforeinput',...eventparams));
  307. this._nativeSetText(text, this._nativeRange(this.bounds()));
  308. this[1] = this[0]+text.length;
  309. this.dispatch (inputEventInit('input',...eventparams));
  310. return this; // allow for chaining
  311. }else{
  312. return this._nativeGetText(this._nativeRange(this.bounds()));
  313. }
  314. },
  315. top: function(){
  316. return this._nativeTop(this._nativeRange(this.bounds()));
  317. },
  318. get window() {
  319. return this._win;
  320. },
  321. wrap: function (n){
  322. this._nativeWrap(n, this._nativeRange(this.bounds()));
  323. return this;
  324. },
  325. };
  326.  
  327. // allow extensions ala jQuery
  328. bililiteRange.prototype = Range.prototype;
  329. bililiteRange.extend = function(fns){
  330. Object.assign(bililiteRange.prototype, fns);
  331. };
  332.  
  333. bililiteRange.override = (name, fn) => {
  334. const oldfn = bililiteRange.prototype[name];
  335. bililiteRange.prototype[name] = function(){
  336. const oldsuper = this.super;
  337. this.super = oldfn;
  338. const ret = fn.apply(this, arguments);
  339. this.super = oldsuper;
  340. return ret;
  341. };
  342. }
  343.  
  344. //bounds functions
  345. bililiteRange.bounds = {
  346. all: function() { return [0, this.length] },
  347. start: function() { return 0 },
  348. end: function() { return this.length },
  349. selection: function() {
  350. if (this._el === this._doc.activeElement){
  351. this.bounds ('all'); // first select the whole thing for constraining
  352. return this._nativeSelection();
  353. }else{
  354. return this.data.selection;
  355. }
  356. },
  357. startbounds: function() { return this[0] },
  358. endbounds: function() { return this[1] },
  359. union: function (name,...rest) {
  360. const b = this.clone().bounds(...rest);
  361. return [ Math.min(this[0], b[0]), Math.max(this[1], b[1]) ];
  362. },
  363. intersection: function (name,...rest) {
  364. const b = this.clone().bounds(...rest);
  365. return [ Math.max(this[0], b[0]), Math.min(this[1], b[1]) ];
  366. }
  367. };
  368.  
  369. // sendkeys functions
  370. bililiteRange.sendkeys = {
  371. '{tab}': function (rng, c, simplechar){
  372. simplechar(rng, '\t'); // useful for inserting what would be whitespace
  373. },
  374. '{newline}': function (rng){
  375. rng.text('\n', {inputType: 'insertLineBreak'}).bounds('endbounds');
  376. },
  377. '{backspace}': function (rng){
  378. var b = rng.bounds();
  379. if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
  380. rng.text('', {inputType: 'deleteContentBackward'}); // delete the characters and update the selection
  381. },
  382. '{del}': function (rng){
  383. var b = rng.bounds();
  384. if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
  385. rng.text('', {inputType: 'deleteContentForward'}).bounds('endbounds'); // delete the characters and update the selection
  386. },
  387. '{rightarrow}': function (rng){
  388. var b = rng.bounds();
  389. if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
  390. rng.bounds([b[1], b[1]]);
  391. },
  392. '{leftarrow}': function (rng){
  393. var b = rng.bounds();
  394. if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
  395. rng.bounds([b[0], b[0]]);
  396. },
  397. '{selectall}': function (rng){
  398. rng.bounds('all');
  399. },
  400. '{selection}': function (rng){
  401. // insert the characters without the sendkeys processing
  402. rng.text(rng.data.sendkeysOriginalText).bounds('endbounds');
  403. },
  404. '{mark}': function (rng){
  405. rng.data.sendkeysBounds = rng.bounds();
  406. },
  407. '{ctrl-Home}': (rng, c, simplechar) => rng.bounds('start'),
  408. '{ctrl-End}': (rng, c, simplechar) => rng.bounds('end')
  409. };
  410. // Synonyms from the DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/)
  411. bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}'] = bililiteRange.sendkeys['{newline}'];
  412. bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}'];
  413. bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}'];
  414. bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}'];
  415. bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}'];
  416.  
  417. // an input element in a standards document. "Native Range" is just the bounds array
  418. function InputRange(){}
  419. InputRange.prototype = new Range();
  420. InputRange.prototype._textProp = 'value';
  421. InputRange.prototype._nativeRange = function(bounds) {
  422. return bounds || [0, this.length];
  423. };
  424. InputRange.prototype._nativeSelect = function (rng){
  425. this._el.setSelectionRange(rng[0], rng[1]);
  426. };
  427. InputRange.prototype._nativeSelection = function(){
  428. return [this._el.selectionStart, this._el.selectionEnd];
  429. };
  430. InputRange.prototype._nativeGetText = function(rng){
  431. return this._el.value.substring(rng[0], rng[1]);
  432. };
  433. InputRange.prototype._nativeSetText = function(text, rng){
  434. var val = this._el.value;
  435. this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
  436. };
  437. InputRange.prototype._nativeEOL = function(){
  438. this.text('\n');
  439. };
  440. InputRange.prototype._nativeTop = function(rng){
  441. if (rng[0] == 0) return 0; // the range starts at the top
  442. const el = this._el;
  443. if (el.nodeName == 'INPUT') return 0;
  444. const text = el.value;
  445. const selection = [el.selectionStart, el.selectionEnd];
  446. // hack from https://code.google.com/archive/p/proveit-js/source/default/source, highlightLengthAtIndex function
  447. // note that this results in the element being scrolled; the actual number returned is irrelevant
  448. el.value = text.slice(0, rng[0]);
  449. el.scrollTop = Number.MAX_SAFE_INTEGER;
  450. el.value = text;
  451. el.setSelectionRange(...selection);
  452. return el.scrollTop;
  453. }
  454. InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")};
  455.  
  456. function W3CRange(){}
  457. W3CRange.prototype = new Range();
  458. W3CRange.prototype._textProp = 'textContent';
  459. W3CRange.prototype._nativeRange = function (bounds){
  460. var rng = this._doc.createRange();
  461. rng.selectNodeContents(this._el);
  462. if (bounds){
  463. w3cmoveBoundary (rng, bounds[0], true, this._el);
  464. rng.collapse (true);
  465. w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
  466. }
  467. return rng;
  468. };
  469. W3CRange.prototype._nativeSelect = function (rng){
  470. this._win.getSelection().removeAllRanges();
  471. this._win.getSelection().addRange (rng);
  472. };
  473. W3CRange.prototype._nativeSelection = function (){
  474. // returns [start, end] for the selection constrained to be in element
  475. var rng = this._nativeRange(); // range of the element to constrain to
  476. if (this._win.getSelection().rangeCount == 0) return [this.length, this.length]; // append to the end
  477. var sel = this._win.getSelection().getRangeAt(0);
  478. return [
  479. w3cstart(sel, rng),
  480. w3cend (sel, rng)
  481. ];
  482. };
  483. W3CRange.prototype._nativeGetText = function (rng){
  484. return rng.toString();
  485. };
  486. W3CRange.prototype._nativeSetText = function (text, rng){
  487. rng.deleteContents();
  488. rng.insertNode (this._doc.createTextNode(text));
  489. // Lea Verou's "super dirty fix" to #31
  490. if(text == '\n' && this[1]+1 == this._el.textContent.length) {
  491. // inserting a newline at the end
  492. this._el.innerHTML = this._el.innerHTML + '\n';
  493. }
  494. this._el.normalize(); // merge the text with the surrounding text
  495. };
  496. W3CRange.prototype._nativeEOL = function(){
  497. var rng = this._nativeRange(this.bounds());
  498. rng.deleteContents();
  499. var br = this._doc.createElement('br');
  500. br.setAttribute ('_moz_dirty', ''); // for Firefox
  501. rng.insertNode (br);
  502. rng.insertNode (this._doc.createTextNode('\n'));
  503. rng.collapse (false);
  504. };
  505. W3CRange.prototype._nativeTop = function(rng){
  506. if (this.length == 0) return 0; // no text, no scrolling
  507. if (rng.toString() == ''){
  508. var textnode = this._doc.createTextNode('X');
  509. rng.insertNode (textnode);
  510. }
  511. var startrng = this._nativeRange([0,1]);
  512. var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top;
  513. if (textnode) textnode.parentNode.removeChild(textnode);
  514. return top;
  515. }
  516. W3CRange.prototype._nativeWrap = function(n, rng) {
  517. rng.surroundContents(n);
  518. };
  519.  
  520. // W3C internals
  521. function nextnode (node, root){
  522. // in-order traversal
  523. // we've already visited node, so get kids then siblings
  524. if (node.firstChild) return node.firstChild;
  525. if (node.nextSibling) return node.nextSibling;
  526. if (node===root) return null;
  527. while (node.parentNode){
  528. // get uncles
  529. node = node.parentNode;
  530. if (node == root) return null;
  531. if (node.nextSibling) return node.nextSibling;
  532. }
  533. return null;
  534. }
  535. function w3cmoveBoundary (rng, n, bStart, el){
  536. // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
  537. // if the start is moved after the end, then an exception is raised
  538. if (n <= 0) return;
  539. var node = rng[bStart ? 'startContainer' : 'endContainer'];
  540. if (node.nodeType == 3){
  541. // we may be starting somewhere into the text
  542. n += rng[bStart ? 'startOffset' : 'endOffset'];
  543. }
  544. while (node){
  545. if (node.nodeType == 3){
  546. var length = node.nodeValue.length;
  547. if (n <= length){
  548. rng[bStart ? 'setStart' : 'setEnd'](node, n);
  549. // special case: if we end next to a <br>, include that node.
  550. if (n == length){
  551. // skip past zero-length text nodes
  552. for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
  553. rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
  554. }
  555. if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
  556. }
  557. return;
  558. }else{
  559. rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
  560. n -= length; // and eat these characters
  561. }
  562. }
  563. node = nextnode (node, el);
  564. }
  565. }
  566. var START_TO_START = 0; // from the w3c definitions
  567. var START_TO_END = 1;
  568. var END_TO_END = 2;
  569. var END_TO_START = 3;
  570. // from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
  571. // -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
  572. // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
  573. // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
  574. // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
  575. // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
  576. function w3cstart(rng, constraint){
  577. if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
  578. if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
  579. rng = rng.cloneRange(); // don't change the original
  580. rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
  581. return constraint.toString().length - rng.toString().length;
  582. }
  583. function w3cend (rng, constraint){
  584. if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
  585. if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
  586. rng = rng.cloneRange(); // don't change the original
  587. rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
  588. return rng.toString().length;
  589. }
  590.  
  591. function NothingRange(){}
  592. NothingRange.prototype = new Range();
  593. NothingRange.prototype._textProp = 'value';
  594. NothingRange.prototype._nativeRange = function(bounds) {
  595. return bounds || [0,this.length];
  596. };
  597. NothingRange.prototype._nativeSelect = function (rng){ // do nothing
  598. };
  599. NothingRange.prototype._nativeSelection = function(){
  600. return [0,0];
  601. };
  602. NothingRange.prototype._nativeGetText = function (rng){
  603. return this._el[this._textProp].substring(rng[0], rng[1]);
  604. };
  605. NothingRange.prototype._nativeSetText = function (text, rng){
  606. var val = this._el[this._textProp];
  607. this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
  608. };
  609. NothingRange.prototype._nativeEOL = function(){
  610. this.text('\n');
  611. };
  612. NothingRange.prototype._nativeTop = function(){
  613. return 0;
  614. };
  615. NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")};
  616.  
  617.  
  618. // data for elements, similar to jQuery data, but allows for monitoring with custom events
  619. const monitored = new Set();
  620.  
  621. function signalMonitor(prop, value, element){
  622. const attr = `data-${prop}`;
  623. element.dispatchEvent(new CustomEvent(attr, {bubbles: true, detail: value}));
  624. try{
  625. element.setAttribute (attr, value); // illegal attribute names will throw. Ignore it
  626. } finally { /* ignore */ }
  627. }
  628.  
  629. function createDataObject (el){
  630. return el[datakey] = new Proxy(new Data(el), {
  631. set(obj, prop, value) {
  632. obj[prop] = value;
  633. if (monitored.has(prop)) signalMonitor(prop, value, obj.sourceElement);
  634. return true; // in strict mode, 'set' returns a success flag
  635. }
  636. });
  637. }
  638.  
  639. var Data = function(el) {
  640. Object.defineProperty(this, 'sourceElement', {
  641. value: el
  642. });
  643. }
  644.  
  645. Data.prototype = {};
  646. // for use with ex options. JSON.stringify(range.data) should return only the options that were
  647. // both defined with bililiteRange.option() *and* actually had a value set on this particular data object.
  648. // JSON.stringify (range.data.all) should return all the options that were defined.
  649. Object.defineProperty(Data.prototype, 'toJSON', {
  650. value: function(){
  651. let ret = {};
  652. for (let key in Data.prototype) if (this.hasOwnProperty(key)) ret[key] = this[key];
  653. return ret;
  654. }
  655. });
  656. Object.defineProperty(Data.prototype, 'all', {
  657. get: function(){
  658. let ret = {};
  659. for (let key in Data.prototype) ret[key] = this[key];
  660. return ret;
  661. }
  662. });
  663. Object.defineProperty(Data.prototype, 'trigger', {
  664. value: function(){
  665. monitored.forEach(prop => signalMonitor (prop, this[prop], this.sourceElement));
  666. }
  667. });
  668.  
  669. bililiteRange.createOption = function (name, desc = {}){
  670. desc = Object.assign({
  671. enumerable: true, // use these as the defaults
  672. writable: true,
  673. configurable: true
  674. }, Object.getOwnPropertyDescriptor(Data.prototype, name), desc);
  675. if ('monitored' in desc) monitored[desc.monitored ? 'add' : 'delete'](name);
  676. Object.defineProperty(Data.prototype, name, desc);
  677. return Data.prototype[name]; // return the default value
  678. }
  679.  
  680. })();
  681.  
  682. // bililiteRange.find.js commit ef1c276
  683. 'use strict';
  684.  
  685. (function(bililiteRange){
  686.  
  687. bililiteRange.createOption('dotall', {value: false});
  688. bililiteRange.createOption('global', {value: false});
  689. bililiteRange.createOption('ignorecase', {value: false});
  690. bililiteRange.createOption('magic', {value: true});
  691. bililiteRange.createOption('multiline', {value: false});
  692. bililiteRange.createOption('unicode', {value: false});
  693. bililiteRange.createOption('wrapscan', {value: true});
  694.  
  695. bililiteRange.bounds.find = function (name, restring, flags = ''){
  696. return find (this, restring, 'V'+flags);
  697. };
  698.  
  699. bililiteRange.override('bounds', function (re, flags = ''){
  700. // duck typed RegExps are OK, allows for flags to be part of re
  701. if (!(re instanceof Object && 'source' in re && 'flags' in re)) return this.super(...arguments);
  702. return find (this, re.source, flags + re.flags);
  703. });
  704.  
  705. bililiteRange.prototype.replace = function (search, replace, flags = ''){
  706. if (search instanceof Object && 'source' in search && 'flags' in search){
  707. // a RegExp or similar
  708. flags = flags + search.flags;
  709. search = search.source;
  710. }else{
  711. search = search.toString();
  712. flags = 'V' + flags;
  713. }
  714. return this.text(
  715. replaceprimitive (search, parseFlags(this, flags), this.all(), replace, this[0], this[1]),
  716. { inputType: 'insertReplacementText' }
  717. );
  718. }
  719.  
  720. bililiteRange.createOption ('word', {value: /\b/});
  721. bililiteRange.createOption ('bigword', {value: /\s+/});
  722. bililiteRange.createOption ('sentence', {value: /\n\n|\.\s/});
  723. bililiteRange.createOption ('paragraph', {value: /\n\s*\n/});
  724. bililiteRange.createOption ('section', {value: /\n(<hr\/?>|(-|\*|_){3,})\n/i});
  725. bililiteRange.createOption ('()', {value: [/\(/, /\)/] });
  726. bililiteRange.createOption ('[]', {value: [/\[/, /]/] });
  727. bililiteRange.createOption ('{}', {value: [/{/, /}/] });
  728. bililiteRange.createOption ('"', {value: [/"/, /"/] });
  729. bililiteRange.createOption ("'", {value: [/'/, /'/] });
  730.  
  731. bililiteRange.bounds.to = function(name, separator, outer = false){
  732. if (separator in this.data) separator = this.data[separator];
  733. if (separator.length == 2) separator = separator[1];
  734. if (!(separator instanceof RegExp)) separator = new RegExp (quoteRegExp (separator));
  735. // end of text counts as a separator
  736. const match = findprimitive(`(${separator.source})|$`, 'g'+separator.flags, this.all(), this[1], this.length);
  737. return this.bounds('union', outer ? match.index + match[0].length : match.index);
  738. };
  739.  
  740. bililiteRange.bounds.from = function(name, separator, outer = false){
  741. if (separator in this.data) separator = this.data[separator];
  742. if (separator.length == 2) separator = separator[0];
  743. if (!(separator instanceof RegExp)) separator = new RegExp (quoteRegExp (separator));
  744. // start of text counts as a separator
  745. const match = findprimitiveback(`(${separator.source})|^`, 'g'+separator.flags, this.all(), 0, this[0]);
  746. return this.bounds('union', outer ? match.index : match.index + match[0].length);
  747. };
  748.  
  749. bililiteRange.bounds.whole = function(name, separator, outer = false){
  750. if (separator in this.data) separator = this.data[separator];
  751. // if it's a two-part separator (like parentheses or quotes) then "outer" should include both.
  752. return this.bounds('union', 'from', separator, outer && separator?.length == 2).bounds('union', 'to', separator, outer);
  753. };
  754.  
  755. //------- private functions -------
  756.  
  757. function find (range, source, sourceflags){
  758. const {
  759. backward,
  760. magic,
  761. restricted,
  762. sticky,
  763. wrapscan,
  764. flags
  765. } = parseFlags (range, sourceflags + 'g');
  766. if (!magic) source = quoteRegExp (source);
  767. const findfunction = backward ? findprimitiveback : findprimitive;
  768. let from, to;
  769. if (restricted){
  770. from = range[0];
  771. to = range[1];
  772. }else if (backward){
  773. from = 0;
  774. to = range[0];
  775. }else{
  776. from = range[1];
  777. to = range.length;
  778. }
  779. let match = findfunction (source, flags, range.all(), from, to);
  780. if (!match && wrapscan && !sticky && !restricted){
  781. match = findfunction(source, flags, range.all(), 0, range.length);
  782. }
  783. range.match = match || false; // remember this for the caller
  784. if (match) range.bounds([match.index, match.index+match[0].length]); // select the found string
  785. return range;
  786. }
  787.  
  788. function parseFlags (range, flags){
  789. let flagobject = {
  790. b: false,
  791. g: range.data.global,
  792. i: range.data.ignorecase,
  793. m: range.data.multiline,
  794. r: false,
  795. s: range.data.dotall,
  796. u: range.data.unicode,
  797. v: range.data.magic,
  798. w: range.data.wrapscan,
  799. y: false
  800. };
  801. flags.split('').forEach( flag => flagobject[flag.toLowerCase()] = flag === flag.toLowerCase() );
  802. return {
  803. // these are the "real" flags
  804. flags: (flagobject.g ? 'g' : '') + (flagobject.i ? 'i' : '') + (flagobject.m ? 'm' : '') +
  805. (flagobject.s ? 's' : '') + (flagobject.u ? 'u' : '') + (flagobject.y ? 'y' : ''),
  806. backward: flagobject.b,
  807. global: flagobject.g,
  808. magic: flagobject.v,
  809. restricted: flagobject.r,
  810. wrapscan: flagobject.w,
  811. sticky: flagobject.y
  812. };
  813. }
  814.  
  815. function quoteRegExp (source){
  816. // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
  817. return source.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
  818. }
  819.  
  820. function findprimitive (source, flags, text, from, to){
  821. // code from https://github.com/idupree/bililiteRange/tree/findback-greedy-correctness
  822. if (to < text.length){
  823. // make sure that there are at least length-to characters after the match
  824. source = `(?:${source})(?=[^]{${text.length-to}})`;
  825. }
  826. const re = new RegExp (source, flags);
  827. re.lastIndex = from;
  828. return re.exec(text);
  829. }
  830.  
  831. function findprimitiveback (source, flags, text, from, to){
  832. // code from https://github.com/idupree/bililiteRange/tree/findback-greedy-correctness
  833. if (to < text.length){
  834. // make sure that there are at least length-to characters after the match
  835. source = `(?:${source})(?=[^]{${text.length-to}})`;
  836. }
  837. if (/y/.test(flags)){
  838. // sticky. Only match the end of the string.
  839. flags = flags.replace('y','');
  840. source = `(?:${source})(?![^]{${text.length-to+1}})`; // *don't* match too many characters
  841. // this works even if $ won't, if multiline is true
  842. const re = new RegExp (source, flags);
  843. re.lastIndex = from;
  844. return re.exec(text);
  845. }else{
  846. // no way to search backward; have to search forward until we fail
  847. const re = new RegExp (source, flags);
  848. re.lastIndex = from;
  849. let match = false;
  850. do {
  851. var lastmatch = match;
  852. match = re.exec(text);
  853. if (match && re.lastIndex == match.index) ++re.lastIndex; // beware zero-length matches and infinite loops
  854. }while (match);
  855. return lastmatch;
  856. }
  857. }
  858.  
  859. function replaceprimitive (search, flagobject, text, replace, from, to){
  860. if (!flagobject.magic) search = quoteRegExp (search);
  861. if (from > 0){
  862. // make sure we have at least (from) characters before the match
  863. search = `(?<=[^]{${from}})(?:${search})`;
  864. }
  865. if (to < text.length){
  866. // make sure we have at least (length - to) characters after the match
  867. search = `(?:${search})(?=[^]{${text.length - to}})`;
  868. }
  869. if (flagobject.sticky && flagobject.backward){
  870. flagobject.flags = flagobject.flags.replace(/[gy]/g, '');
  871. // make sure we don't have too many characters after the match
  872. search = `(?:${search})(?![^]{${text.length - to + 1}})`;
  873. }else if (flagobject.backward && ! flagobject.global){
  874. // would anyone ever do this? Replace only the last match?
  875. const match = findprimitiveback (search, flagobject.flags+'g', text, from, to);
  876. if (!match) return text.slice (from, to); // no match, no change
  877. search = `(?<=[^]{${match.index}})(?:${search})`;
  878. }
  879. const re = new RegExp (search, flagobject.flags);
  880. re.lastIndex = from; // only relevant for sticky && !backward
  881. // if to == length, then go to the end of the string,not to position 0!
  882. return text.replace (re, replace).slice(from, to-text.length || undefined);
  883. }
  884.  
  885. })(bililiteRange);
  886.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement