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