1/* DokuWiki MoaiEditor Watcher.js file 2 Version : 0.5a (May 6, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* This class watches for changes lines in the textarea contents. 7 8 It detects deleted and inserted lines. An edited or replaced line is interpreted 9 as a deletion folowed by an insertion. 10 11 Handling textarea input events and toolbar button clicks might not be sufficient 12 to detect changes. Javascript can change the text without generating any events, 13 so unknown plugins might bypass this detection. 14 15 This is why we also check regularly if the text has changed even if no user input 16 event has been detected. This process is not very cpu intensive even when the page 17 is huge (it takes less than 1 ms on a 400 Kb page, which is larger than the largest 18 english wikipedia page). 19 20 We have a timer class to make these background checks more often if the user has 21 interacted recently and less often otherwise (to minimize CPU usage when there is 22 no interaction). 23*/ 24MoaiEditor.WatchTextChanges = class { 25 26 constructor(outer) { 27 28 // Arguments 29 this.outer = outer; 30 31 // Objects 32 this.timer = new MoaiEditor.Timer(); 33 34 // Intervals 35 this.interval = setInterval(this.onInterval.bind(this), 200); 36 } 37 // ──────────────────────────────────── 38 onInterval() { 39 40 // Return if the parent is disabled 41 if (!this.outer.enabled) 42 return; 43 44 // Return if not enough time has elapsed 45 if (!this.timer.due()) 46 return; 47 48 // Return if the editor is not enabled and ready 49 if (!moaiEditor.layoutReady) 50 return; 51 52 // Check for text changes 53 this.checkTextChange(); 54 55 // Optional debug actions 56 this.debug(); 57 58 } 59 // ──────────────────────────────────── 60 onInput() { 61 this.timer.resetPeriod(); 62 this.timer.restart(); 63 this.checkTextChange(); 64 } 65 // ──────────────────────────────────── 66 getTextLines() { 67 return moaiEditor.layout.textarea.value.split("\n"); 68 } 69 // ──────────────────────────────────── 70 checkTextChange() { 71 // Init 72 var i; 73 const start = Date.now(); 74 var previous = this.lines; 75 var current = this.getTextLines(); 76 77 // Get shift (delta) 78 const shift = current.length - previous.length; 79 80 // Walk lines form top to bottom 81 for (i=0; i<previous.length; i++) { 82 if (current.length <= i) 83 break; 84 if (previous[i] != current[i]) 85 break; 86 } 87 // Return if the text is the same 88 if (i == previous.length && shift == 0) { 89 return null; 90 } 91 // First changed line (top to bottom) 92 const i0 = i; 93 94 // Walk lines form bottom to top and detect the first change 95 for (var k=0; k<previous.length; k++) { 96 i = previous.length - k - 1; 97 var j = current.length - k - 1; 98 if (j < 0) 99 break; 100 if (previous[i] != current[j]) 101 break; 102 } 103 // First changed line (bottom to top) 104 const i1 = i; 105 106 // Calc some values 107 var removed = Math.max(0, i1 - i0 + 1); 108 if (previous.length == 0) { 109 removed = 0; 110 } 111 var inserted = removed + shift; 112 if (inserted < 0) { 113 removed -= inserted; 114 inserted = 0; 115 } 116 // Pack the result 117 const elapsed = Date.now() - start; 118 const data = { num:{keepfirst:i0, remove:removed, insert:inserted}, index:{keeplast: (i0+removed)}, i0:i0, i1:i1, shift:shift, elapsed:elapsed }; 119 120 // Store current lines 121 this.lines = current; 122 123 // Start checking more frequently for changes 124 this.timer.resetPeriod(); 125 126 // Enable "This page is asking you to confirm that you want to leave" popup from: lib/scripts/edit.js 127 window.textChanged = true; 128 129 // Style the Commit and Cancel buttons 130 moaiEditor.buttons.styleExitButtons(); 131 132 // Inform parent object of the changes 133 this.outer.onTextChanged(data); 134 } 135 // ──────────────────────────────────── 136 debug() { 137 // Placeholder 138 } 139}; // End Class 140 141// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 142// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 143 144MoaiEditor.Timer = class { 145 146 constructor () { 147 this.period = 200; 148 this.minPeriod = 200; 149 this.maxPeriod = 2000; 150 this.inc = 50; 151 this.last = Date.now(); 152 } 153 restart () { 154 this.last = Date.now(); 155 this.period += this.inc; 156 this.period = Math.min(this.period, this.maxPeriod); 157 } 158 due () { 159 this.debug(); 160 const elapsed = Date.now() - this.last; 161 if (elapsed > this.period) { 162 this.restart(); 163 return true; 164 } 165 return false; 166 } 167 set (period) { 168 this.period = Math.round(period); 169 } 170 setMin (period) { 171 this.minPeriod = Math.round(period); 172 } 173 setMax (period) { 174 this.minPeriod = Math.round(period); 175 } 176 setInc (inc) { 177 this.inc = inc; 178 } 179 resetPeriod () { 180 this.period = this.minPeriod; 181 } 182 debug () { 183 const line = document.querySelector("#moai__debug div:nth-child(1)"); 184 if (!line) return; 185 const elapsed = '<span style="opacity:0.2">'+(Date.now()-this.last).toString().padStart(4," ")+'</span>'; 186 const text = "TIMER: "+elapsed+" period:"+this.period+" min:"+this.minPeriod+" max:"+this.maxPeriod; 187 line.innerHTML = text; 188 line.title = 'Texarea '; 189 } 190}; // End Class 191 192 193