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 15: Line 15:
             deletedVariables: [],
             deletedVariables: [],
             mode: 'idle',
             mode: 'idle',
 
             dirty: false,
             /*
             dirtyPreload: false,
            * Keep preload and config dirty independently.
             dirtyConfig: false
            * This is important because "Save Config" must not
            * accidentally mark an unsaved preload as saved.
            */
            preloadDirty: false,
             configDirty: false,
 
             dirty: false
         },
         },


Line 34: Line 27:
         status: null,
         status: null,
         editorTools: null,
         editorTools: null,
        pageResizeHandler: null,


         draftStorageKey:
         draftStorageKey:
Line 44: Line 36:
         },
         },


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


         getDraftStorage() {
         getDraftStorage() {
Line 210: Line 200:
                     'edit',
                     'edit',


                 preloadDirty:
                 dirty:
                     !!this.state.preloadDirty,
                     !!this.state.dirty,


                 configDirty:
                 dirtyPreload:
                     !!this.state.configDirty,
                     !!this.state.dirtyPreload,


                 dirty: true,
                 dirtyConfig:
                    !!this.state.dirtyConfig,


                 savedAt:
                 savedAt:
Line 423: Line 414:
                 }
                 }
             }
             }
            const hasDirtyFlags =
                typeof draft.dirtyPreload === 'boolean' ||
                typeof draft.dirtyConfig === 'boolean';


             this.state = {
             this.state = {
Line 482: Line 477:
                     'edit',
                     'edit',


                 preloadDirty:
                 dirty:
                     draft.preloadDirty !== undefined
                     hasDirtyFlags
                         ? !!draft.preloadDirty
                         ? !!(
                            draft.dirtyPreload ||
                            draft.dirtyConfig
                        )
                         : true,
                         : true,


                 configDirty:
                 dirtyPreload:
                     draft.configDirty !== undefined
                     hasDirtyFlags
                         ? !!draft.configDirty
                         ? !!draft.dirtyPreload
                         : true,
                         : true,


                 dirty: true
                 dirtyConfig:
                    hasDirtyFlags
                        ? !!draft.dirtyConfig
                        : true
             };
             };


Line 513: Line 514:
         },
         },


        // ------------------------------------------------------------
        // Page / viewport mode
        // ------------------------------------------------------------
