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