1/*  DokuWiki MoaiEditor Scroll.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 is the umbrella class for scrolling functions:
7       - Scrolling the preview when the user scrolls the editor (keeping it synchronized).
8       - Scrolling the editor when the user clicks an element on the preview.
9       - Scrolling both editor and preview when the user clicks on the table of contents.
10       - Scrolling both editor and preview when the user clicks on the go to top button.
11       - Scrolling both editor and preview when the user clicks on the go to bottom button.
12*/
13MoaiEditor.Scroll = class {
14
15    constructor(outer) {
16
17        // Arguments
18        this.outer = outer;
19
20        // Variables
21        this.clipboard = null;
22
23        // Objects
24        this.tools = new MoaiEditor.ScrollTools();
25        this.sync  = new MoaiEditor.ScrollSync(this);
26        this.left  = new MoaiEditor.ScrollTo();
27        this.right = new MoaiEditor.ScrollTo();
28    }
29    // ────────────────────────────────────
30    init() {
31        this.sync.init();
32    }
33    // ────────────────────────────────────
34    halt() {
35        // Stop any scroll loop in progress before switching editors
36        this.left.halt();
37        this.right.halt();
38        this.sync.scroller.engaged = false;
39    }
40    // ────────────────────────────────────
41    // Called whenever the 'ScrollTo' class finishes scrolling
42    onScrollEnd() {
43        // Wait for both sides to end
44        if (!this.left.ended  ||  !this.right.ended)
45            return;
46        // Flash elements
47        var flashleft = null;           // null:no flash, true:flash blue, false:flash red
48        var flashright = null;
49        if (this.scroll.type == 'click') {
50            //this.left.success = false;                 // TODO: Test this condition
51            flashright = this.left.success;
52            if (this.left.success)
53                flashleft = true;
54        }
55        else if (this.scroll.type == 'toc') {
56            if (this.left.success)
57                flashleft = true;
58            if (this.right.success)
59                flashright = true;
60        }
61        moaiEditor.editor.current.flash(flashleft, this.scroll);
62        moaiEditor.preview.flash(flashright, this.scroll);
63        // Hide autoscrolling visual indicator
64        moaiEditor.layout.indicatorScrolling.style.opacity = '0';
65        // Re-enable external scroll synchronization
66        this.sync.lastScroll = Math.round(moaiEditor.editor.current.scroll.top);
67        moaiEditor.scroll.sync.disabled = false;
68        //setTimeout(function(){}, 100);
69
70    }
71    // ────────────────────────────────────
72    toTop() {
73        // Setup variables
74        this.smoothScrollInit();
75        this.scroll = {type:'top'};
76        // Setup the callbacks
77        this.left.getTargetScroll = function() {return 0;};
78        this.right.getTargetScroll = function() {return 0;};
79        // Start the process
80        this.left.start();
81        this.right.start();
82    }
83    // ────────────────────────────────────
84    toBottom() {
85        // Setup variables
86        this.smoothScrollInit();
87        this.scroll = {type:'bottom'};
88        // Setup the callbacks
89        this.left.getTargetScroll = function() {return this.object.scroll.max;};
90        this.right.getTargetScroll = function() {return this.object.scroll.max;};
91        // Start the process
92        this.left.start();
93        this.right.start();
94    }
95    // ────────────────────────────────────
96    // Scrolls both panes when the user clicks on the table of contents
97    toc(element) {
98        // Exit if the match does not exist (can happen if the user edited the line before the preview updated)
99        const match = this.findMatch(element);
100        if (match === null) {
101            return;
102        }
103        // Setup variables
104        this.smoothScrollInit();
105        this.left.linenum = match.startline;
106        this.right.element = element;
107        this.scroll = {type:'toc', startline:match.startline, endline:match.endline, element:element};
108        // Setup the callbacks
109        this.left.getTargetScroll = this.toc_getTargetScroll_left;
110        this.right.getTargetScroll = this.toc_getTargetScroll_right;
111        // Start the process
112        this.left.start();
113        this.right.start();
114    }
115    toc_getTargetScroll_left() {
116        return this.object.getLineRect(this.linenum).top;
117    }
118    toc_getTargetScroll_right() {
119        return moaiEditor.scroll.tools.getRectRelativeToParent(this.element).top;
120    }
121    // ────────────────────────────────────
122    // Scrolls the editor when the user clicks an element on the preview
123    onClick(element) {
124        // Exit if the match does not exist (can happen if the user edited the line before the preview updated)
125        const match = this.findMatch(element);
126        if (match === null) {
127            moaiEditor.preview.flash(false, {element:element});     // TODO: Test this condition
128            return;
129        }
130        // Current right position
131        let pos = element.getBoundingClientRect();
132        let container = document.querySelector("#moaied__preview").getBoundingClientRect();
133        let right = (pos.top + pos.bottom)/2 - container.top;
134        // Setup variables
135        this.smoothScrollInit();
136        this.left.startline = match.startline;
137        this.left.endline = match.endline;
138        this.left.target = right;
139        this.scroll = {type:'click', startline:match.startline, endline:match.endline, element:element};
140        // Setup the callback
141        this.left.getTargetScroll = this.onClick_getTargetScroll;
142        // Start the process
143        this.left.start();
144    }
145    onClick_getTargetScroll() {
146        const editor = this.object;
147        // Current left position
148        let container = document.querySelector("#moaied__mirror").getBoundingClientRect();
149        let top    = editor.getLineRect(this.startline, 'viewport').top;
150        let bottom = editor.getLineRect(this.endline, 'viewport').bottom;
151        let left =  (top + bottom)/2 - container.top;
152        // Calc final scroll
153        let correction = left - this.target;
154        // Return
155        return editor.scroll.top + correction;
156    }
157    // ────────────────────────────────────
158    makeElementClickable(element) {
159
160        // Exit if the element already has been made clickable
161        if (element.classList.contains('moaied-preview-header'))
162            return;
163
164        // Add event listener
165        element.addEventListener('click', function(){ moaiEditor.scroll.onClick(this); });
166
167        // Style the header
168        const headerLvl = Number(element.tagName.substr(1,1));
169        element.classList.add('moaied-preview-header');
170        element.title = 'Click to scroll';
171
172        // Add icon image
173        const h = 24 - 2*headerLvl;
174        const m = 14 - 2*headerLvl;
175        const img = moaiEditor.createHTML('<img class="moaied-scroller-icon" src="lib/plugins/moaieditor/icons/sp_aim.png">');
176        img.style.setProperty('height', h+'px');
177        img.style.setProperty('margin-left', m+'px');
178        element.appendChild(img);
179    }
180    // ────────────────────────────────────
181    smoothScrollInit () {
182        // In phones, scroll to the textarea side (call goRight before to prevent refresh bug)
183        moaiEditor.layout.goRight();
184        // Setup variables
185        this.left.smooth = true;
186        this.right.smooth = true;
187        this.left.object = moaiEditor.editor.current;
188        this.right.object = moaiEditor.preview;
189    }
190    // ────────────────────────────────────
191    findMatch (element) {
192
193        // Search in existing matches
194        const match = moaiEditor.matches.find(element);
195        if (match)
196            return match;
197
198        // TODO:  Recalculate the matches and try again (needed if the user edited the matched line before a preview update)
199        // The code below works right away, but needs testing before enabling it because it will run very seldom,
200        // so a bug there could become hard to track.
201        /*
202        moaiEditor.matches.update(null,null);
203        return moaiEditor.matches.find(element);
204        */
205
206        // No matching line was found
207        return null;
208    }
209    // ────────────────────────────────────
210    copy () {
211        // Store scroll position before switching editors
212        const editor = moaiEditor.editor.current;
213        const scrollTop = editor.scroll.top;
214        const n = editor.watcher.lines.length;
215        for (var i=0; i<n; i++) {
216            var rect = editor.getLineRect(i);
217            if (rect.bottom >= scrollTop)
218                break;
219        }
220        var linenum = i;
221        var fraction = (scrollTop-rect.top)/rect.height;
222        fraction = Math.max(0, fraction);   // Prevent negative number if gap between document top and first line
223        this.clipboard = linenum + fraction;
224    }
225    paste () {
226        // Restore scroll position after switching editors
227        if (this.clipboard === null)
228            return;
229        // Setup variables
230        this.left.float = this.clipboard;
231        this.left.smooth = false;
232        this.left.object = moaiEditor.editor.current;
233        this.scroll = {type:'paste'};
234        // Setup the callback
235        this.left.getTargetScroll = this.paste_getTargetScroll;
236        // Start the process
237        this.left.start();
238    }
239    paste_getTargetScroll() {
240        const linenum = Math.floor(this.float);
241        const fraction = this.float % 1;
242        const rect = this.object.getLineRect(linenum);
243        const scrollTop = rect.top + rect.height * fraction;
244        return scrollTop;
245    }
246}; // End Class
247
248
249