applyPageMode() {
    $('body')
        .addClass(
            'preload-editor-page'
        );
    /*
    * Hide the normal MediaWiki page heading.
    * The editor supplies its own internal headings.
    */
    $(
        '#firstHeading, ' +
        '.firstHeading, ' +
        '.mw-body-header'
    ).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 683: Line 604:
         },
         },


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


         extractIncludeonly(source) {
         extractIncludeonly(source) {
Line 798: Line 717:
         },
         },


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


         getAutomaticPageField() {
         getAutomaticPageField() {
Line 1,102: Line 1,019:
         },
         },


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


         transformationObject(
         transformationObject(
Line 1,148: Line 1,063:
         },
         },


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


         createSection(
         createSection(
Line 1,323: Line 1,236:
         },
         },


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


         createConfirmDialog(
         createConfirmDialog(
Line 1,357: Line 1,268:
                     messageLines.forEach(
                     messageLines.forEach(
                         line => {
                         line => {
                            if (line === '') {
                                $('<div>')
                                    .addClass(
                                        'preload-editor-prompt-spacer'
                                    )
                                    .appendTo(box);
                                return;
                            }
                             $('<p>')
                             $('<p>')
                                 .text(line)
                                 .text(line)
Line 1,399: Line 1,320:
                     $(document.body)
                     $(document.body)
                         .append(overlay);
                         .append(overlay);
                    const close =
                        value => {
                            if (value === 'Escape') {
                                cancel.trigger('click');
                            }
                        };
                    overlay.on(
                        'keydown.preloadEditor',
                        event => {
                            close(event.key);
                        }
                    );


                     overlay.attr(
                     overlay.attr(
Line 1,408: Line 1,343:


                     overlay.on(
                     overlay.on(
                         'keydown.preloadEditor',
                         'click.preloadEditor',
                         event => {
                         event => {
                             if (
                             if (
                                 event.key === 'Escape'
                                 event.target ===
                                overlay.get(0)
                             ) {
                             ) {
                                 cancel.trigger('click');
                                 cancel.trigger('click');
                             }
                             }
 
                         }
                            if (
                                event.key === 'Enter'
                            ) {
                                confirm.trigger('click');
                            }
                        }
                    );
 
                    overlay.on(
                        'click.preloadEditor',
                        event => {
                            if (
                                event.target ===
                                overlay.get(0)
                            ) {
                                cancel.trigger('click');
                            }
                         }
                     );
                     );
                 }
                 }
Line 1,546: Line 1,464:
                         $('<div>')
                         $('<div>')
                             .addClass(
                             .addClass(
                                 'preload-editor-prompt-overlay preload-editor-config-review-overlay'
                                 'preload-editor-prompt-overlay preload-editor-code-review-overlay'
                             );
                             );


Line 1,552: Line 1,470:
                         $('<div>')
                         $('<div>')
                             .addClass(
                             .addClass(
                                 'preload-editor-prompt preload-editor-config-review'
                                 'preload-editor-prompt preload-editor-code-review'
                             );
                             );


                     $('<h2>')
                     $('<h2>')
                         .text(
                         .text('Review config.json')
                            'Generated configuration'
                        )
                         .appendTo(box);
                         .appendTo(box);


                     $('<p>')
                     $('<p>')
                         .text(
                         .text(
                             'This is the JSON that will be uploaded to the preload\'s config.json page.'
                             'This is the config.json that will be saved for the current preload.'
                         )
                         )
                         .appendTo(box);
                         .appendTo(box);


                     const preview =
                     const code =
                         $('<pre>')
                         $('<pre>')
                             .addClass(
                             .addClass(
                                 'preload-editor-config-review-preview'
                                 'preload-editor-config-review'
                             )
                             )
                             .text(
                             .text(
Line 1,576: Line 1,492:
                             );
                             );


                     box.append(preview);
                    const codeWrap =
                        $('<div>')
                            .addClass(
                                'preload-editor-config-review-wrap'
                            )
                            .append(code);
 
                     box.append(codeWrap);


                     const buttons =
                     const buttons =
Line 1,634: Line 1,557:
         },
         },


         // ------------------------------------------------------------
         async confirmSave(
         // CodeMirror
            mode,
        // ------------------------------------------------------------
            title
         ) {
            const configTitle =
                title.replace(
                    /\/+$/,
                    ''
                ) +
                '/config.json';
 
            let saveDescription;


        destroyEditor() {
            if (mode === 'preload') {
             if (!this.editor) {
                saveDescription =
                 this.editorView = null;
                    'Save the preload page only?';
                 this.editorTools = null;
             } else if (mode === 'config') {
                return;
                 saveDescription =
                    'Save the config.json page only?';
            } else {
                 saveDescription =
                    'Save both the preload and config.json?';
             }
             }


             const {
             let pages = [];
                view,
                inputHandler
            } = this.editor;


             if (
             if (
                 view &&
                mode === 'preload' ||
                 inputHandler
                mode === 'both'
            ) {
                pages.push(
                    'Preload: ' + title
                );
            }
 
            if (
                mode === 'config' ||
                mode === 'both'
            ) {
                pages.push(
                    'Config: ' + configTitle
                );
            }
 
            const message =
                saveDescription +
                '\n\n' +
                pages.join('\n') +
                '\n\nThis will write the changes to the wiki.';
 
            return this.createConfirmDialog(
                'Confirm save',
                message,
                'Save',
                'Cancel'
            );
        },
 
        // CodeMirror
 
        destroyEditor() {
            if (!this.editor) {
                this.editorView = null;
                this.editorTools = null;
                return;
            }
 
            const {
                view,
                inputHandler
            } = this.editor;
 
            if (
                 view &&
                 inputHandler
             ) {
             ) {
                 view.dom.removeEventListener(
                 view.dom.removeEventListener(
Line 1,792: Line 1,771:
         },
         },


        // ------------------------------------------------------------
         // Editor tools
         // Editor tools
        // ------------------------------------------------------------


         wrapSelectionWithVariable() {
         wrapSelectionWithVariable() {
Line 2,229: Line 2,206:
         },
         },


        // ------------------------------------------------------------
         // Syntax highlighting
         // Syntax highlighting
        // ------------------------------------------------------------


         getKnownVariables() {
         getKnownVariables() {
Line 2,504: Line 2,479:
                                         view.state.doc.toString();
                                         view.state.doc.toString();


                                     this.state.preloadDirty =
                                     this.markPreloadDirty();
                                        true;
 
                                    this.updateDirtyState();


                                     this.reanalyze();
                                     this.reanalyze();
Line 2,599: Line 2,571:
         },
         },


        // ------------------------------------------------------------
         // Main UI
         // Main UI
        // ------------------------------------------------------------


         init() {
         init() {
Line 2,622: Line 2,592:


             root.append(this.root);
             root.append(this.root);
            this.applyPageMode();


             this.renderShell();
             this.renderShell();
Line 2,728: Line 2,696:


             this.renderWelcome();
             this.renderWelcome();
            this.updatePageHeight();
         },
         },


Line 2,792: Line 2,758:
         },
         },


         updateDirtyState() {
         // Load / create
            this.state.dirty =
                !!(
                    this.state.preloadDirty ||
                    this.state.configDirty
                );
        },


        markPreloadDirty() {
         async loadExisting(title) {
            this.state.preloadDirty = true;
             title =
            this.updateDirtyState();
                 String(title || '').trim();
            this.saveDraft();
        },
 
        markConfigDirty() {
            this.state.configDirty = true;
            this.updateDirtyState();
            this.saveDraft();
        },
 
        markDirty() {
            /*
            * Existing callers generally modify config fields,
            * so markDirty() means the configuration changed.
            */
            this.markConfigDirty();
        },
 
        // ------------------------------------------------------------
        // Load / create
        // ------------------------------------------------------------
 
         async loadExisting(title) {
             title =
                 String(title || '').trim();


             if (!title) {
             if (!title) {
Line 3,001: Line 2,937:
                 deletedVariables: [],
                 deletedVariables: [],
                 mode: 'new',
                 mode: 'new',
 
                 dirty: true,
                 preloadDirty: true,
                 dirtyPreload: true,
                 configDirty: true,
                 dirtyConfig: true
 
                 dirty: true
             };
             };


Line 3,062: Line 2,996:
                     deletedVariables: [],
                     deletedVariables: [],
                     mode: 'new',
                     mode: 'new',
 
                     dirty: true,
                     preloadDirty: true,
                     dirtyPreload: true,
                     configDirty: true,
                     dirtyConfig: true
 
                     dirty: true
                 };
                 };


Line 3,189: Line 3,121:


                 mode: 'edit',
                 mode: 'edit',
 
                 dirty: false,
                 preloadDirty: false,
                 dirtyPreload: false,
                 configDirty: false,
                 dirtyConfig: false
 
                 dirty: false
             };
             };


Line 3,200: Line 3,130:
         },
         },


        // ------------------------------------------------------------
         // Editor
         // Editor
        // ------------------------------------------------------------


         renderEditor() {
         renderEditor() {
Line 3,282: Line 3,210:
             editorSection.append(editorMount);
             editorSection.append(editorMount);
             left.append(editorSection);
             left.append(editorSection);
            // Analysis


             const analysisSection =
             const analysisSection =
Line 3,297: Line 3,223:
                 analysisContainer
                 analysisContainer
             );
             );
            // Variables


             const variablesSection =
             const variablesSection =
Line 3,340: Line 3,264:
                 variableContainer
                 variableContainer
             );
             );
            // Configuration


             const configurationSection =
             const configurationSection =
