Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
If you make an edit, a temporary account will be created for you.

MediaWiki:Gadget-PreloadEditor.js: Difference between revisions

MediaWiki interface page
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
Line 3: Line 3:


     const PreloadEditor = {
     const PreloadEditor = {
         version: '0.7.0',
         version: '0.8.0',


         state: {
         state: {
Line 15: Line 15:
             deletedVariables: [],
             deletedVariables: [],
             mode: 'idle',
             mode: 'idle',
            /*
            * Keep preload and config dirty independently.
            * This is important because "Save Config" must not
            * accidentally mark an unsaved preload as saved.
            */
            preloadDirty: false,
            configDirty: false,
             dirty: false
             dirty: false
         },
         },
Line 25: Line 34:
         status: null,
         status: null,
         editorTools: null,
         editorTools: null,
        pageResizeHandler: null,


         draftStorageKey:
         draftStorageKey:
Line 34: Line 44:
         },
         },


        // ------------------------------------------------------------
         // Browser-local draft storage
         // Browser-local draft storage
        // ------------------------------------------------------------


         getDraftStorage() {
         getDraftStorage() {
Line 148: Line 160:


             return {
             return {
                 version: 1,
                 version: 2,


                 preloadTitle:
                 preloadTitle:
Line 197: Line 209:
                     this.state.mode ||
                     this.state.mode ||
                     'edit',
                     'edit',
                preloadDirty:
                    !!this.state.preloadDirty,
                configDirty:
                    !!this.state.configDirty,


                 dirty: true,
                 dirty: true,
Line 463: Line 481:
                     draft.mode ||
                     draft.mode ||
                     'edit',
                     'edit',
                preloadDirty:
                    draft.preloadDirty !== undefined
                        ? !!draft.preloadDirty
                        : true,
                configDirty:
                    draft.configDirty !== undefined
                        ? !!draft.configDirty
                        : true,


                 dirty: true
                 dirty: true
Line 485: Line 513:
         },
         },


        // ------------------------------------------------------------
        // Page / viewport mode
        // ------------------------------------------------------------
        applyPageMode() {
            $('body')
                .addClass(
                    'preload-editor-page'
                );
            /*
            * Hide the normal MediaWiki page heading.
            * The editor supplies its own internal headings.
            */
            $(
                '#firstHeading, ' +
                '.firstHeading'
            ).hide();
            /*
            * Remove the normal heading spacing.
            */
            $(
                '#mw-content-text'
            ).css({
                marginTop: 0
            });
            /*
            * Make the editor occupy the remaining viewport
            * and keep the outer page from scrolling.
            */
            this.updatePageHeight();
            if (!this.pageResizeHandler) {
                this.pageResizeHandler =
                    () =>
                        this.updatePageHeight();
                window.addEventListener(
                    'resize',
                    this.pageResizeHandler
                );
            }
        },
        updatePageHeight() {
            if (!this.root || !this.root.length) {
                return;
            }
            const element =
                this.root.get(0);
            if (!element) {
                return;
            }
            const rect =
                element.getBoundingClientRect();
            const height =
                Math.max(
                    300,
                    window.innerHeight -
                    rect.top
                );
            this.root.css({
                height:
                    height + 'px',
                maxHeight:
                    height + 'px'
            });
        },
        // ------------------------------------------------------------
         // API
         // API
        // ------------------------------------------------------------


         async getPage(title) {
         async getPage(title) {
Line 575: Line 682:
         },
         },


        // ------------------------------------------------------------
         // Preload parsing
         // Preload parsing
        // ------------------------------------------------------------


         extractIncludeonly(source) {
         extractIncludeonly(source) {
Line 688: Line 797:
         },
         },


        // ------------------------------------------------------------
         // Config
         // Config
        // ------------------------------------------------------------


         getAutomaticPageField() {
         getAutomaticPageField() {
Line 990: Line 1,101:
         },
         },


        // ------------------------------------------------------------
         // Transformation helpers
         // Transformation helpers
        // ------------------------------------------------------------


         transformationObject(
         transformationObject(
Line 1,034: Line 1,147:
         },
         },


        // ------------------------------------------------------------
         // UI helpers
         // UI helpers
        // ------------------------------------------------------------


         createSection(
         createSection(
Line 1,207: Line 1,322:
         },
         },


        // ------------------------------------------------------------
         // In-wiki dialogs
         // In-wiki dialogs
        // ------------------------------------------------------------


         createConfirmDialog(
         createConfirmDialog(
Line 1,282: Line 1,399:
                         .append(overlay);
                         .append(overlay);


                     const close =
                     overlay.attr(
                         value => {
                         'tabindex',
                            if (value === 'Escape') {
                        '-1'
                                cancel.trigger('click');
                    );
                            }
 
                        };
                    overlay.trigger('focus');


                     overlay.on(
                     overlay.on(
                         'keydown.preloadEditor',
                         'keydown.preloadEditor',
                         event => {
                         event => {
                             close(event.key);
                             if (
                                event.key === 'Escape'
                            ) {
                                cancel.trigger('click');
                            }
 
                            if (
                                event.key === 'Enter'
                            ) {
                                confirm.trigger('click');
                            }
                         }
                         }
                     );
                     );


                    overlay.attr(
                        'tabindex',
                        '-1'
                    );
                    overlay.trigger('focus');
                    /*
                    * Keep the browser from submitting or
                    * otherwise handling Enter/Escape itself.
                    */
                     overlay.on(
                     overlay.on(
                         'click.preloadEditor',
                         'click.preloadEditor',
Line 1,423: Line 1,539:
         },
         },


         // CodeMirror
         createConfigReviewDialog() {
            return new Promise(
                resolve => {
                    const overlay =
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt-overlay preload-editor-config-review-overlay'
                            );


        destroyEditor() {
                    const box =
            if (!this.editor) {
                        $('<div>')
                this.editorView = null;
                            .addClass(
                this.editorTools = null;
                                'preload-editor-prompt preload-editor-config-review'
                return;
                            );
            }


            const {
                    $('<h2>')
                view,
                        .text(
                inputHandler
                            'Generated configuration'
            } = this.editor;
                        )
                        .appendTo(box);


            if (
                    $('<p>')
                view &&
                        .text(
                inputHandler
                            'This is the JSON that will be uploaded to the preload\'s config.json page.'
            ) {
                        )
                view.dom.removeEventListener(
                        .appendTo(box);
                    'input',
                    inputHandler
                );
            }


            if (view) {
                    const preview =
                view.destroy();
                        $('<pre>')
            }
                            .addClass(
                                'preload-editor-config-review-preview'
                            )
                            .text(
                                this.getConfigJSON()
                            );


            this.editor = null;
                    box.append(preview);
            this.editorView = null;
            this.editorTools = null;
        },


        getEditorView() {
                    const buttons =
            return this.editorView;
                        $('<div>')
        },
                            .addClass(
                                'preload-editor-prompt-buttons'
                            );


        getEditorContent() {
                    const close =
            if (
                        this.createButton(
                this.editorView &&
                            'Close',
                this.editorView.state
                            'preload-editor-button-primary',
            ) {
                            () => {
                return this.editorView.state.doc.toString();
                                overlay.remove();
            }
                                resolve();
                            }
                        );


            return (
                    buttons.append(close);
                this.state.preloadContent || ''
                    box.append(buttons);
            );
                    overlay.append(box);
        },


        setEditorContent(content) {
                    $(document.body)
            content =
                        .append(overlay);
                String(content || '');


            if (!this.editorView) {
                    overlay.attr(
                this.state.preloadContent =
                        'tabindex',
                     content;
                        '-1'
                return;
                     );
            }


            const current =
                    overlay.trigger('focus');
                this.editorView.state.doc.toString();


            if (current === content) {
                    overlay.on(
                return;
                        'keydown.preloadEditor',
            }
                        event => {
                            if (
                                event.key === 'Escape'
                            ) {
                                close.trigger('click');
                            }
                        }
                    );


            this.editorView.dispatch({
                    overlay.on(
                changes: {
                        'click.preloadEditor',
                    from: 0,
                        event => {
                    to:
                            if (
                         this.editorView.state.doc.length,
                                event.target ===
                     insert: content
                                overlay.get(0)
                            ) {
                                close.trigger('click');
                            }
                         }
                     );
                 }
                 }
             });
             );
         },
         },


         getEditorSelection() {
         // ------------------------------------------------------------
        // CodeMirror
        // ------------------------------------------------------------
 
        destroyEditor() {
            if (!this.editor) {
                this.editorView = null;
                this.editorTools = null;
                return;
            }
 
            const {
                view,
                inputHandler
            } = this.editor;
 
             if (
             if (
                 !this.editorView ||
                 view &&
                 !this.editorView.state
                 inputHandler
             ) {
             ) {
                 return null;
                 view.dom.removeEventListener(
                    'input',
                    inputHandler
                );
             }
             }


             const selection =
             if (view) {
                 this.editorView.state
                 view.destroy();
                    .selection.main;
            }


             return {
             this.editor = null;
                from:
            this.editorView = null;
                    selection.from,
            this.editorTools = null;
        },


                to:
        getEditorView() {
                    selection.to,
            return this.editorView;
 
                text:
                    this.editorView.state.doc.sliceString(
                        selection.from,
                        selection.to
                    )
            };
         },
         },


         focusEditor() {
         getEditorContent() {
             if (this.editorView) {
             if (
                 this.editorView.focus();
                this.editorView &&
                this.editorView.state
            ) {
                 return this.editorView.state.doc.toString();
             }
             }
            return (
                this.state.preloadContent || ''
            );
         },
         },


         insertEditorText(
         setEditorContent(content) {
             text,
             content =
            selectionFrom,
                String(content || '');
            selectionTo,
 
            cursorPosition
        ) {
             if (!this.editorView) {
             if (!this.editorView) {
                this.state.preloadContent =
                    content;
                 return;
                 return;
             }
             }


             const view =
             const current =
                 this.editorView;
                 this.editorView.state.doc.toString();


             const selection =
             if (current === content) {
                 view.state.selection.main;
                 return;
            }


             const from =
             this.editorView.dispatch({
                selectionFrom === undefined
                    ? selection.from
                    : selectionFrom;
 
            const to =
                selectionTo === undefined
                    ? selection.to
                    : selectionTo;
 
            const cursor =
                cursorPosition === undefined
                    ? from + text.length
                    : cursorPosition;
 
            view.dispatch({
                 changes: {
                 changes: {
                     from,
                     from: 0,
                     to,
                     to:
                    insert: text
                        this.editorView.state.doc.length,
                },
                     insert: content
 
                selection: {
                     anchor: cursor
                 }
                 }
             });
             });
            view.focus();
         },
         },


         // Editor tools
         getEditorSelection() {
 
        wrapSelectionWithVariable() {
            const selected =
                this.getEditorSelection();
 
             if (
             if (
                 !selected ||
                 !this.editorView ||
                 selected.from === selected.to
                 !this.editorView.state
             ) {
             ) {
                this.focusEditor();
                 return null;
                 return;
             }
             }


             const replacement =
             const selection =
                 '%%' +
                 this.editorView.state
                 selected.text +
                    .selection.main;
                 '%%';
 
            return {
                 from:
                    selection.from,
 
                 to:
                    selection.to,


            this.insertEditorText(
                text:
                replacement,
                    this.editorView.state.doc.sliceString(
                selected.from,
                        selection.from,
                selected.to,
                        selection.to
                selected.from +
                     )
                     replacement.length
             };
             );
         },
         },


         insertVariable(name) {
         focusEditor() {
             name =
             if (this.editorView) {
                String(name || '')
                this.editorView.focus();
                    .trim()
            }
                    .toUpperCase();
        },


             if (!name) {
        insertEditorText(
            text,
            selectionFrom,
            selectionTo,
            cursorPosition
        ) {
             if (!this.editorView) {
                 return;
                 return;
             }
             }


             const selected =
             const view =
                 this.getEditorSelection();
                 this.editorView;


             if (!selected) {
             const selection =
                 return;
                 view.state.selection.main;
            }


             this.insertEditorText(
             const from =
                 '%%' + name + '%%',
                 selectionFrom === undefined
                selected.from,
                    ? selection.from
                selected.to
                    : selectionFrom;
            );
        },


        insertCondition(
             const to =
            type,
                 selectionTo === undefined
             variable
                     ? selection.to
        ) {
                    : selectionTo;
            type =
                 String(type || '')
                     .toLowerCase();


             variable =
             const cursor =
                 String(variable || '')
                 cursorPosition === undefined
                     .trim()
                     ? from + text.length
                     .toUpperCase();
                     : cursorPosition;


             const selected =
             view.dispatch({
                 this.getEditorSelection();
                 changes: {
                    from,
                    to,
                    insert: text
                },


            if (!selected) {
                selection: {
                 return;
                    anchor: cursor
             }
                 }
             });


             let text;
             view.focus();
            let cursorOffset;
        },


            switch (type) {
        // ------------------------------------------------------------
                case 'if': {
        // Editor tools
                    if (!variable) {
        // ------------------------------------------------------------
                        return;
                    }


                    const opening =
        wrapSelectionWithVariable() {
                        '<!-- @if ' +
            const selected =
                        variable +
                this.getEditorSelection();
                        ' -->';


                    const closing =
            if (
                        '<!-- @endif -->';
                !selected ||
                selected.from === selected.to
            ) {
                this.focusEditor();
                return;
            }


                    if (
            const replacement =
                        selected.from !==
                '%%' +
                        selected.to
                selected.text +
                    ) {
                '%%';
                        text =
                            opening +
                            '\n' +
                            selected.text +
                            '\n' +
                            closing;


                        cursorOffset =
             this.insertEditorText(
                            text.length;
                 replacement,
                    } else {
                        text =
                            opening +
                            '\n\n' +
                            closing;
 
                        cursorOffset =
                            opening.length + 1;
                    }
 
                    break;
                }
 
                case 'elseif':
                    text =
                        '<!-- @elseif -->';
                    cursorOffset =
                        text.length;
                    break;
 
                case 'else':
                    text =
                        '<!-- @else -->';
                    cursorOffset =
                        text.length;
                    break;
 
                case 'endif':
                    text =
                        '<!-- @endif -->';
                    cursorOffset =
                        text.length;
                    break;
 
                default:
                    return;
            }
 
             this.insertEditorText(
                 text,
                 selected.from,
                 selected.from,
                 selected.to,
                 selected.to,
                 selected.from +
                 selected.from +
                     cursorOffset
                     replacement.length
             );
             );
         },
         },


         getToolVariables() {
         insertVariable(name) {
             const variables =
             name =
                 new Set(['PAGE']);
                 String(name || '')
                    .trim()
                    .toUpperCase();


             if (
             if (!name) {
                this.analysis &&
                 return;
                this.analysis.variables
            ) {
                 this.analysis.variables.forEach(
                    name => {
                        variables.add(
                            String(
                                name
                            ).toUpperCase()
                        );
                    }
                );
             }
             }


             return Array.from(variables);
             const selected =
        },
                this.getEditorSelection();


        createToolSelect(
            if (!selected) {
            options,
                 return;
            className
            }
        ) {
            const select =
                 $('<select>')
                    .addClass(
                        'preload-editor-tool-select'
                    );


             if (className) {
             this.insertEditorText(
                 select.addClass(className);
                 '%%' + name + '%%',
            }
                selected.from,
                selected.to
            );
        },


             options.forEach(
        insertCondition(
                option => {
            type,
                    $('<option>')
             variable
                        .attr(
        ) {
                            'value',
            type =
                            option.value
                String(type || '')
                        )
                    .toLowerCase();
                        .text(option.label)
 
                        .appendTo(select);
            variable =
                }
                String(variable || '')
            );
                    .trim()
                    .toUpperCase();


             return select;
             const selected =
        },
                this.getEditorSelection();


        updateEditorToolVariables() {
             if (!selected) {
             if (!this.editorTools) {
                 return;
                 return;
             }
             }


             const variables =
             let text;
                this.getToolVariables();
            let cursorOffset;


             const current =
             switch (type) {
                 this.editorTools
                 case 'if': {
                    .variableSelect
                     if (!variable) {
                     .val();
                        return;
                    }


            const currentCondition =
                    const opening =
                this.editorTools
                        '<!-- @if ' +
                    .conditionVariableSelect
                        variable +
                    .val();
                        ' -->';


            const populate =
                    const closing =
                (
                         '<!-- @endif -->';
                    select,
                    value
                ) => {
                    select.empty();
 
                    variables.forEach(
                         name => {
                            $('<option>')
                                .attr(
                                    'value',
                                    name
                                )
                                .text(name)
                                .appendTo(select);
                        }
                    );


                     if (
                     if (
                         value &&
                         selected.from !==
                         variables.includes(value)
                         selected.to
                     ) {
                     ) {
                         select.val(value);
                         text =
                    } else if (
                            opening +
                        variables.length
                            '\n' +
                    ) {
                            selected.text +
                        select.val(
                             '\n' +
                             variables[0]
                            closing;
                        );
                    }
                };


            populate(
                        cursorOffset =
                this.editorTools.variableSelect,
                            text.length;
                current
                    } else {
            );
                        text =
                            opening +
                            '\n\n' +
                            closing;


            populate(
                        cursorOffset =
                this.editorTools.conditionVariableSelect,
                            opening.length + 1;
                currentCondition
                    }
            );
        },


        updateConditionVariableVisibility() {
                    break;
            if (!this.editorTools) {
                 }
                 return;
            }


            const type =
                case 'elseif':
                this.editorTools
                    text =
                     .conditionTypeSelect
                        '<!-- @elseif -->';
                     .val();
                     cursorOffset =
                        text.length;
                     break;


            this.editorTools
                 case 'else':
                 .conditionVariableWrap
                     text =
                .toggle(
                        '<!-- @else -->';
                     type === 'if'
                    cursorOffset =
                );
                        text.length;
        },
                    break;


        renderEditorTools() {
                case 'endif':
            const tools =
                    text =
                $('<div>')
                        '<!-- @endif -->';
                     .addClass(
                     cursorOffset =
                         'preload-editor-tools'
                         text.length;
                     );
                     break;


            const wrapGroup =
                 default:
                 $('<div>')
                     return;
                     .addClass(
            }
                        'preload-editor-tool-group preload-editor-tool-group-wrap'
                    );


             wrapGroup.append(
             this.insertEditorText(
                 this.createButton(
                 text,
                    'Wrap selection with %%',
                selected.from,
                    'preload-editor-tool-button',
                selected.to,
                    () =>
                selected.from +
                        this.wrapSelectionWithVariable()
                    cursorOffset
                )
             );
             );
        },


             const variableGroup =
        getToolVariables() {
                 $('<div>')
             const variables =
                    .addClass(
                 new Set(['PAGE']);
                        'preload-editor-tool-group preload-editor-tool-group-variable'
                    );


             $('<span>')
             if (
                 .addClass(
                 this.analysis &&
                    'preload-editor-tool-label'
                this.analysis.variables
                )
            ) {
                 .text(
                 this.analysis.variables.forEach(
                     'Insert existing variable'
                     name => {
                )
                        variables.add(
                .appendTo(variableGroup);
                            String(
 
                                name
            const variableSelect =
                            ).toUpperCase()
                this.createToolSelect(
                        );
                    [],
                     }
                     'preload-editor-variable-select'
                 );
                 );
            }


             variableGroup.append(
             return Array.from(variables);
                variableSelect,
        },


                this.createButton(
        createToolSelect(
                    'Insert',
            options,
                    'preload-editor-tool-button',
            className
                    () =>
        ) {
                        this.insertVariable(
             const select =
                            variableSelect.val()
                 $('<select>')
                        )
                )
            );
 
             const conditionGroup =
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-tool-group preload-editor-tool-group-condition'
                         'preload-editor-tool-select'
                     );
                     );


             $('<span>')
             if (className) {
                .addClass(
                select.addClass(className);
                    'preload-editor-tool-label'
            }
                )
 
                .text('Insert condition')
            options.forEach(
                .appendTo(conditionGroup);
                option => {
                    $('<option>')
                        .attr(
                            'value',
                            option.value
                        )
                        .text(option.label)
                        .appendTo(select);
                }
            );


             const conditionTypeSelect =
             return select;
                this.createToolSelect(
        },
                    [
                        {
                            value: 'if',
                            label: '@if'
                        },
                        {
                            value: 'elseif',
                            label: '@elseif'
                        },
                        {
                            value: 'else',
                            label: '@else'
                        },
                        {
                            value: 'endif',
                            label: '@endif'
                        }
                    ],
                    'preload-editor-condition-type-select'
                );


             const conditionVariableSelect =
        updateEditorToolVariables() {
                this.createToolSelect(
             if (!this.editorTools) {
                    [],
                 return;
                    'preload-editor-condition-variable-select'
            }
                 );


             const conditionVariableWrap =
             const variables =
                 $('<span>')
                 this.getToolVariables();
                    .addClass(
                        'preload-editor-condition-variable'
                    )
                    .append(
                        conditionVariableSelect
                    );


             const insertCondition =
             const current =
                 this.createButton(
                 this.editorTools
                     'Insert',
                     .variableSelect
                     'preload-editor-tool-button',
                     .val();
                    () =>
                        this.insertCondition(
                            conditionTypeSelect.val(),
                            conditionVariableSelect.val()
                        )
                );


             conditionTypeSelect.on(
             const currentCondition =
                 'change.preloadEditor',
                 this.editorTools
                () =>
                    .conditionVariableSelect
                     this.updateConditionVariableVisibility()
                     .val();
            );


             conditionGroup.append(
             const populate =
                 conditionTypeSelect,
                (
                 conditionVariableWrap,
                    select,
                 insertCondition
                    value
                ) => {
                    select.empty();
 
                    variables.forEach(
                        name => {
                            $('<option>')
                                .attr(
                                    'value',
                                    name
                                )
                                .text(name)
                                .appendTo(select);
                        }
                    );
 
                    if (
                        value &&
                        variables.includes(value)
                    ) {
                        select.val(value);
                    } else if (
                        variables.length
                    ) {
                        select.val(
                            variables[0]
                        );
                    }
                 };
 
            populate(
                 this.editorTools.variableSelect,
                 current
             );
             );


             tools.append(
             populate(
                 wrapGroup,
                 this.editorTools.conditionVariableSelect,
                variableGroup,
                 currentCondition
                 conditionGroup
             );
             );
        },


             this.editorTools = {
        updateConditionVariableVisibility() {
                 root: tools,
             if (!this.editorTools) {
                variableSelect,
                 return;
                conditionTypeSelect,
             }
                conditionVariableSelect,
                conditionVariableWrap,
                insertCondition
             };


             this.updateEditorToolVariables();
             const type =
            this.updateConditionVariableVisibility();
                this.editorTools
                    .conditionTypeSelect
                    .val();


             return tools;
             this.editorTools
                .conditionVariableWrap
                .toggle(
                    type === 'if'
                );
         },
         },


         updateEditorTools() {
         renderEditorTools() {
             this.updateEditorToolVariables();
             const tools =
            this.updateConditionVariableVisibility();
                $('<div>')
        },
                    .addClass(
                        'preload-editor-tools'
                    );


        // Syntax highlighting
            const wrapGroup =
                $('<div>')
                    .addClass(
                        'preload-editor-tool-group preload-editor-tool-group-wrap'
                    );


        getKnownVariables() {
            wrapGroup.append(
             const known =
                this.createButton(
                 new Set(['PAGE']);
                    'Wrap selection with %%',
 
                    'preload-editor-tool-button',
             const fields =
                    () =>
                 this.state.config &&
                        this.wrapSelectionWithVariable()
                 this.state.config.fields;
                )
            );
 
             const variableGroup =
                 $('<div>')
                    .addClass(
                        'preload-editor-tool-group preload-editor-tool-group-variable'
                    );
 
             $('<span>')
                 .addClass(
                    'preload-editor-tool-label'
                )
                .text(
                    'Insert existing variable'
                )
                 .appendTo(variableGroup);


             if (
             const variableSelect =
                fields &&
                 this.createToolSelect(
                typeof fields === 'object'
                     [],
            ) {
                     'preload-editor-variable-select'
                 Object.keys(fields).forEach(
                     name => {
                        known.add(
                            String(
                                name
                            ).toUpperCase()
                        );
                     }
                 );
                 );
            }


             return known;
             variableGroup.append(
        },
                variableSelect,


        createSyntaxDecorations(
                this.createButton(
            state,
                    'Insert',
            Decoration
                    'preload-editor-tool-button',
        ) {
                    () =>
            const decorations = [];
                        this.insertVariable(
            const source =
                            variableSelect.val()
                state.doc.toString();
                        )
                )
            );


             const knownVariables =
             const conditionGroup =
                 this.getKnownVariables();
                 $('<div>')
                    .addClass(
                        'preload-editor-tool-group preload-editor-tool-group-condition'
                    );


             const variablePattern =
             $('<span>')
                 /%%([A-Za-z0-9_-]+)%%/g;
                 .addClass(
                    'preload-editor-tool-label'
                )
                .text('Insert condition')
                .appendTo(conditionGroup);


             let match;
             const conditionTypeSelect =
 
                 this.createToolSelect(
            while (
                     [
                 (
                        {
                     match =
                            value: 'if',
                         variablePattern.exec(
                            label: '@if'
                             source
                        },
                         )
                         {
                ) !== null
                            value: 'elseif',
            ) {
                             label: '@elseif'
                const name =
                        },
                     match[1].toUpperCase();
                         {
 
                            value: 'else',
                 const known =
                            label: '@else'
                    knownVariables.has(name);
                        },
                        {
                            value: 'endif',
                            label: '@endif'
                        }
                     ],
                    'preload-editor-condition-type-select'
                 );


                 decorations.push(
            const conditionVariableSelect =
                     Decoration.mark({
                 this.createToolSelect(
                        class: [
                     [],
                            'preload-editor-syntax-variable',
                    'preload-editor-condition-variable-select'
                            known
                                ? 'preload-editor-syntax-variable-known'
                                : 'preload-editor-syntax-variable-unknown'
                        ].join(' ')
                    }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                 );
                 );
            }


             const conditionPattern =
             const conditionVariableWrap =
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                 $('<span>')
                    .addClass(
                        'preload-editor-condition-variable'
                    )
                    .append(
                        conditionVariableSelect
                    );


             while (
             const insertCondition =
                 (
                 this.createButton(
                     match =
                     'Insert',
                         conditionPattern.exec(
                    'preload-editor-tool-button',
                             source
                    () =>
                         this.insertCondition(
                             conditionTypeSelect.val(),
                            conditionVariableSelect.val()
                         )
                         )
                ) !== null
            ) {
                decorations.push(
                    Decoration.mark({
                        class:
                            'preload-editor-syntax-condition'
                    }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                 );
                 );


                 const directiveStart =
            conditionTypeSelect.on(
                     match.index +
                 'change.preloadEditor',
                    match[0].indexOf('@');
                () =>
                     this.updateConditionVariableVisibility()
            );
 
            conditionGroup.append(
                conditionTypeSelect,
                conditionVariableWrap,
                insertCondition
            );


                decorations.push(
            tools.append(
                    Decoration.mark({
                wrapGroup,
                        class:
                variableGroup,
                            'preload-editor-syntax-directive'
                conditionGroup
                    }).range(
            );
                        directiveStart,
                        directiveStart +
                            match[1].length +
                            1
                    )
                );


                 const type =
            this.editorTools = {
                    match[1].toLowerCase();
                root: tools,
                variableSelect,
                 conditionTypeSelect,
                conditionVariableSelect,
                conditionVariableWrap,
                insertCondition
            };


                if (
            this.updateEditorToolVariables();
                    type !== 'if' &&
            this.updateConditionVariableVisibility();
                    type !== 'elseif'
                ) {
                    continue;
                }


                const expression =
            return tools;
                    match[2];
        },


                const expressionStart =
        updateEditorTools() {
                    match.index +
            this.updateEditorToolVariables();
                    match[0].indexOf(
            this.updateConditionVariableVisibility();
                        expression
        },
                    );


                const expressionPattern =
        // ------------------------------------------------------------
                    /[A-Za-z0-9_-]+/g;
        // Syntax highlighting
        // ------------------------------------------------------------


                 let expressionMatch;
        getKnownVariables() {
            const known =
                 new Set(['PAGE']);


                while (
            const fields =
                    (
                this.state.config &&
                        expressionMatch =
                 this.state.config.fields;
                            expressionPattern.exec(
                                expression
                            )
                    ) !== null
                 ) {
                    const name =
                        expressionMatch[0]
                            .toUpperCase();


                    if (
            if (
                         !knownVariables.has(name)
                fields &&
                    ) {
                typeof fields === 'object'
                         continue;
            ) {
                Object.keys(fields).forEach(
                    name => {
                         known.add(
                            String(
                                name
                            ).toUpperCase()
                         );
                     }
                     }
                );
            }


                    const start =
            return known;
                        expressionStart +
        },
                        expressionMatch.index;


                    decorations.push(
        createSyntaxDecorations(
                        Decoration.mark({
            state,
                            class:
            Decoration
                                'preload-editor-syntax-condition-variable'
        ) {
                        }).range(
            const decorations = [];
                            start,
            const source =
                            start +
                state.doc.toString();
                                expressionMatch[0]
                                    .length
                        )
                    );
                }
            }


             return Decoration.set(
             const knownVariables =
                decorations,
                this.getKnownVariables();
                true
            );
        },


        async installSyntaxHighlighting(view) {
             const variablePattern =
             await mw.loader.using(
                 /%%([A-Za-z0-9_-]+)%%/g;
                 'ext.CodeMirror.v6.lib'
            );


             const require =
             let match;
                mw.loader.require;


             const {
             while (
                 StateField,
                 (
                StateEffect,
                    match =
                EditorView,
                        variablePattern.exec(
                 Decoration
                            source
             } = require(
                        )
                'ext.CodeMirror.v6.lib'
                 ) !== null
            );
             ) {
                const name =
                    match[1].toUpperCase();


            const syntaxField =
                const known =
                StateField.define({
                     knownVariables.has(name);
                     create: state =>
                        this.createSyntaxDecorations(
                            state,
                            Decoration
                        ),


                     update: (
                decorations.push(
                         decorations,
                     Decoration.mark({
                         transaction
                         class: [
                     ) =>
                            'preload-editor-syntax-variable',
                         transaction.docChanged
                            known
                            ? this.createSyntaxDecorations(
                                ? 'preload-editor-syntax-variable-known'
                                transaction.state,
                                : 'preload-editor-syntax-variable-unknown'
                                Decoration
                         ].join(' ')
                            )
                     }).range(
                            : decorations,
                         match.index,
                        match.index +
                            match[0].length
                    )
                );
            }


                    provide: field =>
            const conditionPattern =
                        EditorView.decorations.from(
                /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                            field
                        )
                });


             view.dispatch({
             while (
                 effects:
                (
                     StateEffect.appendConfig.of(
                    match =
                         syntaxField
                        conditionPattern.exec(
                            source
                        )
                ) !== null
            ) {
                 decorations.push(
                    Decoration.mark({
                        class:
                            'preload-editor-syntax-condition'
                     }).range(
                        match.index,
                         match.index +
                            match[0].length
                     )
                     )
            });
                );


            this.editor.syntaxField =
                const directiveStart =
                syntaxField;
                    match.index +
        },
                    match[0].indexOf('@');


        async mountCodeMirror(textarea) {
                decorations.push(
            return new Promise(
                    Decoration.mark({
                (
                        class:
                    resolve,
                            'preload-editor-syntax-directive'
                     reject
                    }).range(
                 ) => {
                        directiveStart,
                    let settled = false;
                        directiveStart +
                            match[1].length +
                            1
                     )
                 );


                    const cleanup =
                const type =
                        () => {
                    match[1].toLowerCase();
                            mw.hook(
                                'dev.CCM.ready'
                            ).remove(onReady);


                            mw.hook(
                if (
                                'dev.CCM.load'
                    type !== 'if' &&
                            ).remove(onLoad);
                    type !== 'elseif'
                        };
                ) {
                    continue;
                }


                    const onReady =
                const expression =
                        async cm => {
                    match[2];
                            if (
                                settled ||
                                !cm ||
                                cm.textarea !== textarea ||
                                !cm.view
                            ) {
                                return;
                            }


                            settled = true;
                const expressionStart =
                            cleanup();
                    match.index +
                    match[0].indexOf(
                        expression
                    );


                            const view =
                const expressionPattern =
                                cm.view;
                    /[A-Za-z0-9_-]+/g;


                            const inputHandler =
                let expressionMatch;
                                () => {
                                    this.state
                                        .preloadContent =
                                        view.state.doc.toString();


                                    this.state.dirty =
                while (
                                        true;
                    (
 
                        expressionMatch =
                                    this.reanalyze();
                            expressionPattern.exec(
 
                                expression
                                    this.renderVariablesBySelector();
                            )
                                    this.renderVariableConfigurationBySelector();
                    ) !== null
                                    this.renderAnalysisBySelector();
                ) {
                                    this.renderConfigPreviewBySelector();
                    const name =
                                    this.updateEditorTools();
                        expressionMatch[0]
                            .toUpperCase();


                                    this.saveDraft();
                    if (
                                };
                        !knownVariables.has(name)
                    ) {
                        continue;
                    }


                            view.dom.addEventListener(
                    const start =
                                'input',
                        expressionStart +
                                inputHandler
                        expressionMatch.index;
                            );


                            view.dom.classList.add(
                    decorations.push(
                                 'preload-editor-codemirror'
                        Decoration.mark({
                             );
                            class:
                                 'preload-editor-syntax-condition-variable'
                        }).range(
                             start,
                            start +
                                expressionMatch[0]
                                    .length
                        )
                    );
                }
            }


                            const wrapper =
            return Decoration.set(
                                view.dom.closest(
                decorations,
                                    '.ext-codemirror-wrapper'
                true
                                );
            );
        },


                            if (wrapper) {
        async installSyntaxHighlighting(view) {
                                wrapper.classList.add(
            await mw.loader.using(
                                    'preload-editor-codemirror-wrapper'
                'ext.CodeMirror.v6.lib'
                                );
            );
                            }


                            this.editor = {
            const require =
                                cm,
                mw.loader.require;
                                view,
                                textarea,
                                inputHandler
                            };


                            this.editorView =
            const {
                                view;
                StateField,
                StateEffect,
                EditorView,
                Decoration
            } = require(
                'ext.CodeMirror.v6.lib'
            );


                            this.state.preloadContent =
            const syntaxField =
                                view.state.doc.toString();
                StateField.define({
                    create: state =>
                        this.createSyntaxDecorations(
                            state,
                            Decoration
                        ),


                             try {
                    update: (
                                await this.installSyntaxHighlighting(
                        decorations,
                                    view
                        transaction
                                 );
                    ) =>
                        transaction.docChanged
                             ? this.createSyntaxDecorations(
                                transaction.state,
                                 Decoration
                            )
                            : decorations,


                                if (
                    provide: field =>
                                    this.state.dirty
                        EditorView.decorations.from(
                                ) {
                            field
                                    this.saveDraft();
                        )
                                }
                });


                                resolve(view);
            view.dispatch({
                            } catch (error) {
                effects:
                                reject(error);
                    StateEffect.appendConfig.of(
                            }
                        syntaxField
                        };
                    )
            });


                    const onLoad =
            this.editor.syntaxField =
                        loader => {
                syntaxField;
                            if (
        },
                                settled ||
                                typeof loader !==
                                    'function'
                            ) {
                                return;
                            }


                            try {
        async mountCodeMirror(textarea) {
                                loader(textarea);
            return new Promise(
                            } catch (error) {
                (
                                settled = true;
                    resolve,
                                cleanup();
                    reject
                                 reject(error);
                ) => {
                             }
                    let settled = false;
 
                    const cleanup =
                        () => {
                            mw.hook(
                                 'dev.CCM.ready'
                            ).remove(onReady);
 
                             mw.hook(
                                'dev.CCM.load'
                            ).remove(onLoad);
                         };
                         };


                     mw.hook(
                     const onReady =
                        'dev.CCM.ready'
                        async cm => {
                    ).add(onReady);
                            if (
                                settled ||
                                !cm ||
                                cm.textarea !== textarea ||
                                !cm.view
                            ) {
                                return;
                            }


                    mw.hook(
                            settled = true;
                        'dev.CCM.load'
                            cleanup();
                    ).add(onLoad);
                }
            );
        },


        // Main UI
                            const view =
                                cm.view;


        init() {
                            const inputHandler =
            const root =
                                () => {
                $('#mw-content-text');
                                    this.state
                                        .preloadContent =
                                        view.state.doc.toString();


            if (!root.length) {
                                    this.state.preloadDirty =
                return;
                                        true;
            }


            this.root =
                                    this.updateDirtyState();
                $('<div>')
                    .attr(
                        'id',
                        'preload-editor'
                    )
                    .addClass(
                        'preload-editor'
                    );


            root.append(this.root);
                                    this.reanalyze();


            this.renderShell();
                                    this.renderVariablesBySelector();
                                    this.renderVariableConfigurationBySelector();
                                    this.renderAnalysisBySelector();
                                    this.updateEditorTools();


            this.restoreDraft();
                                    this.saveDraft();
                                };


            window.addEventListener(
                            view.dom.addEventListener(
                'beforeunload',
                                'input',
                () => {
                                inputHandler
                    if (this.state.dirty) {
                            );
                        this.saveDraft();
                    }
                }
            );
        },


        renderShell() {
                            view.dom.classList.add(
            this.destroyEditor();
                                'preload-editor-codemirror'
            this.root.empty();
                            );


            const loader =
                            const wrapper =
                this.createSection('Preload');
                                view.dom.closest(
                                    '.ext-codemirror-wrapper'
                                );


            const titleInput =
                            if (wrapper) {
                this.textInput(
                                wrapper.classList.add(
                    this.state.preloadTitle,
                                    'preload-editor-codemirror-wrapper'
                    'Template:Example/Preload'
                                );
                ).attr(
                            }
                    'id',
                    'preload-editor-title-input'
                );


            titleInput.on(
                            this.editor = {
                'input.preloadEditor',
                                cm,
                () => {
                                view,
                    this.state.preloadTitle =
                                textarea,
                        String(
                                inputHandler
                             titleInput.val() || ''
                             };
                        ).trim();


                    if (
                            this.editorView =
                        this.state.dirty &&
                                view;
                        this.state.mode === 'new'
                    ) {
                        this.saveDraft();
                    }
                }
            );


            const loadButton =
                            this.state.preloadContent =
                this.createButton(
                                view.state.doc.toString();
                    'Load',
                    'preload-editor-button-primary',
                    () =>
                        this.loadExisting(
                            titleInput.val()
                        )
                );


            const newButton =
                            try {
                this.createButton(
                                await this.installSyntaxHighlighting(
                    'New',
                                    view
                    '',
                                );
                    () => this.startNew()
                );


            const sampleButton =
                                if (
                this.createButton(
                                    this.state.dirty
                    'Load Sample Page',
                                ) {
                    '',
                                    this.saveDraft();
                    () => this.loadSamplePage()
                                }
                );


            loader.append(
                                resolve(view);
                $('<div>')
                            } catch (error) {
                    .addClass(
                                reject(error);
                        'preload-editor-toolbar'
                            }
                    )
                         };
                    .append(
                        titleInput,
                         loadButton,
                        newButton,
                        sampleButton
                    )
            );


            this.root.append(loader);
                    const onLoad =
                        loader => {
                            if (
                                settled ||
                                typeof loader !==
                                    'function'
                            ) {
                                return;
                            }


            this.status =
                            try {
                $('<div>')
                                loader(textarea);
                    .addClass(
                            } catch (error) {
                         'preload-editor-status'
                                settled = true;
                    );
                                cleanup();
                                reject(error);
                            }
                         };


            this.root.append(this.status);
                    mw.hook(
                        'dev.CCM.ready'
                    ).add(onReady);


            this.workspace =
                     mw.hook(
                $('<div>')
                         'dev.CCM.load'
                     .addClass(
                     ).add(onLoad);
                         'preload-editor-workspace'
                }
                     );
             );
 
            this.root.append(this.workspace);
 
             this.renderWelcome();
         },
         },


         renderWelcome() {
         // ------------------------------------------------------------
            this.workspace.empty();
        // Main UI
        // ------------------------------------------------------------


             const section =
        init() {
                 this.createSection('Get started');
             const root =
                 $('#mw-content-text');


             $('<p>')
             if (!root.length) {
                .text(
                 return;
                    'Load an existing preload, create a new one, or start from a sample wiki page.'
            }
                )
                 .appendTo(section);


             const tips =
             this.root =
                 $('<ul>')
                 $('<div>')
                    .attr(
                        'id',
                        'preload-editor'
                    )
                     .addClass(
                     .addClass(
                         'preload-editor-tips'
                         'preload-editor'
                     );
                     );


             [
             root.append(this.root);
                'Existing preload: loads the preload and its config.json if available.',
 
                'New preload: starts with the standard Preload Template structure.',
            this.applyPageMode();
                'Sample page: loads normal wiki page content so you can turn it into a preload.'
 
             ].forEach(
            this.renderShell();
                 text => {
 
                     $('<li>')
            this.restoreDraft();
                        .text(text)
 
                         .appendTo(tips);
             window.addEventListener(
                 'beforeunload',
                () => {
                     if (this.state.dirty) {
                         this.saveDraft();
                    }
                 }
                 }
             );
             );
            section.append(tips);
            this.workspace.append(section);
         },
         },


         setStatus(
         renderShell() {
            message,
             this.destroyEditor();
            type
             this.root.empty();
        ) {
             if (!this.status) {
                return;
             }


             this.status.removeClass(
             const loader =
                'is-error',
                this.createSection('Preload');
                'is-success',
                'is-warning',
                'is-loading'
            );


             if (type) {
             const titleInput =
                this.status.addClass(
                this.textInput(
                     'is-' + type
                    this.state.preloadTitle,
                    'Template:Example/Preload'
                ).attr(
                     'id',
                    'preload-editor-title-input'
                 );
                 );
            }


             this.status.text(
             titleInput.on(
                message || ''
                'input.preloadEditor',
            );
                () => {
        },
                    this.state.preloadTitle =
                        String(
                            titleInput.val() || ''
                        ).trim();


        // Load / create
                    if (
 
                        this.state.dirty &&
        async loadExisting(title) {
                        this.state.mode === 'new'
            title =
                    ) {
                String(title || '').trim();
                        this.saveDraft();
                    }
                }
            );


             if (!title) {
             const loadButton =
                 mw.notify(
                 this.createButton(
                     'Enter a preload page title first.',
                     'Load',
                     {
                    'preload-editor-button-primary',
                         type: 'warn'
                     () =>
                    }
                         this.loadExisting(
                            titleInput.val()
                        )
                 );
                 );


                 return;
            const newButton =
            }
                 this.createButton(
                    'New',
                    '',
                    () => this.startNew()
                );


             this.setStatus(
             const sampleButton =
                'Loading preload...',
                this.createButton(
                'loading'
                    'Load Sample Page',
            );
                    '',
                    () => this.loadSamplePage()
                );


             try {
             loader.append(
                 const page =
                 $('<div>')
                     await this.getPage(title);
                     .addClass(
 
                        'preload-editor-toolbar'
                if (!page) {
                    )
                     const suggestions =
                     .append(
                         await this.findSimilarPreloads(
                        titleInput,
                            title
                         loadButton,
                        );
                        newButton,
                        sampleButton
                    )
            );
 
            this.root.append(loader);


                    this.renderMissingPage(
            this.status =
                        title,
                $('<div>')
                         suggestions
                    .addClass(
                         'preload-editor-status'
                     );
                     );


                    this.setStatus(
            this.root.append(this.status);
                        'Preload not found.',
 
                         'warning'
            this.workspace =
                $('<div>')
                    .addClass(
                         'preload-editor-workspace'
                     );
                     );


                    return;
            this.root.append(this.workspace);
                }
 
            this.renderWelcome();


                await this.loadDefinition(
            this.updatePageHeight();
                    page.title,
        },
                    page.content
                );


                this.setStatus(
        renderWelcome() {
                    'Preload loaded.',
             this.workspace.empty();
                    'success'
                );
             } catch (error) {
                console.error(
                    'Preload Editor:',
                    error
                );


                 this.setStatus(
            const section =
                    error.message ||
                 this.createSection('Get started');
                        'Could not load preload.',
                    'error'
                );
            }
        },


        async findSimilarPreloads(title) {
            $('<p>')
            let query =
                 .text(
                 title
                    'Load an existing preload, create a new one, or start from a sample wiki page.'
                    .replace(
                )
                        /\/Preload$/i,
                .appendTo(section);
                        ''
                    )
                    .trim();


             if (!query) {
             const tips =
                query = title;
                $('<ul>')
            }
                    .addClass(
                        'preload-editor-tips'
                    );


             try {
             [
                 const results =
                 'Existing preload: loads the preload and its config.json if available.',
                    await this.searchPages(query);
                'New preload: starts with the standard Preload Template structure.',
 
                'Sample page: loads normal wiki page content so you can turn it into a preload.'
                return results.filter(
            ].forEach(
                    result =>
                text => {
                        /\/Preload$/i.test(
                    $('<li>')
                            result.title
                        .text(text)
                         )
                         .appendTo(tips);
                 );
                 }
             } catch (error) {
             );
                console.error(
                    'Preload Editor search:',
                    error
                );


                return [];
            section.append(tips);
             }
             this.workspace.append(section);
         },
         },


         renderMissingPage(
         setStatus(
             title,
             message,
             suggestions
             type
         ) {
         ) {
             this.workspace.empty();
            if (!this.status) {
                return;
            }
 
             this.status.removeClass(
                'is-error',
                'is-success',
                'is-warning',
                'is-loading'
            );


             const section =
             if (type) {
                 this.createSection(
                 this.status.addClass(
                     'Preload not found'
                     'is-' + type
                 );
                 );
            }


             $('<p>')
             this.status.text(
                .text(
                message || ''
                    'No preload exists at "' +
            );
                    title +
        },
                    '".'
                )
                .appendTo(section);


            if (suggestions.length) {
        updateDirtyState() {
                 $('<h3>')
            this.state.dirty =
                     .text('Similar preloads')
                 !!(
                     .appendTo(section);
                     this.state.preloadDirty ||
                     this.state.configDirty
                );
        },


                const list =
        markPreloadDirty() {
                    $('<div>')
            this.state.preloadDirty = true;
                        .addClass(
            this.updateDirtyState();
                            'preload-editor-suggestions'
            this.saveDraft();
                        );
        },


                suggestions.forEach(
        markConfigDirty() {
                    result => {
            this.state.configDirty = true;
                        list.append(
            this.updateDirtyState();
                            this.createButton(
            this.saveDraft();
                                result.title,
        },
                                'preload-editor-suggestion',
                                () =>
                                    this.loadExisting(
                                        result.title
                                    )
                            )
                        );
                    }
                );


                section.append(list);
        markDirty() {
            }
            /*
            * Existing callers generally modify config fields,
            * so markDirty() means the configuration changed.
            */
            this.markConfigDirty();
        },


            section.append(
        // ------------------------------------------------------------
                this.createButton(
        // Load / create
                    'Create this preload',
        // ------------------------------------------------------------
                    'preload-editor-button-primary',
                    () =>
                        this.startNew(title)
                )
            );


            this.workspace.append(section);
         async loadExisting(title) {
        },
 
         async startNew(title) {
             title =
             title =
                 String(title || '').trim();
                 String(title || '').trim();


             this.state = {
             if (!title) {
                 preloadTitle: title,
                 mw.notify(
                preloadSource: '',
                    'Enter a preload page title first.',
                 preloadContent:
                    {
                    this.defaults.preload,
                        type: 'warn'
                    }
                 );


                 config:
                 return;
                    this.emptyConfig(),
             }
 
                variables: ['PAGE'],
                conditions: [],
                selectedVariable: 'PAGE',
                deletedVariables: [],
                mode: 'new',
                dirty: true
             };
 
            this.saveDraft();
            this.renderEditor();


             this.setStatus(
             this.setStatus(
                 'Creating a new preload.',
                 'Loading preload...',
                 'success'
                 'loading'
             );
             );
        },


        async loadSamplePage() {
             try {
            const result =
                await this.createPromptDialog(
                    'Load Sample Page',
                    'Enter the title of a normal wiki page to use as the starting point.'
                );
 
            if (!result) {
                return;
            }
 
            this.setStatus(
                'Loading sample page...',
                'loading'
            );
 
             try {
                 const page =
                 const page =
                     await this.getPage(result);
                     await this.getPage(title);


                 if (!page) {
                 if (!page) {
                     throw new Error(
                     const suggestions =
                         'Sample page not found: ' +
                        await this.findSimilarPreloads(
                         result
                            title
                        );
 
                    this.renderMissingPage(
                         title,
                         suggestions
                     );
                     );
                }


                const sample =
                     this.setStatus(
                     this.convertSampleToPreload(
                         'Preload not found.',
                         page.content
                        'warning'
                     );
                     );


                this.state = {
                     return;
                     preloadTitle: '',
                }
                    preloadSource: '',
                    preloadContent: sample,


                    config:
                await this.loadDefinition(
                        this.emptyConfig(),
                     page.title,
 
                     page.content
                     variables: ['PAGE'],
                 );
                    conditions: [],
                     selectedVariable: 'PAGE',
                    deletedVariables: [],
                    mode: 'new',
                    dirty: true
                };
 
                this.reanalyze();
                this.saveDraft();
                 this.renderEditor();


                 this.setStatus(
                 this.setStatus(
                     'Sample page loaded.',
                     'Preload loaded.',
                     'success'
                     'success'
                 );
                 );
             } catch (error) {
             } catch (error) {
                 console.error(
                 console.error(
                     'Preload Editor sample:',
                     'Preload Editor:',
                     error
                     error
                 );
                 );
Line 2,823: Line 2,883:
                 this.setStatus(
                 this.setStatus(
                     error.message ||
                     error.message ||
                         'Could not load sample page.',
                         'Could not load preload.',
                     'error'
                     'error'
                 );
                 );
Line 2,829: Line 2,889:
         },
         },


         convertSampleToPreload(source) {
         async findSimilarPreloads(title) {
             let content =
             let query =
                 String(source || '').trim();
                 title
 
            const include =
                content.match(
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
                );
 
            if (include) {
                content =
                    include[1].trim();
            }
 
            content =
                content
                     .replace(
                     .replace(
                         /<noinclude>[\s\S]*?<\/noinclude>/gi,
                         /\/Preload$/i,
                         ''
                         ''
                     )
                     )
                     .trim();
                     .trim();


             return (
             if (!query) {
                 '<includeonly>\n' +
                 query = title;
                 content +
            }
                 '\n</includeonly>\n' +
 
                 '<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
            try {
            );
                 const results =
                    await this.searchPages(query);
 
                 return results.filter(
                    result =>
                        /\/Preload$/i.test(
                            result.title
                        )
                );
            } catch (error) {
                 console.error(
                    'Preload Editor search:',
                    error
                );
 
                return [];
            }
         },
         },


         async loadDefinition(
         renderMissingPage(
             title,
             title,
             source
             suggestions
         ) {
         ) {
             const content =
             this.workspace.empty();
                this.extractIncludeonly(source);


             let config =
             const section =
                 this.emptyConfig();
                 this.createSection(
                    'Preload not found'
                );


             const configTitle =
             $('<p>')
                 title.replace(
                 .text(
                     /\/+$/,
                     'No preload exists at "' +
                     ''
                    title +
                 ) +
                     '".'
                 '/config.json';
                 )
                 .appendTo(section);


             const configPage =
             if (suggestions.length) {
                 await this.getPage(configTitle);
                 $('<h3>')
                    .text('Similar preloads')
                    .appendTo(section);


            if (configPage) {
                 const list =
                 try {
                     $('<div>')
                     config =
                         .addClass(
                         JSON.parse(
                             'preload-editor-suggestions'
                             configPage.content
                         );
                         );
                } catch (error) {
                    throw new Error(
                        'The existing config.json contains invalid JSON: ' +
                        error.message
                    );
                }
            }


            const analysis =
                suggestions.forEach(
                this.analyzePreload(content);
                    result => {
                        list.append(
                            this.createButton(
                                result.title,
                                'preload-editor-suggestion',
                                () =>
                                    this.loadExisting(
                                        result.title
                                    )
                            )
                        );
                    }
                );


            this.state.deletedVariables = [];
                section.append(list);
            }


             config =
             section.append(
                 this.prepareConfigForVariables(
                 this.createButton(
                     config,
                     'Create this preload',
                     analysis
                    'preload-editor-button-primary',
                 );
                     () =>
                        this.startNew(title)
                )
            );
 
            this.workspace.append(section);
        },
 
        async startNew(title) {
            title =
                 String(title || '').trim();


             this.state = {
             this.state = {
                 preloadTitle: title,
                 preloadTitle: title,
                 preloadSource: source,
                 preloadSource: '',
                 preloadContent: source,
                 preloadContent:
                config,
                    this.defaults.preload,


                 variables:
                 config:
                     analysis.variables,
                     this.emptyConfig(),


                 conditions:
                variables: ['PAGE'],
                    analysis.conditions,
                 conditions: [],
                selectedVariable: 'PAGE',
                deletedVariables: [],
                mode: 'new',


                 selectedVariable:
                 preloadDirty: true,
                    analysis.variables.includes('PAGE')
                configDirty: true,
                        ? 'PAGE'
                        : (
                            analysis.variables[0] ||
                            'PAGE'
                        ),


                 deletedVariables: [],
                 dirty: true
            };


                mode: 'edit',
            this.saveDraft();
                dirty: false
             this.renderEditor();
             };


             this.clearDraft(title);
             this.setStatus(
             this.renderEditor();
                'Creating a new preload.',
                'success'
             );
         },
         },


         // Editor
         async loadSamplePage() {
            const result =
                await this.createPromptDialog(
                    'Load Sample Page',
                    'Enter the title of a normal wiki page to use as the starting point.'
                );
 
            if (!result) {
                return;
            }


        renderEditor() {
             this.setStatus(
             this.destroyEditor();
                'Loading sample page...',
            this.workspace.empty();
                'loading'
            );


             const layout =
             try {
                $('<div>')
                const page =
                     .addClass(
                     await this.getPage(result);
                        'preload-editor-layout'
                    );


            const left =
                 if (!page) {
                 $('<div>')
                     throw new Error(
                     .addClass(
                         'Sample page not found: ' +
                         'preload-editor-left'
                        result
                     );
                     );
                }


            const right =
                const sample =
                $('<div>')
                     this.convertSampleToPreload(
                     .addClass(
                         page.content
                         'preload-editor-right'
                     );
                     );


            const editorSection =
                 this.state = {
                 this.createSection(
                     preloadTitle: '',
                     'Preload',
                     preloadSource: '',
                     'preload-editor-main-section'
                    preloadContent: sample,
                );


            const meta =
                    config:
                $('<div>')
                        this.emptyConfig(),
                    .addClass(
                        'preload-editor-meta'
                    );


            $('<strong>')
                    variables: ['PAGE'],
                .text(
                     conditions: [],
                     this.state.preloadTitle ||
                     selectedVariable: 'PAGE',
                     'New preload'
                    deletedVariables: [],
                )
                    mode: 'new',
                .appendTo(meta);


            if (
                     preloadDirty: true,
                this.state.mode === 'new'
                     configDirty: true,
            ) {
                $('<span>')
                    .addClass(
                        'preload-editor-badge'
                     )
                     .text('NEW')
                    .appendTo(meta);
            }


            editorSection.append(meta);
                    dirty: true
            editorSection.append(
                 };
                 this.renderEditorTools()
            );


            const textarea =
                 this.reanalyze();
                 this.textarea(
                this.saveDraft();
                    this.state.preloadContent
                this.renderEditor();
                )
                    .attr(
                        'id',
                        'preload-editor-content'
                    )
                    .addClass(
                        'preload-editor-content'
                    );


            const editorMount =
                 this.setStatus(
                 $('<div>')
                    'Sample page loaded.',
                    .addClass(
                    'success'
                        'preload-editor-codemirror-mount'
                );
                     )
            } catch (error) {
                    .append(textarea);
                console.error(
                    'Preload Editor sample:',
                     error
                );


            editorSection.append(editorMount);
                this.setStatus(
             left.append(editorSection);
                    error.message ||
                        'Could not load sample page.',
                    'error'
                );
             }
        },


             const analysisSection =
        convertSampleToPreload(source) {
                 this.createSection('Analysis');
             let content =
                 String(source || '').trim();


             const analysisContainer =
             const include =
                 $('<div>')
                 content.match(
                    .addClass(
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
                        'preload-editor-analysis'
                );
                    );


             analysisSection.append(
             if (include) {
                 analysisContainer
                 content =
            );
                    include[1].trim();
            }


             const variablesSection =
             content =
                 this.createSection('Variables');
                 content
                    .replace(
                        /<noinclude>[\s\S]*?<\/noinclude>/gi,
                        ''
                    )
                    .trim();


             const detectVariablesButton =
             return (
                 this.createButton(
                 '<includeonly>\n' +
                    'Detect variables',
                content +
                    'preload-editor-button-primary preload-editor-detect-variables',
                '\n</includeonly>\n' +
                    () =>
                '<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
                        this.detectVariables()
            );
                );
        },


            const detectHelp =
        async loadDefinition(
                $('<div>')
            title,
                    .addClass(
            source
                        'preload-editor-help preload-editor-detect-variables-help'
        ) {
                    )
            const content =
                    .text(
                this.extractIncludeonly(source);
                        'Refresh the variable list after editing the preload.'
                    );


             variablesSection.append(
             let config =
                 $('<div>')
                 this.emptyConfig();
                    .addClass(
                        'preload-editor-variable-detect'
                    )
                    .append(
                        detectVariablesButton,
                        detectHelp
                    )
            );


             const variableContainer =
             const configTitle =
                 $('<div>')
                 title.replace(
                     .addClass(
                     /\/+$/,
                        'preload-editor-variables'
                    ''
                    );
                ) +
                '/config.json';


             variablesSection.append(
             const configPage =
                variableContainer
                await this.getPage(configTitle);
            );


             const configurationSection =
             if (configPage) {
                 this.createSection(
                try {
                     'Variable configuration'
                    config =
                );
                        JSON.parse(
                            configPage.content
                        );
                 } catch (error) {
                     throw new Error(
                        'The existing config.json contains invalid JSON: ' +
                        error.message
                    );
                }
            }


             const configurationContainer =
             const analysis =
                 $('<div>')
                 this.analyzePreload(content);
                    .addClass(
                        'preload-editor-variable-configuration'
                    );


             configurationSection.append(
             this.state.deletedVariables = [];
                configurationContainer
            );


             const configSection =
             config =
                 this.createSection(
                 this.prepareConfigForVariables(
                     'Generated configuration'
                     config,
                    analysis
                 );
                 );


             const configPreview =
             this.state = {
                 $('<pre>')
                preloadTitle: title,
                    .addClass(
                 preloadSource: source,
                        'preload-editor-config-preview'
                preloadContent: source,
                    );
                config,


            configSection.append(
                 variables:
                 configPreview
                    analysis.variables,
            );


            const actions =
                 conditions:
                 $('<div>')
                     analysis.conditions,
                     .addClass(
                        'preload-editor-actions'
                    );


            actions.append(
                selectedVariable:
                this.createButton(
                    analysis.variables.includes('PAGE')
                    'Save Preload + Config',
                        ? 'PAGE'
                    'preload-editor-button-primary',
                        : (
                    () =>
                            analysis.variables[0] ||
                        this.saveAll()
                            'PAGE'
                ),
                        ),


                 this.createButton(
                 deletedVariables: [],
                    'Save Preload Only',
                    '',
                    () =>
                        this.savePreloadOnly()
                )
            );


            right.append(
                 mode: 'edit',
                 analysisSection,
                variablesSection,
                configurationSection,
                configSection,
                actions
            );


            layout.append(
                 preloadDirty: false,
                 left,
                 configDirty: false,
                 right
            );


             this.workspace.append(layout);
                dirty: false
             };


             this.reanalyze();
             this.clearDraft(title);
            this.renderEditor();
        },


            this.renderVariables(
        // ------------------------------------------------------------
                variableContainer
        // Editor
            );
        // ------------------------------------------------------------


             this.renderAnalysis(
        renderEditor() {
                analysisContainer
             this.destroyEditor();
             );
             this.workspace.empty();


             this.renderVariableConfiguration(
             const layout =
                configurationContainer
                $('<div>')
            );
                    .addClass(
                        'preload-editor-layout'
                    );


             this.renderConfigPreview(
             const left =
                 configPreview
                 $('<div>')
            );
                     .addClass(
 
                         'preload-editor-left'
            this.updateEditorTools();
 
            this.mountCodeMirror(
                textarea.get(0)
            ).catch(
                error => {
                     console.error(
                         'Preload Editor CodeMirror:',
                        error
                     );
                     );


                     this.setStatus(
            const right =
                        error.message ||
                $('<div>')
                            'Could not initialize CodeMirror.',
                     .addClass(
                         'error'
                         'preload-editor-right'
                     );
                     );
                }
            );
        },


        detectVariables() {
             const editorSection =
             const source =
                 this.createSection(
                 this.getEditorContent();
                    'Preload',
                    'preload-editor-main-section'
                );


             this.state.preloadContent =
             const meta =
                 source;
                 $('<div>')
                    .addClass(
                        'preload-editor-meta'
                    );


             this.state.dirty = true;
             $('<strong>')
                .text(
                    this.state.preloadTitle ||
                    'New preload'
                )
                .appendTo(meta);


             this.reanalyze();
             if (
                this.state.mode === 'new'
            ) {
                $('<span>')
                    .addClass(
                        'preload-editor-badge'
                    )
                    .text('NEW')
                    .appendTo(meta);
            }


             this.renderVariablesBySelector();
             editorSection.append(meta);
             this.renderVariableConfigurationBySelector();
             editorSection.append(
            this.renderAnalysisBySelector();
                this.renderEditorTools()
            this.renderConfigPreviewBySelector();
            this.updateEditorTools();
 
            this.saveDraft();
 
            this.setStatus(
                'Variables detected from the current preload.',
                'success'
             );
             );
        },


        reanalyze() {
             const textarea =
             const source =
                 this.textarea(
                 this.getEditorContent();
                    this.state.preloadContent
                )
                    .attr(
                        'id',
                        'preload-editor-content'
                    )
                    .addClass(
                        'preload-editor-content'
                    );


             const content =
             const editorMount =
                 this.extractIncludeonly(source);
                 $('<div>')
                    .addClass(
                        'preload-editor-codemirror-mount'
                    )
                    .append(textarea);


             const analysis =
             editorSection.append(editorMount);
                this.analyzePreload(content);
            left.append(editorSection);


             this.state.preloadContent =
             // Analysis
                source;


             this.state.variables =
             const analysisSection =
                 analysis.variables;
                 this.createSection('Analysis');


             this.state.conditions =
             const analysisContainer =
                 analysis.conditions;
                 $('<div>')
                    .addClass(
                        'preload-editor-analysis'
                    );


             this.state.config =
             analysisSection.append(
                 this.prepareConfigForVariables(
                 analysisContainer
                    this.state.config ||
            );
                        this.emptyConfig(),
                    analysis
                );


             this.analysis =
             // Variables
                analysis;


             const available =
             const variablesSection =
                 ['PAGE'].concat(
                 this.createSection('Variables');
                    analysis.variables.filter(
                        name =>
                            name !== 'PAGE'
                    )
                );


             if (
             const detectVariablesButton =
                 !available.includes(
                 this.createButton(
                     this.state.selectedVariable
                     'Detect variables',
                )
                    'preload-editor-button-primary preload-editor-detect-variables',
            ) {
                    () =>
                this.state.selectedVariable =
                        this.detectVariables()
                    available[0] ||
                );
                    'PAGE';
            }
        },


        renderAnalysis(container) {
            const detectHelp =
            container.empty();
                $('<div>')
                    .addClass(
                        'preload-editor-help preload-editor-detect-variables-help'
                    )
                    .text(
                        'Refresh the variable list after editing the preload.'
                    );


             const analysis =
             variablesSection.append(
                this.analysis ||
                 $('<div>')
                 this.analyzePreload(
                     .addClass(
                     this.extractIncludeonly(
                         'preload-editor-variable-detect'
                         this.getEditorContent()
                    )
                    .append(
                        detectVariablesButton,
                        detectHelp
                     )
                     )
                );
            );


             const variableCount =
             const variableContainer =
                 analysis.variables.length;
                 $('<div>')
 
                    .addClass(
             const conditionCount =
                        'preload-editor-variables'
                 analysis.conditions.length;
                    );
 
             variablesSection.append(
                 variableContainer
            );
 
            // Configuration


             const configuredCount =
             const configurationSection =
                 Object.keys(
                 this.createSection(
                     this.state.config &&
                     'Variable configuration'
                    this.state.config.fields
                 );
                        ? this.state.config.fields
                        : {}
                 ).length;


             const summary =
             const configurationContainer =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-analysis-summary'
                         'preload-editor-variable-configuration'
                     );
                     );


             const createMetric =
             configurationSection.append(
                (
                configurationContainer
                    value,
            );
                    label
                ) =>
                    $('<div>')
                        .addClass(
                            'preload-editor-analysis-metric'
                        )
                        .append(
                            $('<strong>')
                                .addClass(
                                    'preload-editor-analysis-value'
                                )
                                .text(value),


                            $('<span>')
            /*
                                .addClass(
            * --------------------------------------------------------
                                    'preload-editor-analysis-label'
            * Generated configuration
                                )
            * --------------------------------------------------------
                                .text(label)
            *
                        );
            * The giant JSON preview is gone from the main workspace.
            * We now have one compact button which opens a review
            * dialog.
            */
 
            const configSection =
                this.createSection(
                    'Configuration'
                );


             summary.append(
             const configReview =
                 createMetric(
                 $('<div>')
                     variableCount,
                     .addClass(
                    variableCount === 1
                         'preload-editor-config-review-trigger'
                         ? 'variable'
                    );
                        : 'variables'
                ),


                 createMetric(
            $('<p>')
                     conditionCount,
                 .addClass(
                     conditionCount === 1
                     'preload-editor-analysis-note'
                        ? 'condition marker'
                )
                        : 'condition markers'
                .text(
                 ),
                     'Review the generated config.json before saving it.'
                )
                 .appendTo(configReview);


                 createMetric(
            configReview.append(
                     configuredCount,
                 this.createButton(
                     configuredCount === 1
                     'Review generated JSON',
                        ? 'configured field'
                     'preload-editor-button-primary preload-editor-review-config',
                         : 'configured fields'
                    () =>
                         this.createConfigReviewDialog()
                 )
                 )
             );
             );


             container.append(summary);
             configSection.append(
                configReview
            );
 
            // Save actions


             if (
             const actions =
                analysis.conditionVariables.length
                 $('<div>')
            ) {
                    .addClass(
                 container.append(
                        'preload-editor-actions'
                    $('<div>')
                    );
                        .addClass(
                            'preload-editor-analysis-note'
                        )
                        .text(
                            'Condition variables: ' +
                            analysis.conditionVariables.join(', ')
                        )
                );
            }


             const validation =
             actions.append(
                 this.validateConfig(
                 this.createButton(
                     this.state.config,
                     'Save Preload',
                     analysis
                    'preload-editor-button-primary preload-editor-save-preload',
                 );
                     () =>
                        this.savePreloadOnly()
                 ),


            if (validation.errors.length) {
                this.createButton(
                const errors =
                     'Save Config',
                     $('<ul>')
                    'preload-editor-button-primary preload-editor-save-config',
                        .addClass(
                    () =>
                            'preload-editor-validation-errors'
                         this.saveConfigOnly()
                         );
                ),


                 validation.errors.forEach(
                 this.createButton(
                     error => {
                     'Save',
                         $('<li>')
                    'preload-editor-button-primary preload-editor-save-all',
                            .text(error)
                    () =>
                            .appendTo(errors);
                         this.saveAll()
                    }
                )
                 );
            );
 
            right.append(
                analysisSection,
                variablesSection,
                configurationSection,
                configSection,
                 actions
            );


                container.append(errors);
            layout.append(
            }
                left,
                right
            );


             if (validation.warnings.length) {
             this.workspace.append(layout);
                const warnings =
                    $('<ul>')
                        .addClass(
                            'preload-editor-validation-warnings'
                        );


                validation.warnings.forEach(
            this.reanalyze();
                    warning => {
                        $('<li>')
                            .text(warning)
                            .appendTo(warnings);
                    }
                );


                container.append(warnings);
            this.renderVariables(
            }
                variableContainer
            );


             if (
             this.renderAnalysis(
                 !validation.errors.length &&
                 analysisContainer
                !validation.warnings.length
             );
             ) {
 
                container.append(
            this.renderVariableConfiguration(
                    $('<div>')
                configurationContainer
                        .addClass(
            );
                            'preload-editor-validation-success'
                        )
                        .text(
                            'Preload and configuration are in sync.'
                        )
                );
            }
        },


        renderAnalysisBySelector() {
             this.updateEditorTools();
             const container =
                this.root &&
                this.root.find(
                    '.preload-editor-analysis'
                );


             if (
             this.mountCodeMirror(
                 container &&
                 textarea.get(0)
                container.length
             ).catch(
             ) {
                error => {
                this.renderAnalysis(container);
                    console.error(
            }
                        'Preload Editor CodeMirror:',
        },
                        error
                    );


        // Variables
                    this.setStatus(
                        error.message ||
                            'Could not initialize CodeMirror.',
                        'error'
                    );
                }
            );


        renderVariables(container) {
             this.updatePageHeight();
             container.empty();
        },


             const analysis =
        detectVariables() {
                 this.analysis ||
             const source =
                this.analyzePreload(
                 this.getEditorContent();
                    this.extractIncludeonly(
                        this.getEditorContent()
                    )
                );


             const variables =
             this.state.preloadContent =
                 analysis.variables.slice();
                 source;


             const ordered =
             this.markPreloadDirty();
                ['PAGE'];


             variables.forEach(
             this.reanalyze();
                name => {
                    if (
                        name !== 'PAGE' &&
                        !ordered.includes(name)
                    ) {
                        ordered.push(name);
                    }
                }
            );


             const configuredFields =
             this.renderVariablesBySelector();
                this.state.config &&
            this.renderVariableConfigurationBySelector();
                this.state.config.fields
            this.renderAnalysisBySelector();
                    ? Object.keys(
            this.updateEditorTools();
                        this.state.config.fields
                    )
                    : [];


             configuredFields.forEach(
             this.saveDraft();
                name => {
                    const upper =
                        name.toUpperCase();


                    if (
            this.setStatus(
                        upper !== 'PAGE' &&
                'Variables detected from the current preload.',
                        !ordered.includes(upper)
                 'success'
                    ) {
                        ordered.push(upper);
                    }
                 }
             );
             );
        },


             const deleted =
        reanalyze() {
                 Array.isArray(
             const source =
                    this.state.deletedVariables
                 this.getEditorContent();
                )
                    ? this.state.deletedVariables
                    : [];


             deleted.forEach(
             const content =
                name => {
                this.extractIncludeonly(source);
                    const upper =
                        String(name).toUpperCase();


                    if (
            const analysis =
                        upper !== 'PAGE' &&
                this.analyzePreload(content);
                        !ordered.includes(upper)
                    ) {
                        ordered.push(upper);
                    }
                }
            );


             const list =
             this.state.preloadContent =
                 $('<div>')
                 source;
                    .addClass(
                        'preload-editor-variable-list'
                    );


             ordered.forEach(
             this.state.variables =
                 name => {
                 analysis.variables;
                    const isSelected =
                        name ===
                        this.state.selectedVariable;


                    const isUsed =
            this.state.conditions =
                        analysis.variables.includes(
                analysis.conditions;
                            name
                        );


                    const isConfigured =
            const beforeConfig =
                        !!(
                this.state.config
                            this.state.config &&
                    ? JSON.stringify(
                            this.state.config.fields &&
                        this.state.config
                            this.state.config.fields[name]
                    )
                        );
                    : '';


                    const item =
            this.state.config =
                        $('<button>')
                this.prepareConfigForVariables(
                            .attr({
                    this.state.config ||
                                type: 'button',
                        this.emptyConfig(),
                                'data-variable':
                    analysis
                                    name
                );
                            })
                            .addClass(
                                'preload-editor-variable-item'
                            )
                            .toggleClass(
                                'is-selected',
                                isSelected
                            )
                            .on(
                                'click.preloadEditor',
                                () =>
                                    this.selectVariable(
                                        name
                                    )
                            );


                    $('<span>')
            const afterConfig =
                        .addClass(
                JSON.stringify(
                            'preload-editor-variable-name'
                    this.state.config
                        )
                );
                        .text(name)
                        .appendTo(item);


                    if (name === 'PAGE') {
            /*
                        item.append(
            * If analysis automatically created a new field,
                            $('<span>')
            * that is a real config change and should be saved.
                                .addClass(
            */
                                    'preload-editor-badge'
            if (
                                )
                beforeConfig &&
                                .text('AUTOMATIC')
                beforeConfig !== afterConfig
                        );
            ) {
                    } else if (!isConfigured) {
                this.state.configDirty = true;
                        item.append(
                this.updateDirtyState();
                            $('<span>')
            }
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text(
                                    isUsed
                                        ? 'NOT CONFIGURED'
                                        : 'DELETED'
                                )
                        );
                    } else if (!isUsed) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text('UNUSED')
                        );
                    }


                    list.append(item);
            this.analysis =
                 }
                 analysis;
            );


             container.append(list);
             const available =
 
                 ['PAGE'].concat(
            if (!ordered.length) {
                     analysis.variables.filter(
                 container.append(
                         name =>
                     $('<div>')
                             name !== 'PAGE'
                        .addClass(
                    )
                            'preload-editor-empty'
                         )
                        .text(
                             'No variables found in the preload.'
                        )
                );
            }
        },
 
        renderVariablesBySelector() {
            const container =
                this.root &&
                this.root.find(
                    '.preload-editor-variables'
                 );
                 );


             if (
             if (
                 container &&
                 !available.includes(
                 container.length
                    this.state.selectedVariable
                 )
             ) {
             ) {
                 this.renderVariables(container);
                 this.state.selectedVariable =
                    available[0] ||
                    'PAGE';
             }
             }
         },
         },


         selectVariable(name) {
         // ------------------------------------------------------------
            name =
        // Analysis
                String(name || '')
        // ------------------------------------------------------------
                    .trim()
                    .toUpperCase();


            if (!name) {
        renderAnalysis(container) {
                return;
            container.empty();
            }


             const available =
             const analysis =
                 ['PAGE'].concat(
                 this.analysis ||
                    (
                this.analyzePreload(
                        this.analysis &&
                    this.extractIncludeonly(
                        this.analysis.variables
                        this.getEditorContent()
                            ? this.analysis.variables
                            : []
                    ).filter(
                        variable =>
                            variable !== 'PAGE'
                     )
                     )
                 );
                 );


             const configuredUnused =
             const variableCount =
                 this.state.config &&
                 analysis.variables.length;
                this.state.config.fields
                    ? Object.keys(
                        this.state.config.fields
                    )
                        .map(
                            variable =>
                                variable.toUpperCase()
                        )
                    : [];


             configuredUnused.forEach(
             const conditionCount =
                 variable => {
                 analysis.conditions.length;
                    if (
                        !available.includes(variable) &&
                        variable !== 'PAGE'
                    ) {
                        available.push(variable);
                    }
                }
            );


             if (!available.includes(name)) {
             const configuredCount =
                 return;
                Object.keys(
            }
                    this.state.config &&
                    this.state.config.fields
                        ? this.state.config.fields
                        : {}
                 ).length;


             this.state.selectedVariable =
             const summary =
                 name;
                 $('<div>')
                    .addClass(
                        'preload-editor-analysis-summary'
                    );


             this.renderVariablesBySelector();
             const createMetric =
            this.renderVariableConfigurationBySelector();
                (
        },
                    value,
                    label
                ) =>
                    $('<div>')
                        .addClass(
                            'preload-editor-analysis-metric'
                        )
                        .append(
                            $('<strong>')
                                .addClass(
                                    'preload-editor-analysis-value'
                                )
                                .text(value),


        renderVariableConfigurationBySelector() {
                            $('<span>')
            const container =
                                .addClass(
                this.root &&
                                    'preload-editor-analysis-label'
                this.root.find(
                                )
                    '.preload-editor-variable-configuration'
                                .text(label)
                );
                        );


             if (
             summary.append(
                 container &&
                 createMetric(
                container.length
                    variableCount,
            ) {
                    variableCount === 1
                this.renderVariableConfiguration(
                        ? 'variable'
                    container
                        : 'variables'
                 );
                 ),
            }
        },


        renderVariableConfiguration(container) {
                createMetric(
            container.empty();
                    conditionCount,
                    conditionCount === 1
                        ? 'condition marker'
                        : 'condition markers'
                ),


            const analysis =
                 createMetric(
                 this.analysis ||
                    configuredCount,
                this.analyzePreload(
                     configuredCount === 1
                     this.extractIncludeonly(
                        ? 'configured field'
                         this.getEditorContent()
                         : 'configured fields'
                    )
                )
                );
            );


             const name =
             container.append(summary);
                this.state.selectedVariable ||
                'PAGE';


             const fields =
             if (
                this.state.config &&
                 analysis.conditionVariables.length
                 this.state.config.fields
             ) {
                    ? this.state.config.fields
                    : {};
 
            const field =
                fields[name];
 
             if (name === 'PAGE') {
                 container.append(
                 container.append(
                     this.createAutomaticPageCard(
                     $('<div>')
                         field ||
                        .addClass(
                         this.getAutomaticPageField()
                            'preload-editor-analysis-note'
                    )
                         )
                         .text(
                            'Condition variables: ' +
                            analysis.conditionVariables.join(', ')
                        )
                 );
                 );
            }


                 return;
            const validation =
            }
                this.validateConfig(
                    this.state.config,
                    analysis
                 );


             if (!field) {
             if (validation.errors.length) {
                 const missing =
                 const errors =
                     $('<div>')
                     $('<ul>')
                         .addClass(
                         .addClass(
                             'preload-editor-variable preload-editor-variable-missing'
                             'preload-editor-validation-errors'
                         );
                         );


                 $('<div>')
                 validation.errors.forEach(
                    .addClass(
                    error => {
                        'preload-editor-variable-header'
                        $('<li>')
                     )
                            .text(error)
                    .append(
                            .appendTo(errors);
                        $('<h3>')
                     }
                            .text(name),
                );
 
                container.append(errors);
            }


                        $('<span>')
            if (validation.warnings.length) {
                            .addClass(
                const warnings =
                                'preload-editor-badge is-warning'
                    $('<ul>')
                            )
                        .addClass(
                            .text(
                            'preload-editor-validation-warnings'
                                'NOT CONFIGURED'
                        );
                            )
                    )
                    .appendTo(missing);


                 $('<p>')
                 validation.warnings.forEach(
                    .addClass(
                    warning => {
                        'preload-editor-analysis-note'
                        $('<li>')
                    )
                            .text(warning)
                    .text(
                            .appendTo(warnings);
                        'This variable is currently not present in config.json.'
                     }
                     )
                );
                    .appendTo(missing);


                 container.append(missing);
                 container.append(warnings);
                return;
             }
             }


             container.append(
             if (
                this.createVariableCard(
                !validation.errors.length &&
                    name,
                !validation.warnings.length
                    field,
            ) {
                    analysis
                container.append(
                 )
                    $('<div>')
             );
                        .addClass(
                            'preload-editor-validation-success'
                        )
                        .text(
                            'Preload and configuration are in sync.'
                        )
                 );
             }
         },
         },


         createAutomaticPageCard(field) {
         renderAnalysisBySelector() {
             const card =
             const container =
                 $('<div>')
                 this.root &&
                    .addClass(
                this.root.find(
                        'preload-editor-variable preload-editor-variable-automatic'
                    '.preload-editor-analysis'
                    )
                );
                    .attr(
                        'data-variable',
                        'PAGE'
                    );


             const header =
             if (
                 $('<div>')
                container &&
                    .addClass(
                 container.length
                        'preload-editor-variable-header'
            ) {
                    );
                this.renderAnalysis(container);
            }
        },


            header.append(
        // ------------------------------------------------------------
                $('<h3>').text('PAGE'),
        // Variables
        // ------------------------------------------------------------


                $('<span>')
        renderVariables(container) {
                    .addClass(
            container.empty();
                        'preload-editor-badge'
                    )
                    .text('AUTOMATIC')
            );


             card.append(header);
             const analysis =
                this.analysis ||
                this.analyzePreload(
                    this.extractIncludeonly(
                        this.getEditorContent()
                    )
                );


             $('<p>')
             const variables =
                 .addClass(
                 analysis.variables.slice();
                    'preload-editor-analysis-note'
                )
                .text(
                    'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                )
                .appendTo(card);


             const label =
             const ordered =
                 this.textInput(
                 ['PAGE'];
                    field.label ||
                    'Page Name'
                ).prop(
                    'disabled',
                    true
                );


             const description =
             variables.forEach(
                this.textarea(
                 name => {
                    field.description ||
                     if (
                    'Name of the page.'
                         name !== 'PAGE' &&
                 )
                        !ordered.includes(name)
                     .addClass(
                     ) {
                         'preload-editor-small-textarea'
                        ordered.push(name);
                     )
                    }
                    .prop(
                }
                        'disabled',
            );
                        true
                    );


             const type =
             const configuredFields =
                 this.textInput(
                 this.state.config &&
                     field.type || 'page'
                this.state.config.fields
                ).prop(
                     ? Object.keys(
                     'disabled',
                        this.state.config.fields
                     true
                     )
                );
                     : [];


             const required =
             configuredFields.forEach(
                 this.checkbox(
                 name => {
                     field.required !== false
                     const upper =
                ).prop(
                        name.toUpperCase();
                    'disabled',
                    true
                );


            const namespace =
                    if (
                this.textInput(
                        upper !== 'PAGE' &&
                     field.namespace || 'Main'
                        !ordered.includes(upper)
                ).prop(
                     ) {
                     'disabled',
                        ordered.push(upper);
                    true
                     }
                );
                }
            );


             const allowNew =
             const deleted =
                 this.checkbox(
                 Array.isArray(
                     field['allow-new'] === true
                     this.state.deletedVariables
                 ).prop(
                 )
                     'disabled',
                     ? this.state.deletedVariables
                     true
                     : [];
                );


             card.append(
             deleted.forEach(
                 this.fieldRow(
                 name => {
                     'Label',
                     const upper =
                    label
                        String(name).toUpperCase();
                ),


                this.fieldRow(
                    if (
                     'Description',
                        upper !== 'PAGE' &&
                     description
                        !ordered.includes(upper)
                 ),
                     ) {
                        ordered.push(upper);
                     }
                 }
            );


                 this.fieldRow(
            const list =
                    'Type',
                 $('<div>')
                     type
                    .addClass(
                ),
                        'preload-editor-variable-list'
                     );


                this.fieldRow(
            ordered.forEach(
                     'Required',
                name => {
                    required
                     const isSelected =
                ),
                        name ===
                        this.state.selectedVariable;


                this.fieldRow(
                    const isUsed =
                    'Namespace',
                        analysis.variables.includes(
                    namespace
                            name
                ),
                        );


                this.fieldRow(
                    const isConfigured =
                    'Allow new page',
                        !!(
                    allowNew
                            this.state.config &&
                )
                            this.state.config.fields &&
            );
                            this.state.config.fields[name]
                        );


            return card;
                    const item =
        },
                        $('<button>')
                            .attr({
                                type: 'button',
                                'data-variable':
                                    name
                            })
                            .addClass(
                                'preload-editor-variable-item'
                            )
                            .toggleClass(
                                'is-selected',
                                isSelected
                            )
                            .on(
                                'click.preloadEditor',
                                () =>
                                    this.selectVariable(
                                        name
                                    )
                            );


        createVariableCard(
            name,
            field,
            analysis
        ) {
            const card =
                $('<div>')
                    .addClass(
                        'preload-editor-variable'
                    )
                    .attr(
                        'data-variable',
                        name
                    );
            const header =
                $('<div>')
                    .addClass(
                        'preload-editor-variable-header'
                    );
            header.append(
                $('<h3>').text(name)
            );
            if (
                !analysis.variables.includes(name)
            ) {
                header.append(
                     $('<span>')
                     $('<span>')
                         .addClass(
                         .addClass(
                             'preload-editor-badge is-warning'
                             'preload-editor-variable-name'
                         )
                         )
                         .text('UNUSED')
                         .text(name)
                );
                        .appendTo(item);
            }


            card.append(header);
                    if (name === 'PAGE') {
 
                        item.append(
            const label =
                            $('<span>')
                this.textInput(
                                .addClass(
                     field.label || ''
                                    'preload-editor-badge'
                );
                                )
                                .text('AUTOMATIC')
                        );
                    } else if (!isConfigured) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text(
                                    isUsed
                                        ? 'NOT CONFIGURED'
                                        : 'DELETED'
                                )
                        );
                     } else if (!isUsed) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text('UNUSED')
                        );
                    }


            label.on(
                     list.append(item);
                'input.preloadEditor',
                () => {
                    field.label =
                        label.val();
 
                    this.markDirty();
                     this.renderConfigPreviewBySelector();
                 }
                 }
             );
             );


             card.append(
             container.append(list);
                this.fieldRow(
                    'Label',
                    label
                )
            );


             const description =
             if (!ordered.length) {
                 this.textarea(
                 container.append(
                     field.description || ''
                     $('<div>')
                ).addClass(
                        .addClass(
                    'preload-editor-small-textarea'
                            'preload-editor-empty'
                        )
                        .text(
                            'No variables found in the preload.'
                        )
                 );
                 );
            }
        },


             description.on(
        renderVariablesBySelector() {
                 'input.preloadEditor',
             const container =
                 () => {
                 this.root &&
                     field.description =
                 this.root.find(
                        description.val();
                     '.preload-editor-variables'
                );


                    this.markDirty();
            if (
                    this.renderConfigPreviewBySelector();
                container &&
                }
                container.length
            );
            ) {
                this.renderVariables(container);
            }
        },


             card.append(
        selectVariable(name) {
                 this.fieldRow(
             name =
                    'Description',
                 String(name || '')
                     description
                     .trim()
                )
                    .toUpperCase();
            );


             const types = [
             if (!name) {
                 ['text', 'Text'],
                 return;
                ['textarea', 'Textarea'],
             }
                ['number', 'Number'],
                ['boolean', 'Boolean'],
                ['select', 'Select'],
                ['multiselect', 'Multi-select'],
                ['page', 'Page'],
                ['user', 'User'],
                ['date', 'Date']
             ];


             const typeSelect =
             const available =
                 this.select(
                 ['PAGE'].concat(
                     types.map(
                     (
                         ([value, label]) => ({
                        this.analysis &&
                             value,
                         this.analysis.variables
                            label
                            ? this.analysis.variables
                        })
                            : []
                     ),
                    ).filter(
                    field.type || 'text'
                        variable =>
                             variable !== 'PAGE'
                     )
                 );
                 );


             typeSelect.on(
             const configuredUnused =
                 'change.preloadEditor',
                this.state.config &&
                () => {
                 this.state.config.fields
                    field.type =
                    ? Object.keys(
                         typeSelect.val();
                        this.state.config.fields
                    )
                        .map(
                            variable =>
                                variable.toUpperCase()
                         )
                    : [];


                    this.markDirty();
            configuredUnused.forEach(
 
                variable => {
                     this.renderVariableConfigurationBySelector();
                     if (
                     this.renderConfigPreviewBySelector();
                        !available.includes(variable) &&
                        variable !== 'PAGE'
                     ) {
                        available.push(variable);
                    }
                 }
                 }
             );
             );


             card.append(
             if (!available.includes(name)) {
                this.fieldRow(
                 return;
                    'Type',
             }
                    typeSelect
                 )
             );


             const required =
             this.state.selectedVariable =
                this.checkbox(
                 name;
                    field.required === true
                 );


             required.on(
             this.renderVariablesBySelector();
                'change.preloadEditor',
            this.renderVariableConfigurationBySelector();
                () => {
        },
                    field.required =
                        required.prop('checked');


                    this.markDirty();
        renderVariableConfigurationBySelector() {
                     this.renderConfigPreviewBySelector();
            const container =
                 }
                this.root &&
            );
                this.root.find(
                     '.preload-editor-variable-configuration'
                 );


             card.append(
             if (
                 this.fieldRow(
                container &&
                     'Required',
                container.length
                    required
            ) {
                )
                 this.renderVariableConfiguration(
             );
                     container
                );
            }
        },
 
        renderVariableConfiguration(container) {
             container.empty();


             const placeholder =
             const analysis =
                 this.textInput(
                 this.analysis ||
                     field.placeholder || ''
                this.analyzePreload(
                     this.extractIncludeonly(
                        this.getEditorContent()
                    )
                 );
                 );


             placeholder.on(
             const name =
                 'input.preloadEditor',
                this.state.selectedVariable ||
                () => {
                 'PAGE';
                    const value =
                        placeholder.val();


                    if (value) {
            const fields =
                        field.placeholder =
                this.state.config &&
                            value;
                this.state.config.fields
                     } else {
                     ? this.state.config.fields
                        delete field.placeholder;
                     : {};
                     }


                    this.markDirty();
            const field =
                    this.renderConfigPreviewBySelector();
                 fields[name];
                 }
            );


             card.append(
             if (name === 'PAGE') {
                this.fieldRow(
                 container.append(
                    'Placeholder',
                     this.createAutomaticPageCard(
                    placeholder
                         field ||
                )
                         this.getAutomaticPageField()
            );
                    )
 
            const defaultInput =
                 this.textInput(
                     field.default === undefined
                         ? ''
                         : field.default
                 );
                 );


            defaultInput.on(
                 return;
                 'input.preloadEditor',
            }
                () => {
                    const value =
                        defaultInput.val();


                    if (value) {
            if (!field) {
                         field.default =
                const missing =
                             value;
                    $('<div>')
                    } else {
                         .addClass(
                         delete field.default;
                             'preload-editor-variable preload-editor-variable-missing'
                    }
                         );


                     this.markDirty();
                $('<div>')
                     this.renderConfigPreviewBySelector();
                     .addClass(
                }
                        'preload-editor-variable-header'
            );
                    )
                     .append(
                        $('<h3>')
                            .text(name),


            card.append(
                        $('<span>')
                 this.fieldRow(
                            .addClass(
                    'Default',
                                'preload-editor-badge is-warning'
                     defaultInput,
                            )
                     'For text-like fields this is used as a placeholder. For select fields it is the empty/default option.'
                            .text(
                                'NOT CONFIGURED'
                            )
                    )
                    .appendTo(missing);
 
                 $('<p>')
                    .addClass(
                        'preload-editor-analysis-note'
                     )
                     .text(
                        'This variable is currently not present in config.json.'
                    )
                    .appendTo(missing);
 
                container.append(missing);
                return;
            }
 
            container.append(
                this.createVariableCard(
                    name,
                    field,
                    analysis
                 )
                 )
             );
             );
        },


            if (
        createAutomaticPageCard(field) {
                field.type === 'textarea'
            const card =
            ) {
                $('<div>')
                const maxRows =
                     .addClass(
                     this.textInput(
                         'preload-editor-variable preload-editor-variable-automatic'
                         field.maxRows || ''
                     )
                     ).attr(
                    .attr(
                         'type',
                         'data-variable',
                         'number'
                         'PAGE'
                     );
                     );


                 maxRows.on(
            const header =
                    'input.preloadEditor',
                 $('<div>')
                     () => {
                     .addClass(
                         const value =
                         'preload-editor-variable-header'
                            parseInt(
                    );
                                maxRows.val(),
                                10
                            );


                        if (
            header.append(
                            Number.isFinite(value) &&
                $('<h3>').text('PAGE'),
                            value > 0
                        ) {
                            field.maxRows =
                                value;
                        } else {
                            delete field.maxRows;
                        }


                        this.markDirty();
                $('<span>')
                        this.renderConfigPreviewBySelector();
                    .addClass(
                     }
                        'preload-editor-badge'
                );
                    )
                     .text('AUTOMATIC')
            );


                card.append(
            card.append(header);
                    this.fieldRow(
                        'Maximum rows',
                        maxRows
                    )
                );
            }


             if (
             $('<p>')
                 field.type === 'select' ||
                 .addClass(
                 field.type === 'multiselect'
                    'preload-editor-analysis-note'
             ) {
                 )
                 this.renderSelectConfiguration(
                .text(
                     card,
                    'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                     field
                )
                .appendTo(card);
 
             const label =
                 this.textInput(
                     field.label ||
                    'Page Name'
                ).prop(
                    'disabled',
                     true
                 );
                 );
            }


            /*
             const description =
            * DELETE VARIABLE
                 this.textarea(
            *
                    field.description ||
            * This removes the field from the generated
                     'Name of the page.'
            * config.json, but does not remove %%VARIABLE%%
                )
            * from the preload itself.
                     .addClass(
            */
                        'preload-editor-small-textarea'
             const deleteButton =
                     )
                 this.createButton(
                    .prop(
                     'Delete variable',
                        'disabled',
                     'preload-editor-button-danger preload-editor-delete-variable',
                        true
                     async () => {
                    );
                        const confirmed =
 
                            await this.createConfirmDialog(
            const type =
                                'Delete variable',
                this.textInput(
                                'Delete "' +
                    field.type || 'page'
                                name +
                ).prop(
                                '" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',
                    'disabled',
                                'Delete variable',
                    true
                                'Cancel'
                );
                            );


                        if (!confirmed) {
            const required =
                            return;
                this.checkbox(
                        }
                    field.required !== false
                ).prop(
                    'disabled',
                    true
                );


                        this.deleteVariable(name);
            const namespace =
                     }
                this.textInput(
                    field.namespace || 'Main'
                ).prop(
                    'disabled',
                     true
                 );
                 );


             card.append(
             const allowNew =
                $('<div>')
                this.checkbox(
                    .addClass(
                    field['allow-new'] === true
                        'preload-editor-variable-delete'
                ).prop(
                     )
                    'disabled',
                    .append(deleteButton)
                     true
            );
                );


             return card;
             card.append(
        },
                this.fieldRow(
                    'Label',
                    label
                ),


        async deleteVariable(name) {
                this.fieldRow(
            name =
                    'Description',
                String(name || '')
                     description
                     .trim()
                ),
                    .toUpperCase();


            if (
                this.fieldRow(
                !name ||
                    'Type',
                name === 'PAGE'
                    type
            ) {
                 ),
                 return;
            }


            if (
                 this.fieldRow(
                 !this.state.config ||
                    'Required',
                !this.state.config.fields
                    required
            ) {
                 ),
                 return;
            }


            delete this.state.config.fields[name];
                this.fieldRow(
                    'Namespace',
                    namespace
                ),


            if (
                 this.fieldRow(
                 !Array.isArray(
                     'Allow new page',
                     this.state.deletedVariables
                    allowNew
                 )
                 )
             ) {
             );
                 this.state.deletedVariables = [];
 
             }
            return card;
        },
 
        createVariableCard(
            name,
            field,
            analysis
        ) {
            const card =
                 $('<div>')
                    .addClass(
                        'preload-editor-variable'
                    )
                    .attr(
                        'data-variable',
                        name
                    );
 
            const header =
                $('<div>')
                    .addClass(
                        'preload-editor-variable-header'
                    );
 
             header.append(
                $('<h3>').text(name)
            );


             if (
             if (
                 !this.state.deletedVariables.includes(
                 !analysis.variables.includes(name)
                    name
                )
             ) {
             ) {
                 this.state.deletedVariables.push(
                 header.append(
                    name
                    $('<span>')
                        .addClass(
                            'preload-editor-badge is-warning'
                        )
                        .text('UNUSED')
                 );
                 );
             }
             }


             this.state.selectedVariable =
             card.append(header);
                name;


            this.markDirty();
             const label =
 
            this.renderVariablesBySelector();
            this.renderVariableConfigurationBySelector();
            this.renderAnalysisBySelector();
            this.renderConfigPreviewBySelector();
 
            this.setStatus(
                'Variable "' +
                name +
                '" removed from the configuration.',
                'warning'
            );
        },
 
        renderSelectConfiguration(
            card,
            field
        ) {
             const source =
                 this.textInput(
                 this.textInput(
                     field.source || '',
                     field.label || ''
                    'category:Example'
                 );
                 );


             source.on(
             label.on(
                 'input.preloadEditor',
                 'input.preloadEditor',
                 () => {
                 () => {
                     const value =
                     field.label =
                         source.val();
                         label.val();


                    if (value) {
                     this.markConfigDirty();
                        field.source = value;
                     this.renderConfigReviewState();
                    } else {
                        delete field.source;
                    }
 
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                 }
                 }
             );
             );
Line 4,284: Line 4,281:
             card.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Source',
                     'Label',
                     source,
                     label
                    'Currently supported source format: category:Category Name'
                 )
                 )
             );
             );


             const sourcePrefix =
             const description =
                 this.textInput(
                 this.textarea(
                     field['source-prefix'] || ''
                     field.description || ''
                ).addClass(
                    'preload-editor-small-textarea'
                 );
                 );


             sourcePrefix.on(
             description.on(
                 'input.preloadEditor',
                 'input.preloadEditor',
                 () => {
                 () => {
                     const value =
                     field.description =
                         sourcePrefix.val();
                         description.val();


                    if (value) {
                     this.markConfigDirty();
                        field['source-prefix'] =
                     this.renderConfigReviewState();
                            value;
                    } else {
                        delete field['source-prefix'];
                    }
 
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                 }
                 }
             );
             );
