1/*  DokuWiki MoaiEditor Scroll_sync.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/*  Scrolls the preview (keeping it synchronized with the editor).
7
8*/
9MoaiEditor.ScrollSync = class {
10
11    constructor(outer) {
12
13        // Variables
14        this.disabled = false;      // synchronization can be disabled temporarily when other processes want to take control
15    }
16    // ────────────────────────────────────
17    init() {
18        this.scroller = new MoaiEditor.Scroller(this);
19    }
20    // ────────────────────────────────────
21    get isEnabled() {
22        if (this.disabled  ||  moaiEditor.buttons.autoscroll.mode == 'off'  ||  !moaiEditor.layoutReady)
23            return false;
24        return true;
25    }
26    // ────────────────────────────────────
27    // This function is called whenever the user scrolls the editor side
28    onScroll(event) {
29
30        // Exit if disabled by the user, by other process, or if the editor is not ready
31        if (!this.isEnabled)
32            return;
33
34        // Exit if the editor scroll has not changed (to avoid interfering right after another process has done scrolling)
35        const scroll = moaiEditor.editor.current.scroll.top;
36        const sameScroll = Math.abs(scroll - this.lastScroll) <= 2;
37        if (sameScroll)
38            return;
39
40        // Synchronize the scroll of the preview
41        const target = this.calcScroll().right;
42        if (target !== null) {
43            this.scroller.target = target;
44            this.scroller.engaged = true;
45        }
46        // Remember last scroll position
47        this.lastScroll = scroll;
48    }
49    // ────────────────────────────────────
50    calcScroll() {
51
52        // There should be at least one match
53        var matches = moaiEditor.matches.matches;
54        if (matches.length == 0)
55            return null;
56
57        // Get the editor side scroll position
58        const leftScroll = moaiEditor.editor.current.scroll.top;
59
60        // Get the preview container top position relative to the screen
61        this.container = moaiEditor.preview.container;
62        this.containerTop = this.container.getBoundingClientRect().top;
63
64        // Get the scroll points above and below our current scroll position
65        var p1, p2, match;
66        for (var i = 0; i < matches.length; i++) {
67            if (matches[i].scroll === null)
68                continue;
69            match = matches[i];
70            if (match.scroll.top > leftScroll) {
71                p1 = this.getPoint(i-1, 'bottom');
72                p2 = this.getPoint(i, 'top');
73                break;
74            }
75            if (match.scroll.bottom > leftScroll) {
76                p1 = this.getPoint(i, 'top');
77                p2 = this.getPoint(i, 'bottom');
78                break;
79            }
80        }
81        // Exit if no scroll points where found
82        if (match === undefined) {
83            return null;
84        }
85
86
87        // Build bottom scroll point if needed
88        if (p1 === undefined) {
89            p1 = this.getPoint(i-1, 'bottom');
90            p2 = this.getPoint(i, 'top');
91        }
92
93        // Get the normalized scroll progress between both scrollpoints on the editor side
94        const normalized = (leftScroll - p1.left) / (p2.left - p1.left);
95
96        // Compute the corresponding scroll on the preview side
97        const right = p1.right + normalized * (p2.right - p1.right);
98
99        // Debug line
100        const scroll = {p1:p1, p2:p2, right:right};
101        this.debugLine(scroll);
102
103        // Return
104        return scroll;
105    }
106    // ────────────────────────────────────
107    getPoint(i, side) {
108        var type, left, right;
109        var linenum = side.toUpperCase();
110        // Build a TOP scrollpoint if needed
111        if (i < 0) {
112            type = 'TOP';
113            left = 0;
114            right = 0;
115        // Build a BOTTOM scrollpoint if needed
116        } else if (i >= moaiEditor.matches.matches.length) {
117            type = 'BOTTOM';
118            left = moaiEditor.editor.current.scroll.max;
119            right = this.container.scrollHeight;
120        // Get the scrollpoint from the match
121        } else {
122            const match = moaiEditor.matches.matches[i];
123            type  = match.type,
124            left  = match.scroll[side],
125            right = match.handle.getBoundingClientRect()[side] - this.containerTop + this.container.scrollTop;
126            linenum = match.startline;
127            if (side == 'bottom')
128                linenum = match.endline;
129        }
130        // Pack and return
131        return {linenum:linenum, type:type, left:left, right:right, side:side};
132    }
133    // ────────────────────────────────────
134    debugLine(scroll) {
135        // Exit if debugline is not enabled
136        const line = document.querySelector("#moai__debug div:nth-child(2)");
137        if (!line) return;
138
139        // Debug line
140        const p1 = scroll.p1;
141        const p2 = scroll.p2;
142        const txt1 = "<"+scroll.p1.type+"> "+p1.side+" L"+p1.linenum;
143        const txt2 = "<"+scroll.p2.type+"> "+p2.side+" L"+p2.linenum;
144        const text = txt1+" ⇔ "+txt2;
145        line.textContent = text;
146    }
147}; // End Class
148
149// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆
150// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆
151
152MoaiEditor.Scroller = class {
153
154    constructor(outer) {
155        this.outer = outer;
156        this.i = 0;
157        this.target = null;
158        this._engaged = false;
159        this.onFrame();
160    }
161    get engaged() {
162        return this._engaged;
163    }
164    set engaged(boolean) {
165        this._engaged = boolean;
166        this.lastScroll = moaiEditor.preview.scroll.top;
167    }
168    onFrame() {
169        requestAnimationFrame(this.onFrame.bind(this));
170        //this.i += 1; if (this.i % 3 !== 0) return;      // Test other framerates
171        const dt = Date.now() - this.lastTime;
172        this.lastTime = Date.now();
173        if (!this.engaged  ||  !this.outer.isEnabled  ||  this.target === null)
174            return;
175        // Disengage if the user scrolled the preview
176        const scroll = moaiEditor.preview.scroll.top;
177        const userScrolled = Math.abs(scroll - this.lastScroll) > 2;
178        if (userScrolled) {
179            this.engaged = false;
180            return;
181        }
182        // Scroll the preview
183        const distance = this.target - scroll;
184        const increment = 1*distance*dt/160;
185        moaiEditor.preview.scroll.top = scroll + increment;
186        this.lastScroll = scroll + increment;
187    }
188}; // End Class
189
190
191