Line 3,359: Line 3,281:


             /*
             /*
             * --------------------------------------------------------
             * CONFIG REVIEW
            * Generated configuration
            * --------------------------------------------------------
             *
             *
             * The giant JSON preview is gone from the main workspace.
             * The generated JSON is no longer displayed
             * We now have one compact button which opens a review
            * directly in the page. It can be opened in
            * dialog.
             * the custom review dialog instead.
             */
             */


Line 3,373: Line 3,293:
                 );
                 );


             const configReview =
             const configHelp =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-config-review-trigger'
                         'preload-editor-help'
                    )
                    .text(
                        'Review the exact config.json that will be uploaded to the wiki.'
                     );
                     );


             $('<p>')
             const reviewConfigButton =
                .addClass(
                    'preload-editor-analysis-note'
                )
                .text(
                    'Review the generated config.json before saving it.'
                )
                .appendTo(configReview);
 
            configReview.append(
                 this.createButton(
                 this.createButton(
                     'Review generated JSON',
                     'Review config.json',
                     'preload-editor-button-primary preload-editor-review-config',
                     '',
                     () =>
                     () =>
                         this.createConfigReviewDialog()
                         this.createConfigReviewDialog()
                 )
                 );
            );


             configSection.append(
             configSection.append(
                 configReview
                 configHelp,
                reviewConfigButton
             );
             );