Line 4,315: Line 4,306:
             card.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Source prefix',
                     'Description',
                     sourcePrefix,
                     description
                    'Only source values beginning with this text are included.'
                 )
                 )
             );
             );


             const excludeCategory =
             const types = [
                 this.textInput(
                 ['text', 'Text'],
                    field.exclude &&
                ['textarea', 'Textarea'],
                    field.exclude.category ||
                ['number', 'Number'],
                    ''
                ['boolean', 'Boolean'],
                 );
                ['select', 'Select'],
                ['multiselect', 'Multi-select'],
                ['page', 'Page'],
                ['user', 'User'],
                 ['date', 'Date']
            ];


             excludeCategory.on(
             const typeSelect =
                'input.preloadEditor',
                this.select(
                () => {
                    types.map(
                    const value =
                        ([value, label]) => ({
                         excludeCategory.val();
                            value,
                            label
                         })
                    ),
                    field.type || 'text'
                );


                    if (value) {
            typeSelect.on(
                        field.exclude =
                'change.preloadEditor',
                            Object.assign(
                () => {
                                {},
                     field.type =
                                field.exclude || {},
                         typeSelect.val();
                                {
                                    category: value
                                }
                            );
                     } else if (
                        field.exclude
                    ) {
                         delete field.exclude.category;


                        if (
                    this.markConfigDirty();
                            !Object.keys(
                                field.exclude
                            ).length
                        ) {
                            delete field.exclude;
                        }
                    }


                     this.markDirty();
                     this.renderVariableConfigurationBySelector();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigReviewState();
                 }
                 }
             );
             );
