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