Line 3,409: Line 3,323:
                     );
                     );


             actions.append(
             const savePreloadButton =
                 this.createButton(
                 this.createButton(
                     'Save Preload',
                     'Save Preload',
                     'preload-editor-button-primary preload-editor-save-preload',
                     '',
                     () =>
                     () =>
                         this.savePreloadOnly()
                         this.savePreloadOnly()
                 ),
                 );


            const saveConfigButton =
                 this.createButton(
                 this.createButton(
                     'Save Config',
                     'Save Config',
                     'preload-editor-button-primary preload-editor-save-config',
                     '',
                     () =>
                     () =>
                         this.saveConfigOnly()
                         this.saveConfigOnly()
                 ),
                 );


            const saveBothButton =
                 this.createButton(
                 this.createButton(
                     'Save',
                     'Save',
                     'preload-editor-button-primary preload-editor-save-all',
                     'preload-editor-button-primary',
                     () =>
                     () =>
                         this.saveAll()
                         this.saveAll()
                 )
                 );
 
            actions.append(
                savePreloadButton,
                saveConfigButton,
                saveBothButton
             );
             );


Line 3,479: Line 3,400:
                 }
                 }
             );
             );
            this.updatePageHeight();
         },
         },


Line 3,490: Line 3,409:
                 source;
                 source;


             this.markPreloadDirty();
             this.markDirty();


             this.reanalyze();
             this.reanalyze();
Line 3,525: Line 3,444:
             this.state.conditions =
             this.state.conditions =
                 analysis.conditions;
                 analysis.conditions;
            const beforeConfig =
                this.state.config
                    ? JSON.stringify(
                        this.state.config
                    )
                    : '';


             this.state.config =
             this.state.config =
Line 3,539: Line 3,451:
                     analysis
                     analysis
                 );
                 );
            const afterConfig =
                JSON.stringify(
                    this.state.config
                );
            /*
            * If analysis automatically created a new field,
            * that is a real config change and should be saved.
            */
            if (
                beforeConfig &&
                beforeConfig !== afterConfig
            ) {
                this.state.configDirty = true;
                this.updateDirtyState();
            }


             this.analysis =
             this.analysis =
Line 3,578: Line 3,473:
             }
             }
         },
         },
        // ------------------------------------------------------------
        // Analysis
        // ------------------------------------------------------------


         renderAnalysis(container) {
         renderAnalysis(container) {
Line 3,750: Line 3,641:
         },
         },


        // ------------------------------------------------------------
         // Variables
         // Variables
        // ------------------------------------------------------------


         renderVariables(container) {
         renderVariables(container) {
Line 3,942: Line 3,831:
         },
         },


