1/*  DokuWiki MoaiEditor Watcher.js file
2    Version : 0.5 (May 5, 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