1/*  DokuWiki MoaiEditor Cm_main.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/*  CodeMirror main class
7    ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
8    Handles the CodeMirror plugin integration into MoaiEditor.
9
10
11    Codemirror DOM structure
12    ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
13    .CodeMirror                                               -- Main container
14      .CodeMirror-scroll                                      -- Scrolling element
15        .CodeMirror-sizer                                     -- Full height element (seems to hold the height of the full document even if few lines are rendered)
16          <div style="position:relative; top:233px;">         -- (↕)  Being moved up and down with respect to the parent (has some padding)
17            .CodeMirror-lines                                 -- (=h) Changes height constantly depending on the number of rendered lines (position:static)
18              <div style="position:relative; outline:none;">  -- (=h) (position:relative)
19                .CodeMirror-code                              -- (=h) (position:static)
20                  lines
21                                                                 (=h): Same height as parent (margin 0, padding 0)
22*/
23MoaiEditor.Codemirror = class {
24
25    constructor() {
26
27        // Constants
28        this.name = 'CodeMirror';   // Label to display to the user
29        this.isEditor = true;       // Identify editor plugins
30
31        // Variables
32        this.enabled = false;       // Flag to indicate if this editor is enabled right now
33        this.numHints = 0;          // Show animated hints only a limited number of times
34        this.settings = null;       // Settings button element (we fire a click event on it to enable/disable CM)
35        this.container = null;      // Codemirror container element (.Codemirror)
36        this.editor = null;         // Editor object (or null if CM is disabled)
37
38        // Objects
39        this.watcher = new MoaiEditor.CodemirrorWatcher(this);
40        this.scroll = new MoaiEditor.CodemirrorScroll(this);
41    }
42    // ┌───────────────────────────────────┐
43    // │ Public                            │
44    // └───────────────────────────────────┘
45
46    exists () {
47        this.toggler = this.getToggler();
48        if (this.toggler === null)
49            return false;
50        return true;
51    }
52    // ────────────────────────────────────
53    initBeforeLayout () {
54
55        if (!this.exists())
56            return;
57        // Put the settings img/button inside a container (because <img> tags cannot have children) and add an animated hint
58        this.settings = this.getSettingsButton();
59        this.settingsWrapper = moaiEditor.createHTML('<div id="moaied__cm_settings_wrapper"></div>');
60        this.settingsWrapper.appendChild(this.settings);
61        this.settingsHint = new MoaiEditor.Hint('arrow-right', 'Settings', this.settingsWrapper, 30, -3);
62
63        // Style the settings img/button and wrapper
64        this.settings.style.margin = '0';
65        this.settings.style.marginBottom = '15px';
66        this.settingsWrapper.style.display = 'none';
67        this.settingsWrapper.style.position = 'relative';
68
69        // Disable CodeMirror (if it was enabled before starting MoaiEditor)
70        if (this.isEnabled())
71            this.disable();
72
73        // Hide toggle option from CodeMirror menu (the user should enable/disable CodeMirror only through MoaiEditor now)
74        this.toggler.parentNode.style.display = 'none';
75        document.querySelector(".cm-settings-menu li.ui-menu-divider").style.display = 'none';
76    }
77    // ────────────────────────────────────
78    initAfterLayout () {
79
80        if (!this.exists())
81            return;
82        // Move the settings menu to the new layout
83        moaiEditor.layout.bottomRight.appendChild(this.settingsWrapper);
84        // Create flash box
85        this.flashbox = moaiEditor.createHTML('<div id="moaied__codemirror_flashbox" class="moaied-flashbox"></div>');
86    }
87    // ────────────────────────────────────
88    disable () {
89        if (!this.isEnabled())
90            return;
91        // Click the toggle button
92        this.toggler.click();
93        // Hide the settings
94        this.settingsWrapper.style.display = 'none';
95        // Hide the hint
96        this.settingsHint.disable();
97        // Set flag
98        this.enabled = false;
99    }
100    // ────────────────────────────────────
101    enable (clicked=false) {
102        if (this.isEnabled())
103            return;
104        /* Disable plugin»codemirror»autoheight=1, which sets viewportMargin=Infinty.
105         * This option makes sense in the vanilla editor to avoid nested scrolling, but it is not really
106         * needed in MoaiEditor where there is no nested scrolling anyways, and it will slow down or
107         * freeze the browser on big documents, and therefore it is recommended against in the manual:
108         * https://codemirror.net/5/doc/manual.html#option_viewportMargin
109         * Note: Trying to achieve the same result by doing this: this.editor.setOption('viewportMargin', 20)
110         *       inmediately after starting CodeMirror will sometimes trigger the following error:
111         *       "can't access property 'setOption', a is null"   ← 'a' being a minified name.
112         */
113        JSINFO.plugin_codemirror.autoheight = 0;
114        // Click the toggle button
115        this.toggler.click();
116        // Get DOM elements (.Codemirror)
117        this.container = this.getContainer();
118        this.scroller = this.container.querySelector(".CodeMirror-scroll");
119        // Create overlay to display dirty area (for debug)
120        this.dirty = moaiEditor.createHTML('<div class="moaied-show-dirty-area"></div>');
121        this.container.querySelector(".CodeMirror-sizer").appendChild(this.dirty);
122        // Get the editor object
123        this.editor = this.container.CodeMirror;
124        // Style the container
125        this.container.style.position = 'absolute';
126        this.container.style.top = '0';
127        this.container.style.left = '0';
128        this.container.style.width = '100%';
129        this.container.style.height = '100%';
130        this.container.style.border = 'none';
131        this.container.style.margin = '0';
132        // Display the settings
133        this.settingsWrapper.style.display = 'block';
134        // Show the hint when the user activates Codemirror (maximum 2 times)
135        if (clicked  &&  this.numHints < 2) {
136            this.settingsHint.start();
137            this.numHints += 1;
138        }
139        // Add event listeners
140        this.editor.on('scroll', this.onScroll.bind(this));
141        this.editor.on('refresh', this.onRefresh.bind(this));           // Fires on font size changes
142        this.editor.on('change', this.onDocumentChange.bind(this));
143        new ResizeObserver(this.onResize.bind(this)).observe(this.container);
144        // Copy textarea lines to the watcher
145        this.watcher.lines = this.editor.getValue().split("\n");
146        // Recalc scroll points
147        moaiEditor.matches.recalcScrollPoints();
148        // Set flag
149        this.enabled = true;
150    }
151    // ────────────────────────────────────
152    onAjax () {
153        // This method is required but we don't use it
154    }
155    // ────────────────────────────────────
156    getLineRect(linenum, mode='local') {
157
158        // Preparations
159        if (mode == 'viewport')
160            mode = 'window';
161        const top = this.editor.heightAtLine(linenum, mode);
162        const bottom = this.editor.heightAtLine(linenum+1, mode);
163        const height = bottom-top;
164        return {
165            top: top,
166            bottom: bottom,
167            height: height
168        };
169    }
170    // ────────────────────────────────────
171    addMatches(newMatches) {
172        // This method is required but we don't use it
173    }
174    removeMatches(startline, endline) {
175        // This method is required but we don't use it
176    }
177    // ────────────────────────────────────
178    setWrap(value) {
179        // CodeMirror has a hook on 'dw_editor.setWrap()'
180        dw_editor.setWrap (moaiEditor.layout.elements.textarea, value);
181        moaiEditor.matches.recalcScrollPoints();
182    }
183    // ────────────────────────────────────
184    set pointerEvents(boolean) {
185        if (boolean)
186            this.container.style.pointerEvents = 'auto';
187        else
188            this.container.style.pointerEvents = 'none';
189    }
190    // ────────────────────────────────────
191    flash(flash, data=null) {
192        if (flash == 'remove') {
193            this.flashbox.remove();
194            return;
195        }
196        if (flash === null)
197            return;
198        this.flashbox.remove();
199        this.flashbox = moaiEditor.createHTML('<div id="moaied__codemirror_flashbox" class="moaied-flashbox"></div>');
200        if (flash === false)
201            this.flashbox.classList.add('red');
202        const start = this.getLineRect(data.startline);
203        const end = this.getLineRect(data.endline);
204        const height = end.bottom - start.top;
205        const width = this.editor.getScrollInfo().width;
206        this.flashbox.style.top = start.top + 'px';
207        this.flashbox.style.left = '0px';
208        this.flashbox.style.width = width + 'px';
209        this.flashbox.style.height = height + 'px';
210        this.scroller.appendChild(this.flashbox);
211    }
212    // ────────────────────────────────────
213    get text() {
214        return this.editor.getValue();
215    }
216    // ┌───────────────────────────────────┐
217    // │ Input events                      │
218    // └───────────────────────────────────┘
219
220    onScroll() {
221        // Make sure we update the scroll position of newly rendered lines to avoid CodeMirror approximation errors
222        this.scroll.onScroll();
223        // Synchronize preview scroll
224        moaiEditor.scroll.sync.onScroll();
225    }
226    // ────────────────────────────────────
227
228    onToolbarButtonInput() {
229        this.onInput();
230    }
231    // ────────────────────────────────────
232    onDocumentChange() {
233        this.onInput();
234    }
235    // ────────────────────────────────────
236    onInput() {
237        if (this.enabled) {
238            this.watcher.onInput();
239        }
240    }
241    // ────────────────────────────────────
242    onRefresh() {
243        moaiEditor.matches.recalcScrollPoints();
244    }
245    // ────────────────────────────────────
246    onResize() {
247        moaiEditor.matches.recalcScrollPoints();
248    }
249    // ┌───────────────────────────────────┐
250    // │ Private                           │
251    // └───────────────────────────────────┘
252
253    onTextChanged(change) {
254
255        // Update the matches and scroll positions
256        moaiEditor.matches.onTextChanged(change);
257
258        // Keep track of changed text sections (for partial preview)
259        moaiEditor.dirty.onTextChanged(change);
260
261    }
262    // ────────────────────────────────────
263    isEnabled () {
264        if (this.getContainer() === null)
265            return false;
266        return true;
267    }
268    // ────────────────────────────────────
269    getToggler () {
270        return document.querySelector("#ui-id-73");
271    }
272    getContainer () {
273        return document.querySelector(".CodeMirror");
274    }
275    getSettingsButton () {
276        return document.querySelector("#size__ctl img.cm-settings-button");
277    }
278
279}; // End Class
280
281
282
283