1/*  DokuWiki MoaiEditor Scroll_to.js file
2    Version : 0.5b (2026-05-08)
3    Author  : MoaiTools <info@moaitools.org>
4    License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */
5
6/* This is main class of MoaiEditor.
7*/
8MoaiEditor.Main = class {
9
10    constructor() {
11
12        // We are only supporting 'edit' mode for now (prehaps 'preview' mode might be worth too, e.g on captcha errors)
13        if (JSINFO.ACT !== 'edit')
14            return;
15
16        // Exit if there is no textarea (e.g edittable plugin)
17        if (!document.body.querySelector("#wiki__text"))
18            return;
19
20        // Handle the 'hide_editor_by_default' option (available in the Dokuwiki configuration manager)
21        if (this.editorIsHidden())
22            return;
23
24        // Specify release
25        this.version_number = '0.5b';
26        this.version_date = '2026-05-08';
27
28        // Settings
29        this.strict = false;    // strict mode will show more errors and crash the script on some errors (disable before release)
30
31        // Variables
32        this.layoutReady = false;   // Prevent actions before the layout is ready
33
34        // Continue initialization after the constructor has finished (so that the object exists already)
35        setTimeout(this.init.bind(this), 30);
36    }
37    // ────────────────────────────────────
38    init() {
39
40        // Create the button that starts the editor
41        this.start = new MoaiEditor.StartButton();
42
43        // Initialize the settings variable which controls if the editor starts enabled by default (exposed to the user by this.buttons.enabled)
44        this.enabled = new MoaiEditor.LocalStorage('btn_enabled', 'off', ['on','off']);
45
46        // Detect the template in use (aka theme or skin) and find needed DOM elements (textarea, form, etc)
47        this.template = this.detectTemplate();                      // Detect the template in use
48        this.template.findElements();                               // Try to find the HTML elements we need (asynchronous process, will call 'startEditor' if successful and editor is enabled)
49        this.loadCSS('templates/'+this.template.name+'.css');       // Load an optional template-specific CSS file
50
51        // Handle compatibility with other plugins
52        this.plugins = {
53            'captcha'    : new MoaiEditor.Captcha(),
54            'codemirror' : new MoaiEditor.Codemirror(),
55        };
56    }
57    // ────────────────────────────────────
58    detectTemplate() {
59
60        // Try to identify the template currently in use and create an object to handle it
61        for (let name of moaiEditor_templates) {
62            var template = eval("new MoaiEditor.Template_"+name+"(name);");
63            if (template.detectTemplate()) {
64                return template;
65            }
66        }
67        // If the template currently in use was not identified, use the default class (in templates/default.js)
68        return eval("new MoaiEditor.Template('default');");
69    }
70    // ────────────────────────────────────
71    loadCSS(name)  {
72        // Load an on-demand CSS file and add it to the head of the document
73        const link = this.createHTML('<link href="lib/plugins/moaieditor/'+name+'" rel="stylesheet" type="text/css"/>');
74        document.head.appendChild(link);
75    }
76    // ────────────────────────────────────
77    startEditor()  {
78
79        /* This function starts the editor.
80
81           It will get called whenever:
82             - the editor is enabled by default and the asynchronous process started by 'this.template.findElements()' finishes successfuly.
83             - the 'start editor' button is clicked.
84
85           The following conditions must be met for the editor to start:
86             - The needed elements of the DOM must have been found (signaled by 'this.template.ready')
87             - The editor must not have started already (signaled by '!this.layoutReady')  */
88
89        // Return if conditions are not met
90        if (!this.template.ready  ||  this.layoutReady)
91            return;
92
93        // Remember some user settings we don't want to get changed outside MoaiEditor.
94        this.persistCookies = {
95            'cm-nativeeditor': DokuCookie.getValue('cm-nativeeditor')
96        };
97        // ────────────────── Create objects ───────────────────
98
99        // Create SVG icons
100        this.icons = new MoaiEditor.Icons();
101
102        // Handle AJAX request for previews
103        this.ajax = new MoaiEditor.Ajax(this);
104
105        // Handle the preview pane
106        this.preview = new MoaiEditor.Preview(this);
107
108        // Create buttons
109        this.buttons = new MoaiEditor.Buttons(this);
110
111        // Manage the native editor (textarea mirroring for syntax highlighting and synchronized scrolling)
112        this.mirror = new MoaiEditor.TextAreaMirror(this);
113
114        // Manage html-to-markup matching (for synchronized scrolling, syntax highlight, and for faster previews by rendering only parts of the document)
115        this.matches = new MoaiEditor.Matches(this);
116
117        // Manage partial previews (which means rendering only changed areas of the document for faster previews)
118        this.dirty = new MoaiEditor.Dirty(this);
119
120        // Manage automated scrolling (synchronized panes, clickable html headers, table of contents)
121        this.scroll = new MoaiEditor.Scroll(this);
122
123        // Manage table of contents
124        this.toc = new MoaiEditor.ToC(this);
125
126        // Manage visual inidicator whenever a draft is saved
127        this.draft = new MoaiEditor.Draft(this);
128
129        // Setup the new DOM layout
130        this.layout = new MoaiEditor.Layout(this.template.elements);
131
132        // Manage the current editor and switch between them (native, codemirror, prosemirror, etc)
133        this.editor = new MoaiEditor.Editor();
134
135        // ─────────── Initialize (done only once) ────────────
136
137        this.initPluginsBeforeLayout();
138        this.editor.init();
139        this.layout.init();
140        this.initPluginsAfterLayout();
141        this.mirror.init();
142        this.preview.init();
143        this.scroll.init();
144
145        // ─────────── Add various event listeners ────────────
146
147        // Window resize
148        window.addEventListener("resize", this.onWindowResize.bind(this));
149
150        // Preview
151        this.preview.container.addEventListener("scroll", this.preview.onScroll.bind(this.preview), {passive:true});
152
153        // Textarea
154        var textarea = this.template.elements.textarea;
155            /* Scroll */
156            textarea.addEventListener("scroll",   this.mirror.onTextareaScroll.bind  (this.mirror), {passive:true});
157            /* Text change */
158            textarea.addEventListener("input",   this.mirror.onTextChange.bind (this.mirror));
159            textarea.addEventListener("keydown", this.mirror.onTextChange.bind (this.mirror));
160            textarea.addEventListener("paste",   this.mirror.onTextChange.bind (this.mirror));
161            textarea.addEventListener("cut",     this.mirror.onTextChange.bind (this.mirror));
162            /* Selection/cursor change */
163            textarea.addEventListener("keyup",     this.mirror.onSelectionChange.bind (this.mirror));
164            textarea.addEventListener("mouseup",   this.mirror.onSelectionChange.bind (this.mirror));
165            textarea.addEventListener("mousedown", this.mirror.onSelectionChange.bind (this.mirror));
166            textarea.addEventListener("select",    this.mirror.onSelectionChange.bind (this.mirror));
167            textarea.addEventListener("focus",     this.mirror.onSelectionChange.bind (this.mirror));
168            /* Resize */
169            new ResizeObserver(this.mirror.onTextareaResize.bind(this.mirror)).observe(textarea);
170
171         //keypress, keydown, keyup, touchstart, mousedown, mousemove, click, input, paste, cut, select, selectstart, and focus.
172
173        // Dokuwiki toolbar (catch text changes by toolbar button actions)
174        for (let button of document.querySelectorAll("button.toolbutton"))
175            button.addEventListener("click", this.editor.onToolbarButtonInput.bind(this.editor));
176
177        // OnBeforeUnload
178        window.addEventListener("beforeunload", this.onBeforeUnload.bind(this));
179
180        // ────────────────────── Start ───────────────────────
181
182        requestAnimationFrame(() => {
183
184            // Update the style of the exit buttons (Save, Cancel, Back)
185            this.buttons.styleExitButtons();
186
187            // Allow actions now that the layout is ready
188            this.layoutReady = true;
189
190            // Start the editor pane
191            this.editor.start();
192
193            // Show initial preview
194            this.ajax.request();
195        });
196        // Create debug line
197        //this.createDebugLine();
198    }
199    // ────────────────────────────────────
200    initPluginsBeforeLayout()  {
201        for (let name in this.plugins)
202            if (this.plugins[name].initBeforeLayout)
203                this.plugins[name].initBeforeLayout();
204    }
205    initPluginsAfterLayout()  {
206        for (let name in this.plugins)
207            if (this.plugins[name].initAfterLayout)
208                this.plugins[name].initAfterLayout();
209    }
210    // ────────────────────────────────────
211    onWindowResize()  {
212        this.layout.onWindowResize();
213        this.toc.redraw();           // The width of the dummy dropdown needs refreshing
214    }
215    // ────────────────────────────────────
216    onBeforeUnload() {
217        // Restore some settings cookies we don't want to interfer with outside MoaiEditor
218        if (this.persistCookies)
219            for (let name in this.persistCookies)
220                DokuCookie.setValue(name, this.persistCookies[name]);
221        // If MoaiEditor is enabled by default prevent CodeMirror to start by default to improve startup speed on big documents
222        if (moaiEditor.enabled.value == 'on')
223            DokuCookie.setValue('cm-nativeeditor', '1');
224    }
225    // ────────────────────────────────────
226    createHTML(htmlString) {
227        var div = document.createElement('div');
228        div.innerHTML = htmlString.trim();
229        return div.firstChild;  // Change this to div.childNodes to support multiple top-level nodes.
230    }
231    // ────────────────────────────────────
232    editorIsHidden()  {
233        /* This function makes it possible to hide MoaiEditor from regular users and only make it
234           available on a per-browser basis. Useful if you want to test the plugin in a plublic wiki
235           but not make it available to all users yet. You can enable/disable this option in the DokuWiki
236           configuration manager.
237        */
238        // Return false if the setting is not enabled in Dokuwiki
239        if (JSINFO.plugin_moaieditor.hide_editor_by_default == '0')
240            return false;
241
242        // Initialize the local-storage variable
243        const isHidden = new MoaiEditor.LocalStorage('editor_is_hidden', '1', [0,1]);
244
245        // Change local-storage variable depending on user query-string variable
246        if (window.location.href.includes("showeditor=0"))
247            isHidden.value = '1';
248        if (window.location.href.includes("showeditor=1"))
249            isHidden.value = '0';
250
251        // Return boolean
252        if (isHidden.value == '1')
253            return true;
254        else
255            return false;
256    }
257    // ────────────────────────────────────
258    dokuMessage(message, lvl) {
259        /* Displays a dokuwiki message, similar to inc/infoutils.php -> msg()
260        */
261        const levels = {
262            '-1': 'error',
263            '0' : 'info',
264            '1' : 'success',
265            '2' : 'notify'
266        };
267        const cls = levels[lvl];
268        const msg = moaiEditor.createHTML(`<div class="${cls}">${message}</div>`);
269        document.querySelector("#moaied__msg_area").appendChild(msg);
270    }
271    // ────────────────────────────────────
272    createDebugLine() {
273        const line = this.createHTML("<div id='moai__debug'>  <div></div>  <div></div>  <div></div>  <div></div>  </div>");
274        document.querySelector("html").appendChild(line);
275    }
276}; // End Class
277
278
279
280