1/*  DokuWiki MoaiEditor Scroll_to.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 will scroll smoothly to a desired point in the document while
7   measuring the reported position of that point continously in order to make
8   on-the-fly corrections to the scroll, while at the same time avoiding
9   correcting too often due to most browsers stuttering badly if done so
10   when 'scroll-behavior' is set to 'smooth' (Firefox being the exception).
11
12   This is needed because oftentimes positions of DOM elements get reported
13   inaccurately if that element is far away from the visible area. Therefore
14   scrolling close to that element is needed before an accurate measurement
15   can be obtained.
16
17   Examples of innacurate position reporting can be observed in the preview
18   (due to lazy loading of resources) and in CodeMirror (due to most lines
19   in CodeMirror not being rendered before they are in the visible area).
20*/
21MoaiEditor.ScrollTo = class {
22
23    constructor () {
24
25        // Constants
26        this.timeout = 4000;                // Avoid getting stuck in an infinite loop if something goes wrong
27        this.minUpdateTime = 300;           // Avoid updating the scroll position to often to avoid stutter in browsers
28
29        // Variables
30        this.object = null;                 // Object which needs to be scrolled (requires these getters and setters: pointerEvents, scroll.top, scroll.smooth)
31        this.smooth = true;                 // Smooth scroll can be disabled (but we still get the benefit of giving the the scroll time to converge)
32        this.ended = true;                  // Flag indicating if the loop has ended
33        this.success = false;
34        this.startTime = null;              //
35        this.lastScroll = null;             // Last measured scroll
36        this.lastUpdateTime = null;         // Last time the scroll was set
37        this.sameScrollCount = null;        // Count interations without scroll change
38        this.targetScroll = null;           //
39        this.getTargetScroll = null;        // Callback to update the target scroll
40        this.i = 0;                         // Iteration counter
41    }
42    // ────────────────────────────────────
43    start() {
44        // Remove flash element
45        this.object.flash('remove');
46        // Initialize scroll variables
47        this.targetScroll = this.getClampedTargetScroll();
48        this.lastScroll = Math.floor(this.object.scroll.top);
49        // End inmediately if we are already at the correct position
50        const scroll = Math.floor(this.object.scroll.top);
51        const distance = Math.abs(scroll-this.targetScroll);
52        if (distance < 3) {
53            this.stop(true);
54            return;
55        }
56        // Initialize time variables
57        this.startTime = Date.now();
58        this.lastUpdateTime = Date.now();
59        this.sameScrollCount = 0;
60        // Enable or disable smooth scroll on the DOM element
61        this.object.scroll.smooth = this.smooth;
62        // Set the scroll on the DOM element
63        this.object.scroll.top = this.targetScroll;
64        // Prevent users from interfering during scroll
65        this.object.pointerEvents = false;
66        // Prevent other processes from interfering during scroll (this also disables the mirror-to-textarea synchronization)
67        moaiEditor.scroll.sync.disabled = true;
68        // Show autoscrolling visual indicator (to facilitate bug tracking if the loop crashes)
69        moaiEditor.layout.indicatorScrolling.style.opacity = '1';
70        // Start loop
71        this.i = 0;
72        this.ended = false;
73        this.success = false;
74        clearInterval(this.interval);
75        this.interval = setInterval(this.onInterval.bind(this), 50);
76    }
77    // ────────────────────────────────────
78    halt() {
79        // Stop when switching editors
80        clearInterval(this.interval);
81    }
82    // ────────────────────────────────────
83    stop(reached) {
84        // TODO: Prevent errors: make sure to end the loop if the user switches editors, the element disapears, or the lines get deleted
85        this.ended = true;
86        this.success = reached;
87        clearInterval(this.interval);
88        // Disable smooth scroll
89        this.object.scroll.smooth = false;
90        // Re-enable pointer events
91        this.object.pointerEvents = true;
92        // Callback
93        moaiEditor.scroll.onScrollEnd();
94    }
95    // ────────────────────────────────────
96    getClampedTargetScroll() {
97        // Clamp the target scroll to handle out-of-bounds target values (else we will never reach the target scroll)
98        // Also handle edge cases while scroll is in progress:
99        //    a) preview is updated and preview element gets removed
100        //    b) document gets edited and line number no longer exist
101        try {
102            var target = Math.floor(this.getTargetScroll());
103            target = Math.max(0, target);
104            target = Math.min(target, this.object.scroll.max);
105            return target;
106        } catch ({name, message}) {
107            clearInterval(this.interval);
108            const errorString = "moaiEditor.scrollTo: "+name+": "+message;
109            const knownError = ['TypeError'].includes(name);
110            // Show known errors only in strict mode
111            if (knownError) {
112                if (moaiEditor.strict)
113                    console.warn(errorString);
114            }
115            // Always show unknown errors
116            else {
117                if (moaiEditor.strict)
118                    console.warn(errorString);
119                else
120                    console.error(errorString);
121            }
122            return null;
123        }
124    }
125    // ────────────────────────────────────
126    same(a, b) {
127        // Compare aproximate (and not exact) scroll positions to know if they are the same,
128        // because some browsers seem to not to be pixel-perfect at scrolling to a given position.
129        if (Math.abs(a - b) < 2)
130            return true;
131        return false;
132    }
133    // ────────────────────────────────────
134    onInterval() {
135
136        // Time variables
137        this.i += 1;
138        const now = Date.now();
139        const elapsed = now - this.startTime;
140        const lastUpdateElapsed = now - this.lastUpdateTime;
141        var minUpdateTime = 0;
142        if (this.smooth)
143            minUpdateTime = this.minUpdateTime;
144
145        // Stop if too much time has elapsed
146        if (elapsed > this.timeout) {
147            this.stop(false);
148            return;
149        }
150        // Get scroll values
151        const scroll = Math.floor(this.object.scroll.top);
152        const distance = Math.abs(scroll-this.targetScroll);
153        const targetScroll = this.getClampedTargetScroll();
154
155        // Stop if the target scroll could not be obtained
156        if (targetScroll === null) {
157            clearInterval(this.interval);
158            return;
159        }
160        // Detect if scroll has stopped
161        this.sameScrollCount += 1;
162        if (!this.same (scroll,this.lastScroll))
163            this.sameScrollCount = 0;
164
165        // Determine minUpdateTime
166
167        // Stop if we reached the intended scroll and enough time has passed
168        const targetScrollReached = distance <= 2;
169        const minFramesReached = this.sameScrollCount >= 3;
170        const minTimeLastUpdateReached = lastUpdateElapsed > minUpdateTime+50;
171        if (targetScrollReached   &&   minFramesReached   &&   minTimeLastUpdateReached) {
172            this.stop(true);
173            return;
174        }
175        // Correct scroll if needed and we are close to the previous destination (to avoid browser stutter)
176        if (!this.same (targetScroll,this.targetScroll)   &&   lastUpdateElapsed > minUpdateTime   &&   distance < 600) {
177            this.object.scroll.top = targetScroll;
178            this.targetScroll = targetScroll;
179            this.lastUpdateTime = now;
180        }
181        // Remember some values
182        this.lastScroll = scroll;
183    }
184}; // End Class
185
186
187
188
189
190
191
192