1/*  DokuWiki MoaiEditor Scroll.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 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 center position
131        let pos = element.getBoundingClientRect();
132        let right = (pos.top + pos.bottom)/2;
133        // Setup variables
134        this.smoothScrollInit();
135        this.left.startline = match.startline;
136        this.left.endline = match.endline;
137        this.left.target = right;
138        this.scroll = {type:'click', startline:match.startline, endline:match.endline, element:element};
139        // Setup the callback
140        this.left.getTargetScroll = this.onClick_getTargetScroll;
141        // Start the process
142        this.left.start();
143    }
144    onClick_getTargetScroll() {
145        const editor = this.object;
146        // Current left center position
147        let top    = editor.getLineRect(this.startline, 'viewport').top;
148        let bottom = editor.getLineRect(this.endline, 'viewport').bottom;
149        let left =  (top + bottom)/2;
150        // Calc final scroll
151        let correction = left - this.target;
152        // Return
153        return editor.scroll.top + correction;
154    }
155    // ────────────────────────────────────
156    makeElementClickable(element) {
157
158        // Exit if the element already has been made clickable
159        if (element.classList.contains('moaied-preview-header'))
160            return;
161
162        // Add event listener
163        element.addEventListener('click', function(){ moaiEditor.scroll.onClick(this); });
164
165        // Style the header
166        const headerLvl = Number(element.tagName.substr(1,1));
167        element.classList.add('moaied-preview-header');
168        element.title = 'Click to scroll';
169
170        // Add icon image
171        const h = 24 - 2*headerLvl;
172        const m = 14 - 2*headerLvl;
173        const img = moaiEditor.createHTML('<img class="moaied-scroller-icon" src="lib/plugins/moaieditor/icons/sp_aim.png">');
174        img.style.setProperty('height', h+'px');
175        img.style.setProperty('margin-left', m+'px');
176        element.appendChild(img);
177    }
178    // ────────────────────────────────────
179    smoothScrollInit () {
180        // In phones, scroll to the textarea side (call goRight before to prevent refresh bug)
181        moaiEditor.layout.goRight();
182        // Setup variables
183        this.left.smooth = true;
184        this.right.smooth = true;
185        this.left.object = moaiEditor.editor.current;
186        this.right.object = moaiEditor.preview;
187    }
188    // ────────────────────────────────────
189    findMatch (element) {
190
191        // Search in existing matches
192        const match = moaiEditor.matches.find(element);
193        if (match)
194            return match;
195
196        // TODO:  Recalculate the matches and try again (needed if the user edited the matched line before a preview update)
197        // The code below works right away, but needs testing before enabling it because it will run very seldom,
198        // so a bug there could become hard to track.
199        /*
200        moaiEditor.matches.update(null,null);
201        return moaiEditor.matches.find(element);
202        */
203
204        // No matching line was found
205        return null;
206    }
207    // ────────────────────────────────────
208    copy () {
209        // Store scroll position before switching editors
210        const editor = moaiEditor.editor.current;
211        const scrollTop = editor.scroll.top;
212        const n = editor.watcher.lines.length;
213        for (var i=0; i<n; i++) {
214            var rect = editor.getLineRect(i);
215            if (rect.bottom >= scrollTop)
216                break;
217        }
218        var linenum = i;
219        var fraction = (scrollTop-rect.top)/rect.height;
220        fraction = Math.max(0, fraction);   // Prevent negative number if gap between document top and first line
221        this.clipboard = linenum + fraction;
222    }
223    paste () {
224        // Restore scroll position after switching editors
225        if (this.clipboard === null)
226            return;
227        // Setup variables
228        this.left.float = this.clipboard;
229        this.left.smooth = false;
230        this.left.object = moaiEditor.editor.current;
231        this.scroll = {type:'paste'};
232        // Setup the callback
233        this.left.getTargetScroll = this.paste_getTargetScroll;
234        // Start the process
235        this.left.start();
236    }
237    paste_getTargetScroll() {
238        const linenum = Math.floor(this.float);
239        const fraction = this.float % 1;
240        const rect = this.object.getLineRect(linenum);
241        const scrollTop = rect.top + rect.height * fraction;
242        return scrollTop;
243    }
244}; // End Class
245
246
247