selectVariable(name) {
        selectVariable(name) {
    name =
            name =
        String(name || '')
                String(name || '')
            .trim()
                    .trim()
            .toUpperCase();
                    .toUpperCase();
 
    if (!name) {
            if (!name) {
        return;
                return;
    }
            }
    const available =
        ['PAGE'].concat(
            (
                this.analysis &&
                this.analysis.variables
                    ? this.analysis.variables
                    : []
            ).filter(
                variable =>
                    variable !== 'PAGE'
            )
        );
    const configuredUnused =
        this.state.config &&
        this.state.config.fields
            ? Object.keys(
                this.state.config.fields
            )
                .map(
                    variable =>
                        variable.toUpperCase()
                )
            : [];
    configuredUnused.forEach(
        variable => {
            if (
                !available.includes(variable) &&
                variable !== 'PAGE'
            ) {
                available.push(variable);
            }
        }
    );
    if (!available.includes(name)) {
        return;
    }
    this.state.selectedVariable =
        name;
    this.renderVariablesBySelector();
    this.renderVariableConfigurationBySelector();
    /*
    * Bring the selected variable configuration back
    * into view, as before.
    */
    const configuration =
        this.root &&
        this.root.find(
            '.preload-editor-variable-configuration'
        );
    if (
        configuration &&
        configuration.length
    ) {
        configuration.get(0).scrollIntoView({
            behavior: 'smooth',
            block: 'start'
        });
    }
},


        renderVariableConfigurationBySelector() {
             const available =
             const container =
                 ['PAGE'].concat(
                 this.root &&
                    (
                this.root.find(
                        this.analysis &&
                     '.preload-editor-variable-configuration'
                        this.analysis.variables
                            ? this.analysis.variables
                            : []
                     ).filter(
                        variable =>
                            variable !== 'PAGE'
                    )
                 );
                 );


             if (
            const configuredUnused =
                 container &&
                this.state.config &&
                 container.length
                this.state.config.fields
             ) {
                    ? Object.keys(
                 this.renderVariableConfiguration(
                        this.state.config.fields
                     container
                    )
                        .map(
                            variable =>
                                variable.toUpperCase()
                        )
                    : [];
 
            configuredUnused.forEach(
                variable => {
                    if (
                        !available.includes(variable) &&
                        variable !== 'PAGE'
                    ) {
                        available.push(variable);
                    }
                }
            );
 
            if (!available.includes(name)) {
                return;
            }
 
            this.state.selectedVariable =
                name;
 
            this.renderVariablesBySelector();
            this.renderVariableConfigurationBySelector();
        },
 
        renderVariableConfigurationBySelector() {
            const container =
                this.root &&
                this.root.find(
                    '.preload-editor-variable-configuration'
                );
 
             if (
                 container &&
                 container.length
             ) {
                 this.renderVariableConfiguration(
                     container
                 );
                 );
             }
             }
Line 4,295: Line 4,164:
                         label.val();
                         label.val();


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,320: Line 4,189:
                         description.val();
                         description.val();


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,361: Line 4,230:
                         typeSelect.val();
                         typeSelect.val();


                     this.markConfigDirty();
                     this.markDirty();


                     this.renderVariableConfigurationBySelector();
                     this.renderVariableConfigurationBySelector();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,386: Line 4,255:
                         required.prop('checked');
                         required.prop('checked');


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,416: Line 4,285:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,448: Line 4,317:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,491: Line 4,360:
                         }
                         }


                         this.markConfigDirty();
                         this.markDirty();
                         this.renderConfigReviewState();
                         this.renderConfigReviewAvailability();
                     }
                     }
                 );
                 );
Line 4,514: Line 4,383:
             }
             }


            /*
            * Large delete button.
            *
            * PAGE never reaches this function because PAGE uses
            * createAutomaticPageCard().
            */
             const deleteButton =
             const deleteButton =
                 this.createButton(
                 this.createButton(
Line 4,594: Line 4,457:
             }
             }


            /*
            * Keep the deleted variable selected so the user can
            * immediately see that it is now unconfigured/deleted.
            */
             this.state.selectedVariable =
             this.state.selectedVariable =
                 name;
                 name;


             this.markConfigDirty();
             this.markDirty();


             this.renderVariablesBySelector();
             this.renderVariablesBySelector();
             this.renderVariableConfigurationBySelector();
             this.renderVariableConfigurationBySelector();
             this.renderAnalysisBySelector();
             this.renderAnalysisBySelector();
             this.renderConfigReviewState();
             this.renderConfigReviewAvailability();


             this.setStatus(
             this.setStatus(
Line 4,638: Line 4,497:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,669: Line 4,528:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,718: Line 4,577:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 4,825: Line 4,684:
                     }
                     }


                     this.markConfigDirty();
                     this.markDirty();
                     this.renderConfigReviewState();
                     this.renderConfigReviewAvailability();
                 }
                 }
             );
             );
Line 5,005: Line 4,864:
                     }
                     }


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