Line 4,364: Line 4,349:
             card.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Exclude category',
                     'Type',
                     excludeCategory
                     typeSelect
                 )
                 )
             );
             );


             card.append(
             const required =
                 this.createTransformControls(
                 this.checkbox(
                     field,
                     field.required === true
                    'value',
                );
                     'Value transformation'
 
                 ),
            required.on(
                'change.preloadEditor',
                () => {
                     field.required =
                        required.prop('checked');
 
                    this.markConfigDirty();
                    this.renderConfigReviewState();
                 }
            );


                 this.createTransformControls(
            card.append(
                    field,
                 this.fieldRow(
                     'label',
                     'Required',
                     'Label transformation'
                     required
                 )
                 )
             );
             );


             const optionsValue =
             const placeholder =
                 Array.isArray(field.options)
                 this.textInput(
                    ? field.options
                    field.placeholder || ''
                        .map(
                );
                            option => {
 
                                if (
            placeholder.on(
                                    typeof option ===
                'input.preloadEditor',
                                    'string'
                () => {
                                ) {
                    const value =
                                    return option;
                        placeholder.val();
                                }


                                return (
                    if (value) {
                                    String(
                        field.placeholder =
                                        option.value || ''
                            value;
                                    ) +
                    } else {
                                    ' | ' +
                        delete field.placeholder;
                                    String(
                    }
                                        option.label ||
 
                                        option.value ||
                    this.markConfigDirty();
                                        ''
                    this.renderConfigReviewState();
                                    )
                }
                                );
            );
                            }
 
                        )
            card.append(
                        .join('\n')
                this.fieldRow(
                    : '';
                    'Placeholder',
                    placeholder
                )
            );


             const options =
             const defaultInput =
                 this.textarea(
                 this.textInput(
                     optionsValue
                     field.default === undefined
                ).addClass(
                        ? ''
                    'preload-editor-small-textarea'
                        : field.default
                 );
                 );


             options.on(
             defaultInput.on(
                 'input.preloadEditor',
                 'input.preloadEditor',
                 () => {
                 () => {
                     const lines =
                     const value =
                         options
                         defaultInput.val();
                            .val()
 
                            .split('\n')
                     if (value) {
                            .map(
                         field.default =
                                line =>
                            value;
                                    line.trim()
                            )
                            .filter(Boolean);
 
                     if (!lines.length) {
                         delete field.options;
                     } else {
                     } else {
                         field.options =
                         delete field.default;
                            lines.map(
                                line => {
                                    const parts =
                                        line.split(
                                            /\s*\|\s*/
                                        );
 
                                    if (
                                        parts.length ===
                                        1
                                    ) {
                                        return parts[0];
                                    }
 
                                    return {
                                        value:
                                            parts[0],
 
                                        label:
                                            parts
                                                .slice(1)
                                                .join(
                                                    ' | '
                                                )
                                    };
                                }
                            );
                     }
                     }


                     this.markDirty();
                     this.markConfigDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigReviewState();
                 }
                 }
             );
             );