Line 5,050: Line 4,909:
         },
         },


        // ------------------------------------------------------------
         // Config output
         // Config output / review
        // ------------------------------------------------------------


         getCleanConfig() {
         getCleanConfig() {
Line 5,097: Line 4,954:


         /*
         /*
         * Kept as a tiny compatibility helper because other parts
         * Kept as a compatibility helper in case CSS or
         * of the gadget may still call it.
         * another part of the gadget expects the method.
        * There is no longer a permanently rendered
        * config preview.
         */
         */
         renderConfigPreview(container) {
         renderConfigPreview(container) {
Line 5,109: Line 4,968:


         renderConfigPreviewBySelector() {
         renderConfigPreviewBySelector() {
             this.renderConfigReviewState();
             /*
 
            * The old generated configuration <pre> no
            * longer exists in the editor UI.
            *
            * Keep this method so existing calls do not
            * break.
            */
             this.renderAnalysisBySelector();
             this.renderAnalysisBySelector();
         },
         },


         renderConfigReviewState() {
         renderConfigReviewAvailability() {
             /*
             /*
             * The actual JSON is no longer rendered permanently.
             * The config is generated on demand when the
            * The review dialog always reads getConfigJSON() when
             * Review config.json button is pressed.
             * opened, so it is automatically up to date.
             */
             */
            this.renderAnalysisBySelector();
        },
        // Dirty state
        updateDirtyState() {
            this.state.dirty =
                !!(
                    this.state.dirtyPreload ||
                    this.state.dirtyConfig
                );
        },
        markPreloadDirty() {
            this.state.dirtyPreload = true;
            this.updateDirtyState();
            this.saveDraft();
        },
        markConfigDirty() {
            this.state.dirtyConfig = true;
            this.updateDirtyState();
            this.saveDraft();
        },
        markDirty() {
            /*
            * Existing configuration controls call
            * markDirty(), so keep it conservative:
            * both parts are considered locally dirty.
            */
            this.state.dirtyPreload = true;
            this.state.dirtyConfig = true;
            this.updateDirtyState();
            this.saveDraft();
         },
         },


        // ------------------------------------------------------------
         // Save
         // Save
        // ------------------------------------------------------------


         async getSaveTitle() {
         async savePreloadOnly() {
             let title =
             let title =
                 String(
                 String(
Line 5,140: Line 5,036:


                 if (!result) {
                 if (!result) {
                     return null;
                     return;
                 }
                 }


                 title = result;
                 title = result;
                 this.state.preloadTitle =
                 this.state.preloadTitle =
                     title;
                     title;
             }
             }


             return title;
             this.state.preloadContent =
        },
                this.getEditorContent();


        validateConfigBeforeSave() {
             const validation =
             const errors = [];
                this.validateBeforeSave();
            const warnings = [];


             const title =
             if (validation.errors.length) {
                 String(
                 this.showValidationErrors(
                     this.state.preloadTitle || ''
                     validation
                 ).trim();
                 );


            if (!title) {
                 this.saveDraft();
                 errors.push(
                 return;
                    'A preload title is required.'
                 );
             }
             }


             if (
             const confirmed =
                title &&
                 await this.confirmSave(
                 !/\/Preload$/i.test(title)
                    'preload',
            ) {
                     title
                warnings.push(
                     'The title does not end with "/Preload".'
                 );
                 );
            if (!confirmed) {
                return;
             }
             }


             /*
             this.setStatus(
            * Reanalyze the current editor so config validation
                'Saving preload...',
            * always corresponds to what is currently being edited.
                'loading'
            */
             );
             const content =
                this.getEditorContent();


             const analysis =
             try {
                this.analyzePreload(
                const exists =
                     this.extractIncludeonly(
                     await this.pageExists(title);
                        content
                    )
                );


            this.state.config =
                 await this.savePage(
                 this.prepareConfigForVariables(
                    title,
                     this.state.config ||
                     this.state.preloadContent,
                         this.emptyConfig(),
                    exists
                     analysis
                        ? 'Updated preload with Preload Editor'
                         : 'Created preload with Preload Editor',
                     !exists
                 );
                 );


            const validation =
                this.state.mode = 'edit';
                 this.validateConfig(
                 this.state.preloadTitle = title;
                    this.state.config,
                this.state.dirtyPreload = false;
                    analysis
                );


            errors.push(
                this.updateDirtyState();
                ...validation.errors
            );


            warnings.push(
                if (!this.state.dirty) {
                 ...validation.warnings
                    this.clearDraft(title);
            );
                 } else {
                    this.saveDraft();
                }


            return {
                this.setStatus(
                errors,
                    this.state.dirtyConfig
                warnings,
                        ? 'Preload saved. Config still has unsaved changes.'
                valid:
                        : 'Preload saved successfully.',
                     errors.length === 0
                     'success'
            };
                );
        },


        async savePreloadOnly() {
                 mw.notify(
            const title =
                    this.state.dirtyConfig
                 await this.getSaveTitle();
                        ? 'Preload saved. Config still has unsaved changes.'
 
                        : 'Preload saved.',
            if (!title) {
                    {
                return;
                        type: 'success'
            }
                    }
 
                 );
            this.state.preloadContent =
             } catch (error) {
                this.getEditorContent();
                 console.error(
 
                     'Preload Editor save:',
            const validation =
                    error
                 this.validateBeforeSave();
 
             if (validation.errors.length) {
                 this.showValidationErrors(
                     validation
                 );
                 );


                 this.saveDraft();
                 this.saveDraft();
                return;
            }


            this.setStatus(
                this.setStatus(
                'Saving preload...',
                    error.message ||
                'loading'
                        'Could not save preload.',
            );
                    'error'
                );


            try {
                 mw.notify(
                const exists =
                     error.message ||
                    await this.pageExists(title);
                         'Could not save preload.',
 
                    {
                 await this.savePage(
                         type: 'error'
                     title,
                     }
                    this.state.preloadContent,
                    exists
                         ? 'Updated preload with Preload Editor'
                         : 'Created preload with Preload Editor',
                     !exists
                 );
                 );
            }
        },


                 this.state.mode = 'edit';
        async saveConfigOnly() {
                 this.state.preloadSource =
            let title =
                    this.state.preloadContent;
                 String(
                    this.state.preloadTitle || ''
                 ).trim();


                 this.state.preloadDirty =
            if (!title) {
                     false;
                 const result =
                    await this.createPromptDialog(
                        'Preload title',
                        'Enter the full title for the preload.'
                     );


                this.updateDirtyState();
                 if (!result) {
 
                     return;
                 if (!this.state.dirty) {
                    this.clearDraft(title);
                } else {
                     this.saveDraft();
                 }
                 }


                 this.setStatus(
                 title = result;
                    'Preload saved successfully.',
                 this.state.preloadTitle =
                    'success'
                     title;
                );
 
                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() {
             this.state.preloadContent =
             const title =
                 this.getEditorContent();
                 await this.getSaveTitle();


             if (!title) {
             this.reanalyze();
                return;
            }


             const validation =
             const validation =
                 this.validateConfigBeforeSave();
                 this.validateBeforeSave();


             if (validation.errors.length) {
             if (validation.errors.length) {
Line 5,327: Line 5,174:


                 this.saveDraft();
                 this.saveDraft();
                return;
            }
            const confirmed =
                await this.confirmSave(
                    'config',
                    title
                );
            if (!confirmed) {
                 return;
                 return;
             }
             }
Line 5,357: Line 5,214:
                 );
                 );


                 this.state.configDirty =
                 this.state.mode = 'edit';
                    false;
                this.state.preloadTitle = title;
                this.state.dirtyConfig = false;


                 this.updateDirtyState();
                 this.updateDirtyState();
Line 5,369: Line 5,227:


                 this.setStatus(
                 this.setStatus(
                     'Config saved successfully.',
                     this.state.dirtyPreload
                        ? 'Config saved. Preload still has unsaved changes.'
                        : 'Config saved successfully.',
                     'success'
                     'success'
                 );
                 );


                 mw.notify(
                 mw.notify(
                     'Preload config saved.',
                     this.state.dirtyPreload
                     {
                        ? 'Config saved. Preload still has unsaved changes.'
                         type: 'success'
                        : 'Config saved.',
                     }
                     {
                 );
                         type: 'success'
             } catch (error) {
                     }
                 console.error(
                 );
                     'Preload Editor config save:',
             } catch (error) {
                     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() {
            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;
                }
 
                title = result;
                this.state.preloadTitle =
                    title;
            }
 
            this.state.preloadContent =
                this.getEditorContent();
 
            this.reanalyze();
 
            const validation =
                this.validateBeforeSave();
 
            if (validation.errors.length) {
                this.showValidationErrors(
                    validation
                 );
                 );


                 this.saveDraft();
                 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;
                 return;
             }
             }


            this.state.preloadContent =
             const confirmed =
                this.getEditorContent();
                 await this.confirmSave(
 
                    'both',
            this.reanalyze();
                     title
 
             const validation =
                 this.validateBeforeSave();
 
            if (validation.errors.length) {
                this.showValidationErrors(
                     validation
                 );
                 );


                this.saveDraft();
            if (!confirmed) {
                 return;
                 return;
             }
             }
Line 5,468: Line 5,354:


                 this.state.mode = 'edit';
                 this.state.mode = 'edit';
                this.state.dirtyPreload = false;
                this.state.dirtyConfig = false;
                this.state.dirty = false;
                 this.state.preloadTitle = title;
                 this.state.preloadTitle = title;
                this.state.preloadSource =
                    this.state.preloadContent;
                this.state.preloadDirty =
                    false;
                this.state.configDirty =
                    false;
                this.updateDirtyState();


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