Line 4,471: Line 4,434:
             card.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Manual options',
                     'Default',
                     options,
                     defaultInput,
                     'One per line. Optional label format: value | label. Leave empty when using a source.'
                     'For text-like fields this is used as a placeholder. For select fields it is the empty/default option.'
                 )
                 )
             );
             );
        },


        createTransformControls(
            if (
            field,
                field.type === 'textarea'
             property,
             ) {
            title
                const maxRows =
        ) {
                    this.textInput(
            const wrapper =
                        field.maxRows || ''
                $('<div>')
                     ).attr(
                     .addClass(
                         'type',
                         'preload-editor-transform'
                        'number'
                     );
                     );


            $('<h4>')
                maxRows.on(
                .text(title)
                    'input.preloadEditor',
                .appendTo(wrapper);
                    () => {
                        const value =
                            parseInt(
                                maxRows.val(),
                                10
                            );


            const existing =
                        if (
                field[property] &&
                            Number.isFinite(value) &&
                typeof field[property] === 'object'
                            value > 0
                    ? field[property]
                        ) {
                    : {};
                            field.maxRows =
                                value;
                        } else {
                            delete field.maxRows;
                        }


            const transform =
                        this.markConfigDirty();
                this.select(
                        this.renderConfigReviewState();
                     [
                     }
                        {
                 );
                            value: '',
                            label:
                                'No transformation'
                        },
                        {
                            value:
                                'remove-prefix',
                            label:
                                'Remove prefix'
                        },
                        {
                            value:
                                'remove-suffix',
                            label:
                                'Remove suffix'
                        },
                        {
                            value: 'replace',
                            label:
                                'Replace'
                        }
                    ],
                    existing.transform || ''
                 );


            const prefix =
                 card.append(
                 this.textInput(
                     this.fieldRow(
                     existing.prefix || ''
                        'Maximum rows',
                        maxRows
                    )
                 );
                 );
            }


             const suffix =
             if (
                 this.textInput(
                field.type === 'select' ||
                     existing.suffix || ''
                field.type === 'multiselect'
            ) {
                 this.renderSelectConfiguration(
                     card,
                    field
                 );
                 );
            }


             const from =
            /*
                 this.textInput(
            * Large delete button.
                     existing.from || ''
            *
                );
            * PAGE never reaches this function because PAGE uses
            * createAutomaticPageCard().
            */
             const deleteButton =
                 this.createButton(
                     'Delete variable',
                    'preload-editor-button-danger preload-editor-delete-variable',
                    async () => {
                        const confirmed =
                            await this.createConfirmDialog(
                                'Delete variable',
                                'Delete "' +
                                name +
                                '" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',
                                'Delete variable',
                                'Cancel'
                            );
 
                        if (!confirmed) {
                            return;
                        }


            const to =
                        this.deleteVariable(name);
                this.textInput(
                     }
                     existing.to || ''
                 );
                 );


             const controls =
             card.append(
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-transform-controls'
                         'preload-editor-variable-delete'
                     );
                     )
                    .append(deleteButton)
            );


             const prefixRow =
             return card;
                this.fieldRow(
        },
                    'Prefix',
                    prefix
                );


             const suffixRow =
        async deleteVariable(name) {
                 this.fieldRow(
             name =
                    'Suffix',
                 String(name || '')
                     suffix
                     .trim()
                );
                    .toUpperCase();


             const fromRow =
             if (
                 this.fieldRow(
                 !name ||
                    'From',
                name === 'PAGE'
                    from
            ) {
                 );
                 return;
            }


             const toRow =
             if (
                 this.fieldRow(
                 !this.state.config ||
                    'To',
                !this.state.config.fields
                    to
            ) {
                 );
                 return;
            }


             const renderInputs =
             delete this.state.config.fields[name];
                () => {
                    const type =
                        transform.val();


                     prefixRow.toggle(
            if (
                        type === 'remove-prefix'
                !Array.isArray(
                    );
                     this.state.deletedVariables
                )
            ) {
                this.state.deletedVariables = [];
            }


                     suffixRow.toggle(
            if (
                        type === 'remove-suffix'
                !this.state.deletedVariables.includes(
                    );
                     name
                )
            ) {
                this.state.deletedVariables.push(
                    name
                );
            }


                    fromRow.toggle(
            /*
                        type === 'replace'
            * Keep the deleted variable selected so the user can
                    );
            * immediately see that it is now unconfigured/deleted.
            */
            this.state.selectedVariable =
                name;


                    toRow.toggle(
            this.markConfigDirty();
                        type === 'replace'
                    );
                };


             const update =
             this.renderVariablesBySelector();
                () => {
            this.renderVariableConfigurationBySelector();
                    const type =
            this.renderAnalysisBySelector();
                        transform.val();
            this.renderConfigReviewState();


                    let object = null;
            this.setStatus(
                'Variable "' +
                name +
                '" removed from the configuration.',
                'warning'
            );
        },


                    if (
        renderSelectConfiguration(
                        type === 'remove-prefix' &&
            card,
                        prefix.val()
            field
                    ) {
        ) {
                        object = {
            const source =
                            transform: type,
                this.textInput(
                            prefix:
                     field.source || '',
                                prefix.val()
                     'category:Example'
                        };
                );
                     } else if (
 
                        type === 'remove-suffix' &&
            source.on(
                        suffix.val()
                'input.preloadEditor',
                     ) {
                () => {
                        object = {
                     const value =
                            transform: type,
                         source.val();
                            suffix:
                                suffix.val()
                        };
                    } else if (
                        type === 'replace' &&
                        from.val()
                     ) {
                         object = {
                            transform: type,
                            from:
                                from.val(),
                            to:
                                to.val()
                        };
                    }


                     if (object) {
                     if (value) {
                         field[property] = object;
                         field.source = value;
                     } else {
                     } else {
                         delete field[property];
                         delete field.source;
                     }
                     }


                     this.markDirty();
                     this.markConfigDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigReviewState();
                 };
                 }
            );


             transform.on(
             card.append(
                 'change.preloadEditor',
                this.fieldRow(
                    'Source',
                    source,
                    'Currently supported source format: category:Category Name'
                )
            );
 
            const sourcePrefix =
                this.textInput(
                    field['source-prefix'] || ''
                );
 
            sourcePrefix.on(
                 'input.preloadEditor',
                 () => {
                 () => {
                     update();
                     const value =
                     renderInputs();
                        sourcePrefix.val();
                }
 
            );
                     if (value) {
                        field['source-prefix'] =
                            value;
                    } else {
                        delete field['source-prefix'];
                    }


            [
                    this.markConfigDirty();
                prefix,
                     this.renderConfigReviewState();
                suffix,
                from,
                to
            ].forEach(
                input => {
                     input.on(
                        'input.preloadEditor',
                        update
                    );
                 }
                 }
             );
             );


             controls.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Transformation',
                     'Source prefix',
                     transform
                     sourcePrefix,
                ),
                    'Only source values beginning with this text are included.'
 
                 )
                 prefixRow,
                suffixRow,
                fromRow,
                toRow
             );
             );


             renderInputs();
             const excludeCategory =
                this.textInput(
                    field.exclude &&
                    field.exclude.category ||
                    ''
                );


             wrapper.append(controls);
             excludeCategory.on(
                'input.preloadEditor',
                () => {
                    const value =
                        excludeCategory.val();


            return wrapper;
                    if (value) {
        },
                        field.exclude =
                            Object.assign(
                                {},
                                field.exclude || {},
                                {
                                    category: value
                                }
                            );
                    } else if (
                        field.exclude
                    ) {
                        delete field.exclude.category;


        // Config output
                        if (
                            !Object.keys(
                                field.exclude
                            ).length
                        ) {
                            delete field.exclude;
                        }
                    }


        getCleanConfig() {
                    this.markConfigDirty();
            const config =
                     this.renderConfigReviewState();
                JSON.parse(
                 }
                     JSON.stringify(
            );
                        this.state.config
                    )
                 );


             const fields =
             card.append(
                 config.fields || {};
                 this.fieldRow(
                    'Exclude category',
                    excludeCategory
                )
            );


             const orderedFields = {};
             card.append(
                this.createTransformControls(
                    field,
                    'value',
                    'Value transformation'
                ),


            orderedFields.PAGE =
                 this.createTransformControls(
                 this.getAutomaticPageField();
                    field,
 
                    'label',
            Object.keys(fields)
                     'Label transformation'
                .filter(
                     name =>
                        name.toUpperCase() !== 'PAGE'
                 )
                 )
                .forEach(
            );
                    name => {
                        orderedFields[name] =
                            fields[name];
                    }
                );


             config.fields =
             const optionsValue =
                orderedFields;
                Array.isArray(field.options)
                    ? field.options
                        .map(
                            option => {
                                if (
                                    typeof option ===
                                    'string'
                                ) {
                                    return option;
                                }


            return config;
                                return (
        },
                                    String(
                                        option.value || ''
                                    ) +
                                    ' | ' +
                                    String(
                                        option.label ||
                                        option.value ||
                                        ''
                                    )
                                );
                            }
                        )
                        .join('\n')
                    : '';


        getConfigJSON() {
             const options =
             return JSON.stringify(
                 this.textarea(
                 this.getCleanConfig(),
                    optionsValue
                null,
                 ).addClass(
                 4
                     'preload-editor-small-textarea'
            );
        },
 
        renderConfigPreview(container) {
            if (container) {
                container.text(
                     this.getConfigJSON()
                 );
                 );
            }
        },


        renderConfigPreviewBySelector() {
            options.on(
            const preview =
                'input.preloadEditor',
                this.root &&
                () => {
                this.root.find(
                    const lines =
                    '.preload-editor-config-preview'
                        options
                );
                            .val()
                            .split('\n')
                            .map(
                                line =>
                                    line.trim()
                            )
                            .filter(Boolean);


            if (
                    if (!lines.length) {
                preview &&
                        delete field.options;
                preview.length
                    } else {
            ) {
                        field.options =
                this.renderConfigPreview(
                            lines.map(
                    preview
                                line => {
                );
                                    const parts =
            }
                                        line.split(
                                            /\s*\|\s*/
                                        );


            this.renderAnalysisBySelector();
                                    if (
        },
                                        parts.length ===
                                        1
                                    ) {
                                        return parts[0];
                                    }


        markDirty() {
                                    return {
            this.state.dirty = true;
                                        value:
            this.saveDraft();
                                            parts[0],
        },


        // Save
                                        label:
 
                                            parts
        async savePreloadOnly() {
                                                .slice(1)
            let title =
                                                .join(
                String(
                                                    ' | '
                    this.state.preloadTitle || ''
                                                )
                ).trim();
                                    };
                                }
                            );
                    }


            if (!title) {
                     this.markConfigDirty();
                const result =
                    this.renderConfigReviewState();
                     await this.createPromptDialog(
                        'Preload title',
                        'Enter the full title for the preload.'
                    );
 
                if (!result) {
                    return;
                 }
                 }
            );


                title = result;
            card.append(
                 this.state.preloadTitle =
                 this.fieldRow(
                    title;
                    'Manual options',
            }
                    options,
                    'One per line. Optional label format: value | label. Leave empty when using a source.'
                )
            );
        },


             this.state.preloadContent =
        createTransformControls(
                this.getEditorContent();
             field,
 
            property,
             const validation =
            title
                 this.validateBeforeSave();
        ) {
             const wrapper =
                 $('<div>')
                    .addClass(
                        'preload-editor-transform'
                    );


             if (validation.errors.length) {
             $('<h4>')
                 this.showValidationErrors(
                 .text(title)
                    validation
                 .appendTo(wrapper);
                 );


                 this.saveDraft();
            const existing =
                 return;
                 field[property] &&
            }
                 typeof field[property] === 'object'
                    ? field[property]
                    : {};


             this.setStatus(
             const transform =
                'Saving preload...',
                this.select(
                'loading'
                    [
            );
                        {
 
                            value: '',
            try {
                            label:
                const exists =
                                'No transformation'
                    await this.pageExists(title);
                        },
 
                        {
                await this.savePage(
                            value:
                    title,
                                'remove-prefix',
                    this.state.preloadContent,
                            label:
                    exists
                                'Remove prefix'
                         ? 'Updated preload with Preload Editor'
                        },
                        : 'Created preload with Preload Editor',
                        {
                     !exists
                            value:
                                'remove-suffix',
                            label:
                                'Remove suffix'
                        },
                         {
                            value: 'replace',
                            label:
                                'Replace'
                        }
                    ],
                     existing.transform || ''
                 );
                 );


                 this.state.mode = 'edit';
            const prefix =
                 this.state.dirty = false;
                 this.textInput(
                    existing.prefix || ''
                 );


                 this.clearDraft(title);
            const suffix =
                 this.textInput(
                    existing.suffix || ''
                );


                 this.setStatus(
            const from =
                     'Preload saved successfully.',
                 this.textInput(
                    'success'
                     existing.from || ''
                 );
                 );


                mw.notify(
             const to =
                    'Preload saved.',
                 this.textInput(
                    {
                     existing.to || ''
                        type: 'success'
                    }
                );
             } catch (error) {
                 console.error(
                     'Preload Editor save:',
                    error
                 );
                 );


                 this.saveDraft();
            const controls =
                 $('<div>')
                    .addClass(
                        'preload-editor-transform-controls'
                    );


                 this.setStatus(
            const prefixRow =
                     error.message ||
                 this.fieldRow(
                        'Could not save preload.',
                     'Prefix',
                     'error'
                     prefix
                 );
                 );


                 mw.notify(
            const suffixRow =
                     error.message ||
                 this.fieldRow(
                        'Could not save preload.',
                     'Suffix',
                     {
                     suffix
                        type: 'error'
                    }
                 );
                 );
            }
        },


        async saveAll() {
             const fromRow =
             let title =
                 this.fieldRow(
                 String(
                     'From',
                     this.state.preloadTitle || ''
                    from
                 ).trim();
                 );


             if (!title) {
             const toRow =
                const result =
                this.fieldRow(
                    await this.createPromptDialog(
                    'To',
                        'Preload title',
                    to
                        'Enter the full title for the preload.'
                );
                    );


                 if (!result) {
            const renderInputs =
                     return;
                 () => {
                }
                     const type =
                        transform.val();
 
                    prefixRow.toggle(
                        type === 'remove-prefix'
                    );
 
                    suffixRow.toggle(
                        type === 'remove-suffix'
                    );


                title = result;
                    fromRow.toggle(
                this.state.preloadTitle =
                        type === 'replace'
                     title;
                     );
            }


            this.state.preloadContent =
                    toRow.toggle(
                 this.getEditorContent();
                        type === 'replace'
                    );
                 };


             this.reanalyze();
             const update =
                () => {
                    const type =
                        transform.val();


            const validation =
                    let object = null;
                this.validateBeforeSave();


             if (validation.errors.length) {
                    if (
                 this.showValidationErrors(
                        type === 'remove-prefix' &&
                     validation
                        prefix.val()
                 );
                    ) {
 
                        object = {
                 this.saveDraft();
                            transform: type,
                 return;
                            prefix:
             }
                                prefix.val()
 
                        };
             const configTitle =
                    } else if (
                 title.replace(
                        type === 'remove-suffix' &&
                     /\/+$/,
                        suffix.val()
                     ''
                    ) {
                 ) +
                        object = {
                 '/config.json';
                            transform: type,
 
                            suffix:
             this.setStatus(
                                suffix.val()
                 'Saving preload and config...',
                        };
                 'loading'
                    } else if (
             );
                        type === 'replace' &&
 
                        from.val()
             try {
                    ) {
                 const preloadExists =
                        object = {
                     await this.pageExists(title);
                            transform: type,
                            from:
                                from.val(),
                            to:
                                to.val()
                        };
                    }
 
                    if (object) {
                        field[property] = object;
                    } else {
                        delete field[property];
                    }
 
                    this.markConfigDirty();
                    this.renderConfigReviewState();
                };
 
            transform.on(
                'change.preloadEditor',
                () => {
                    update();
                    renderInputs();
                }
            );
 
            [
                prefix,
                suffix,
                from,
                to
            ].forEach(
                input => {
                    input.on(
                        'input.preloadEditor',
                        update
                    );
                }
            );
 
            controls.append(
                this.fieldRow(
                    'Transformation',
                    transform
                ),
 
                prefixRow,
                suffixRow,
                fromRow,
                toRow
            );
 
            renderInputs();
 
            wrapper.append(controls);
 
            return wrapper;
        },
 
        // ------------------------------------------------------------
        // Config output / review
        // ------------------------------------------------------------
 
        getCleanConfig() {
            const config =
                JSON.parse(
                    JSON.stringify(
                        this.state.config
                    )
                );
 
            const fields =
                config.fields || {};
 
            const orderedFields = {};
 
            orderedFields.PAGE =
                this.getAutomaticPageField();
 
            Object.keys(fields)
                .filter(
                    name =>
                        name.toUpperCase() !== 'PAGE'
                )
                .forEach(
                    name => {
                        orderedFields[name] =
                            fields[name];
                    }
                );
 
            config.fields =
                orderedFields;
 
            return config;
        },
 
        getConfigJSON() {
            return JSON.stringify(
                this.getCleanConfig(),
                null,
                4
            );
        },
 
        /*
        * Kept as a tiny compatibility helper because other parts
        * of the gadget may still call it.
        */
        renderConfigPreview(container) {
            if (container) {
                container.text(
                    this.getConfigJSON()
                );
            }
        },
 
        renderConfigPreviewBySelector() {
            this.renderConfigReviewState();
 
            this.renderAnalysisBySelector();
        },
 
        renderConfigReviewState() {
            /*
            * The actual JSON is no longer rendered permanently.
            * The review dialog always reads getConfigJSON() when
            * opened, so it is automatically up to date.
            */
        },
 
        // ------------------------------------------------------------
        // Save
        // ------------------------------------------------------------
 
        async getSaveTitle() {
            let title =
                String(
                    this.state.preloadTitle || ''
                ).trim();
 
            if (!title) {
                const result =
                    await this.createPromptDialog(
                        'Preload title',
                        'Enter the full title for the preload.'
                    );
 
                if (!result) {
                    return null;
                }
 
                title = result;
 
                this.state.preloadTitle =
                    title;
            }
 
            return title;
        },
 
        validateConfigBeforeSave() {
            const errors = [];
            const warnings = [];
 
            const title =
                String(
                    this.state.preloadTitle || ''
                ).trim();
 
            if (!title) {
                errors.push(
                    'A preload title is required.'
                );
            }
 
            if (
                title &&
                !/\/Preload$/i.test(title)
            ) {
                warnings.push(
                    'The title does not end with "/Preload".'
                );
            }
 
            /*
            * Reanalyze the current editor so config validation
            * always corresponds to what is currently being edited.
            */
            const content =
                this.getEditorContent();
 
            const analysis =
                this.analyzePreload(
                    this.extractIncludeonly(
                        content
                    )
                );
 
            this.state.config =
                this.prepareConfigForVariables(
                    this.state.config ||
                        this.emptyConfig(),
                    analysis
                );
 
            const validation =
                this.validateConfig(
                    this.state.config,
                    analysis
                );
 
            errors.push(
                ...validation.errors
            );
 
            warnings.push(
                ...validation.warnings
            );
 
            return {
                errors,
                warnings,
                valid:
                    errors.length === 0
            };
        },
 
        async savePreloadOnly() {
            const title =
                await this.getSaveTitle();
 
            if (!title) {
                return;
            }
 
            this.state.preloadContent =
                this.getEditorContent();
 
            const validation =
                this.validateBeforeSave();
 
            if (validation.errors.length) {
                this.showValidationErrors(
                    validation
                );
 
                this.saveDraft();
                return;
            }
 
            this.setStatus(
                'Saving preload...',
                'loading'
            );
 
            try {
                const exists =
                    await this.pageExists(title);
 
                await this.savePage(
                    title,
                    this.state.preloadContent,
                    exists
                        ? 'Updated preload with Preload Editor'
                        : 'Created preload with Preload Editor',
                    !exists
                );
 
                this.state.mode = 'edit';
                this.state.preloadSource =
                    this.state.preloadContent;
 
                this.state.preloadDirty =
                    false;
 
                this.updateDirtyState();
 
                if (!this.state.dirty) {
                    this.clearDraft(title);
                } else {
                    this.saveDraft();
                }
 
                this.setStatus(
                    'Preload saved successfully.',
                    'success'
                );
 
                mw.notify(
                    'Preload saved.',
                    {
                        type: 'success'
                    }
                );
            } catch (error) {
                console.error(
                    'Preload Editor save:',
                    error
                );
 
                this.saveDraft();
 
                this.setStatus(
                    error.message ||
                        'Could not save preload.',
                    'error'
                );
 
                mw.notify(
                    error.message ||
                        'Could not save preload.',
                    {
                        type: 'error'
                    }
                );
            }
        },
 
        async saveConfigOnly() {
            const title =
                await this.getSaveTitle();
 
            if (!title) {
                return;
            }
 
            const validation =
                this.validateConfigBeforeSave();
 
            if (validation.errors.length) {
                this.showValidationErrors(
                    validation
                );
 
                this.saveDraft();
                return;
            }
 
            const configTitle =
                title.replace(
                    /\/+$/,
                    ''
                ) +
                '/config.json';
 
            this.setStatus(
                'Saving config...',
                'loading'
            );
 
            try {
                const configExists =
                    await this.pageExists(
                        configTitle
                    );
 
                await this.savePage(
                    configTitle,
                    this.getConfigJSON(),
                    configExists
                        ? 'Updated preload config with Preload Editor'
                        : 'Created preload config with Preload Editor',
                    !configExists
                );
 
                this.state.configDirty =
                    false;
 
                this.updateDirtyState();
 
                if (!this.state.dirty) {
                    this.clearDraft(title);
                } else {
                    this.saveDraft();
                }
 
                this.setStatus(
                    'Config saved successfully.',
                    'success'
                );
 
                mw.notify(
                    'Preload config saved.',
                    {
                        type: 'success'
                    }
                );
            } catch (error) {
                console.error(
                    'Preload Editor config save:',
                    error
                );
 
                this.saveDraft();
 
                this.setStatus(
                    error.message ||
                        'Could not save config.',
                    'error'
                );
 
                mw.notify(
                    error.message ||
                        'Could not save config.',
                    {
                        type: 'error'
                    }
                );
            }
        },
 
        async saveAll() {
            const title =
                await this.getSaveTitle();
 
            if (!title) {
                return;
            }
 
            this.state.preloadContent =
                this.getEditorContent();
 
            this.reanalyze();
 
            const validation =
                this.validateBeforeSave();
 
             if (validation.errors.length) {
                 this.showValidationErrors(
                     validation
                 );
 
                 this.saveDraft();
                 return;
             }
 
             const configTitle =
                 title.replace(
                     /\/+$/,
                     ''
                 ) +
                 '/config.json';
 
             this.setStatus(
                 'Saving preload and config...',
                 'loading'
             );
 
             try {
                 const preloadExists =
                     await this.pageExists(title);
 
                await this.savePage(
                    title,
                    this.state.preloadContent,
                    preloadExists
                        ? 'Updated preload with Preload Editor'
                        : 'Created preload with Preload Editor',
                    !preloadExists
                );
 
                const configExists =
                    await this.pageExists(
                        configTitle
                    );
 
                await this.savePage(
                    configTitle,
                    this.getConfigJSON(),
                    configExists
                        ? 'Updated preload config with Preload Editor'
                        : 'Created preload config with Preload Editor',
                    !configExists
                );
 
                this.state.mode = 'edit';
                this.state.preloadTitle = title;


                 await this.savePage(
                 this.state.preloadSource =
                    title,
                     this.state.preloadContent;
                     this.state.preloadContent,
                    preloadExists
                        ? 'Updated preload with Preload Editor'
                        : 'Created preload with Preload Editor',
                    !preloadExists
                );


                 const configExists =
                 this.state.preloadDirty =
                    await this.pageExists(
                     false;
                        configTitle
                     );


                 await this.savePage(
                 this.state.configDirty =
                    configTitle,
                     false;
                    this.getConfigJSON(),
                     configExists
                        ? 'Updated preload config with Preload Editor'
                        : 'Created preload config with Preload Editor',
                    !configExists
                );


                 this.state.mode = 'edit';
                 this.updateDirtyState();
                this.state.dirty = false;
                this.state.preloadTitle = title;


                 this.clearDraft(title);
                 this.clearDraft(title);