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.4.0',
         version: '0.5.0',


         state: {
         state: {
Line 30: Line 30:
         },
         },


        // =========================================================
         // API
         // API
        // =========================================================


         async getPage(title) {
         async getPage(title) {
Line 59: Line 61:
             return {
             return {
                 title: page.title || title,
                 title: page.title || title,
                 content: typeof content === 'string' ? content : ''
                 content:
                    typeof content === 'string'
                        ? content
                        : ''
             };
             };
         },
         },
Line 84: Line 89:
         },
         },


         async savePage(title, content, summary, createOnly) {
         async savePage(
            title,
            content,
            summary,
            createOnly
        ) {
             const params = {
             const params = {
                 action: 'edit',
                 action: 'edit',
                 title,
                 title,
                 text: content,
                 text: content,
                 summary: summary || 'Updated with Preload Editor',
                 summary:
                    summary ||
                    'Updated with Preload Editor',
                 formatversion: 2
                 formatversion: 2
             };
             };


             if (createOnly) params.createonly = true;
             if (createOnly) {
                params.createonly = true;
            }


             return new mw.Api().postWithToken('csrf', params);
             return new mw.Api().postWithToken(
                'csrf',
                params
            );
         },
         },


        // =========================================================
         // Preload parsing
         // Preload parsing
        // =========================================================


         extractIncludeonly(source) {
         extractIncludeonly(source) {
             const match = String(source || '').match(
             const match =
                /<includeonly>([\s\S]*?)<\/includeonly>/i
                String(source || '').match(
            );
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
                );


             return match
             return match
Line 112: Line 132:
         findVariables(content) {
         findVariables(content) {
             const variables = [];
             const variables = [];
             const pattern = /%%([A-Za-z0-9_-]+)%%/g;
             const pattern =
                /%%([A-Za-z0-9_-]+)%%/g;
 
             let match;
             let match;


             while ((match = pattern.exec(content)) !== null) {
             while (
                 const name = match[1].toUpperCase();
                (match = pattern.exec(content)) !==
                null
            ) {
                 const name =
                    match[1].toUpperCase();


                 if (!variables.includes(name)) {
                 if (!variables.includes(name)) {
Line 128: Line 154:
         findConditions(content) {
         findConditions(content) {
             const conditions = [];
             const conditions = [];
             const pattern =
             const pattern =
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
Line 133: Line 160:
             let match;
             let match;


             while ((match = pattern.exec(content)) !== null) {
             while (
                (match = pattern.exec(content)) !==
                null
            ) {
                 conditions.push({
                 conditions.push({
                     type: match[1].toLowerCase(),
                     type:
                     expression: match[2].trim(),
                        match[1].toLowerCase(),
                     index: match.index
                     expression:
                        match[2].trim(),
                     index:
                        match.index
                 });
                 });
             }
             }
Line 147: Line 180:
             const variables = [];
             const variables = [];


             this.findConditions(content).forEach(condition => {
             this.findConditions(content).forEach(
                if (
                condition => {
                    condition.type !== 'if' &&
                    if (
                    condition.type !== 'elseif'
                        condition.type !== 'if' &&
                ) {
                        condition.type !== 'elseif'
                    return;
                    ) {
                }
                        return;
                    }


                const matches =
                    const matches =
                    condition.expression.match(
                        condition.expression.match(
                        /[A-Za-z0-9_-]+/g
                            /[A-Za-z0-9_-]+/g
                    ) || [];
                        ) || [];


                matches.forEach(name => {
                    matches.forEach(name => {
                    name = name.toUpperCase();
                        name =
                            name.toUpperCase();


                    if (!variables.includes(name)) {
                        if (
                        variables.push(name);
                            !variables.includes(
                     }
                                name
                 });
                            )
             });
                        ) {
                            variables.push(
                                name
                            );
                        }
                     });
                 }
             );


             return variables;
             return variables;
Line 174: Line 216:
         analyzePreload(content) {
         analyzePreload(content) {
             return {
             return {
                 variables: this.findVariables(content),
                 variables:
                 conditions: this.findConditions(content),
                    this.findVariables(content),
                 conditionVariables:
 
                     this.findConditionVariables(content)
                 conditions:
                    this.findConditions(content),
 
                 conditionVariables:
                     this.findConditionVariables(
                        content
                    )
             };
             };
         },
         },


        // =========================================================
         // Config
         // Config
        // =========================================================


         getAutomaticPageField() {
         getAutomaticPageField() {
             return {
             return {
                 label: 'Page Name',
                 label: 'Page Name',
                 description: 'Name of the page.',
                 description:
                    'Name of the page.',
                 type: 'page',
                 type: 'page',
                 required: true,
                 required: true,
Line 199: Line 250:
                     incomplete: 'add-stub'
                     incomplete: 'add-stub'
                 },
                 },
                 fields: {
                 fields: {
                     PAGE: this.getAutomaticPageField()
                     PAGE:
                        this.getAutomaticPageField()
                 }
                 }
             };
             };
Line 216: Line 269:
             if (
             if (
                 !config.settings ||
                 !config.settings ||
                 typeof config.settings !== 'object' ||
                 typeof config.settings !==
                    'object' ||
                 Array.isArray(config.settings)
                 Array.isArray(config.settings)
             ) {
             ) {
Line 224: Line 278:
             if (
             if (
                 !config.fields ||
                 !config.fields ||
                 typeof config.fields !== 'object' ||
                 typeof config.fields !==
                    'object' ||
                 Array.isArray(config.fields)
                 Array.isArray(config.fields)
             ) {
             ) {
Line 230: Line 285:
             }
             }


             // PAGE is always a special, automatic field.
             /*
            // Its configuration is precompiled and only displayed
            * PAGE is always a special automatic field.
            // in the editor for reference.
            */
             config.fields.PAGE =
             config.fields.PAGE =
                 this.getAutomaticPageField();
                 this.getAutomaticPageField();
Line 239: Line 294:
         },
         },


         createDefaultField(name, analysis) {
         createDefaultField(
            name,
            analysis
        ) {
             return {
             return {
                 label: this.humanizeName(name),
                 label:
                    this.humanizeName(name),
 
                 description: '',
                 description: '',
                 type:
                 type:
                     analysis.conditionVariables.includes(name)
                     analysis.conditionVariables.includes(
                        name
                    )
                         ? 'boolean'
                         ? 'boolean'
                         : 'text'
                         : 'text'
Line 252: Line 315:
         humanizeName(name) {
         humanizeName(name) {
             return String(name || '')
             return String(name || '')
                 .replace(/[_-]+/g, ' ')
                 .replace(
                 .replace(/([a-z])([A-Z])/g, '$1 $2')
                    /[_-]+/g,
                 .replace(/\s+/g, ' ')
                    ' '
                )
                 .replace(
                    /([a-z])([A-Z])/g,
                    '$1 $2'
                )
                 .replace(
                    /\s+/g,
                    ' '
                )
                 .trim()
                 .trim()
                 .replace(
                 .replace(
                     /\b\w/g,
                     /\b\w/g,
                     char => char.toUpperCase()
                     char =>
                        char.toUpperCase()
                 );
                 );
         },
         },


         prepareConfigForVariables(config, analysis) {
        /*
             config = this.normalizeConfig(config);
        * Makes sure every variable currently used
        * by the preload has a configuration field.
        *
        * This is the important part for newly typed
        * variables such as %%ZHS%%.
        */
         prepareConfigForVariables(
            config,
            analysis
        ) {
             config =
                this.normalizeConfig(
                    config
                );
 
            const existing =
                config.fields || {};


            const existing = config.fields || {};
             const fields = {};
             const fields = {};


             // PAGE is always automatic/precompiled.
             /*
             fields.PAGE = this.getAutomaticPageField();
            * PAGE is always automatic/precompiled.
            */
             fields.PAGE =
                this.getAutomaticPageField();


             analysis.variables.forEach(name => {
            /*
                if (name === 'PAGE') return;
            * Add every variable currently found
            * in the preload.
            *
            * Existing configuration is preserved.
            * New variables receive a default field.
            */
             analysis.variables.forEach(
                name => {
                    if (name === 'PAGE') {
                        return;
                    }


                fields[name] =
                    const existingField =
                     existing[name] ||
                        existing[name];
                    this.createDefaultField(
 
                        name,
                     fields[name] =
                        analysis
                        existingField ||
                    );
                        this.createDefaultField(
             });
                            name,
                            analysis
                        );
                }
             );


             Object.keys(existing).forEach(name => {
            /*
                const upper = name.toUpperCase();
            * Preserve existing fields which are no
            * longer used. These are shown as UNUSED
            * rather than silently deleted.
            */
             Object.keys(existing).forEach(
                name => {
                    const upper =
                        name.toUpperCase();


                if (
                    if (
                    upper !== 'PAGE' &&
                        upper !== 'PAGE' &&
                    !fields[upper]
                        !fields[upper]
                ) {
                    ) {
                    fields[upper] = existing[name];
                        fields[upper] =
                            existing[name];
                    }
                 }
                 }
             });
             );


             config.fields = fields;
             config.fields = fields;
Line 298: Line 412:
         },
         },


         validateConfig(config, analysis) {
         validateConfig(
            config,
            analysis
        ) {
             const errors = [];
             const errors = [];
             const warnings = [];
             const warnings = [];
Line 309: Line 426:
                 return {
                 return {
                     valid: false,
                     valid: false,
                     errors: ['Config must be an object.'],
                     errors: [
                        'Config must be an object.'
                    ],
                     warnings: []
                     warnings: []
                 };
                 };
Line 316: Line 435:
             if (
             if (
                 !config.fields ||
                 !config.fields ||
                 typeof config.fields !== 'object' ||
                 typeof config.fields !==
                    'object' ||
                 Array.isArray(config.fields)
                 Array.isArray(config.fields)
             ) {
             ) {
                 return {
                 return {
                     valid: false,
                     valid: false,
                     errors: ['"fields" must be an object.'],
                     errors: [
                        '"fields" must be an object.'
                    ],
                     warnings
                     warnings
                 };
                 };
Line 327: Line 449:


             const configured =
             const configured =
                 Object.keys(config.fields).map(
                 Object.keys(
                    name => name.toUpperCase()
                    config.fields
                 );
                 ).map(
 
                     name =>
            analysis.variables.forEach(name => {
                         name.toUpperCase()
                if (!configured.includes(name)) {
                     errors.push(
                        'Variable "' +
                        name +
                        '" is not configured.'
                    );
                }
            });
 
            configured.forEach(name => {
                if (!analysis.variables.includes(name)) {
                    warnings.push(
                        'Field "' +
                         name +
                        '" is not used by the preload.'
                    );
                }
            });
 
            if (!configured.includes('PAGE')) {
                errors.push(
                    'PAGE must always be configured.'
                 );
                 );
            }


             Object.entries(config.fields).forEach(
             analysis.variables.forEach(
                 ([name, field]) => {
                 name => {
                     if (
                     if (
                         !field ||
                         !configured.includes(
                        typeof field !== 'object' ||
                            name
                         Array.isArray(field)
                         )
                     ) {
                     ) {
                         errors.push(
                         errors.push(
                             'Field "' +
                            'Variable "' +
                            name +
                            '" is not configured.'
                        );
                    }
                }
            );
 
            configured.forEach(
                name => {
                    if (
                        !analysis.variables.includes(
                            name
                        )
                    ) {
                        warnings.push(
                            'Field "' +
                            name +
                            '" is not used by the preload.'
                        );
                    }
                }
            );
 
            if (
                !configured.includes(
                    'PAGE'
                )
            ) {
                errors.push(
                    'PAGE must always be configured.'
                );
            }
 
            Object.entries(
                config.fields
            ).forEach(
                ([name, field]) => {
                    if (
                        !field ||
                        typeof field !==
                            'object' ||
                        Array.isArray(field)
                    ) {
                        errors.push(
                             'Field "' +
                             name +
                             name +
                             '" must be an object.'
                             '" must be an object.'
                         );
                         );
                         return;
                         return;
                     }
                     }
Line 382: Line 527:
                     if (
                     if (
                         (
                         (
                             field.type === 'select' ||
                             field.type ===
                             field.type === 'multiselect'
                                'select' ||
                             field.type ===
                                'multiselect'
                         ) &&
                         ) &&
                         !field.source &&
                         !field.source &&
                         !Array.isArray(field.options)
                         !Array.isArray(
                            field.options
                        )
                     ) {
                     ) {
                         warnings.push(
                         warnings.push(
Line 400: Line 549:


             return {
             return {
                 valid: !errors.length,
                 valid:
                    !errors.length,
                 errors,
                 errors,
                 warnings
                 warnings
Line 406: Line 556:
         },
         },


        // =========================================================
         // Transformation helpers
         // Transformation helpers
        // =========================================================


         transformationObject(
         transformationObject(
Line 415: Line 567:
             to
             to
         ) {
         ) {
             if (!transform) return null;
             if (!transform) {
                return null;
            }


             switch (transform) {
             switch (transform) {
                 case 'remove-prefix':
                 case 'remove-prefix':
                     return prefix
                     return prefix
                         ? { transform, prefix }
                         ? {
                            transform,
                            prefix
                        }
                         : null;
                         : null;


                 case 'remove-suffix':
                 case 'remove-suffix':
                     return suffix
                     return suffix
                         ? { transform, suffix }
                         ? {
                            transform,
                            suffix
                        }
                         : null;
                         : null;


Line 442: Line 602:
         },
         },


        // =========================================================
         // UI helpers
         // UI helpers
        // =========================================================


         createSection(title, className) {
         createSection(
             const section = $('<section>')
            title,
                .addClass('preload-editor-section');
            className
        ) {
             const section =
                $('<section>')
                    .addClass(
                        'preload-editor-section'
                    );


             if (className) {
             if (className) {
                 section.addClass(className);
                 section.addClass(
                    className
                );
             }
             }


Line 462: Line 632:
         },
         },


         createButton(label, className, callback) {
         createButton(
             const button = $('<button>')
            label,
                .attr('type', 'button')
            className,
                .addClass('preload-editor-button')
            callback
                .text(label);
        ) {
             const button =
                $('<button>')
                    .attr(
                        'type',
                        'button'
                    )
                    .addClass(
                        'preload-editor-button'
                    )
                    .text(label);


             if (className) {
             if (className) {
                 button.addClass(className);
                 button.addClass(
                    className
                );
             }
             }


Line 482: Line 664:
         },
         },


         fieldRow(label, input, description) {
         fieldRow(
             const row = $('<div>')
            label,
                .addClass('preload-editor-field');
            input,
            description
        ) {
             const row =
                $('<div>')
                    .addClass(
                        'preload-editor-field'
                    );


             $('<label>')
             $('<label>')
                 .addClass('preload-editor-label')
                 .addClass(
                    'preload-editor-label'
                )
                 .text(label)
                 .text(label)
                 .appendTo(row);
                 .appendTo(row);
Line 493: Line 684:
             if (description) {
             if (description) {
                 $('<div>')
                 $('<div>')
                     .addClass('preload-editor-help')
                     .addClass(
                        'preload-editor-help'
                    )
                     .text(description)
                     .text(description)
                     .appendTo(row);
                     .appendTo(row);
Line 503: Line 696:
         },
         },


         textInput(value, placeholder) {
         textInput(
            value,
            placeholder
        ) {
             return $('<input>')
             return $('<input>')
                 .attr({
                 .attr({
Line 514: Line 710:
                         placeholder || ''
                         placeholder || ''
                 })
                 })
                 .addClass('preload-editor-input');
                 .addClass(
                    'preload-editor-input'
                );
         },
         },


Line 531: Line 729:
         checkbox(checked) {
         checkbox(checked) {
             return $('<input>')
             return $('<input>')
                 .attr('type', 'checkbox')
                 .attr(
                 .prop('checked', !!checked)
                    'type',
                    'checkbox'
                )
                 .prop(
                    'checked',
                    !!checked
                )
                 .addClass(
                 .addClass(
                     'preload-editor-checkbox'
                     'preload-editor-checkbox'
Line 538: Line 742:
         },
         },


         select(options, value) {
         select(
             const select = $('<select>')
            options,
                .addClass(
            value
                    'preload-editor-select'
        ) {
                );
             const select =
                $('<select>')
                    .addClass(
                        'preload-editor-select'
                    );


             options.forEach(option => {
             options.forEach(
                $('<option>')
                option => {
                    .attr(
                    $('<option>')
                        'value',
                        .attr(
                        option.value
                            'value',
                    )
                            option.value
                    .text(option.label)
                    .prop(
                        'selected',
                        String(option.value) ===
                        String(
                            value === undefined
                                ? ''
                                : value
                         )
                         )
                    )
                        .text(
                    .appendTo(select);
                            option.label
             });
                        )
                        .prop(
                            'selected',
                            String(
                                option.value
                            ) ===
                                String(
                                    value ===
                                        undefined
                                        ? ''
                                        : value
                                )
                        )
                        .appendTo(select);
                }
             );


             return select;
             return select;
         },
         },


        // =========================================================
         // CodeMirror
         // CodeMirror
        // =========================================================


         destroyEditor() {
         destroyEditor() {
Line 580: Line 797:
             } = this.editor;
             } = this.editor;


             if (view && inputHandler) {
             if (
                view &&
                inputHandler
            ) {
                 view.dom.removeEventListener(
                 view.dom.removeEventListener(
                     'input',
                     'input',
Line 608: Line 828:
             }
             }


             return this.state.preloadContent || '';
             return (
                this.state.preloadContent ||
                ''
            );
         },
         },


         setEditorContent(content) {
         setEditorContent(content) {
             content = String(content || '');
             content =
                String(content || '');


             if (!this.editorView) {
             if (!this.editorView) {
                 this.state.preloadContent = content;
                 this.state.preloadContent =
                    content;
 
                 return;
                 return;
             }
             }
Line 622: Line 848:
                 this.editorView.state.doc.toString();
                 this.editorView.state.doc.toString();


             if (current === content) return;
             if (current === content) {
                return;
            }


             this.editorView.dispatch({
             this.editorView.dispatch({
                 changes: {
                 changes: {
                     from: 0,
                     from: 0,
                     to: this.editorView.state.doc.length,
                     to:
                        this.editorView.state
                            .doc.length,
                     insert: content
                     insert: content
                 }
                 }
Line 642: Line 872:


             const selection =
             const selection =
                 this.editorView.state.selection.main;
                 this.editorView.state
                    .selection.main;


             return {
             return {
Line 648: Line 879:
                 to: selection.to,
                 to: selection.to,
                 text:
                 text:
                     this.editorView.state.doc.sliceString(
                     this.editorView.state.doc
                        selection.from,
                        .sliceString(
                        selection.to
                            selection.from,
                    )
                            selection.to
                        )
             };
             };
         },
         },
Line 667: Line 899:
             cursorPosition
             cursorPosition
         ) {
         ) {
             if (!this.editorView) return;
             if (!this.editorView) {
                return;
            }
 
            const view =
                this.editorView;


            const view = this.editorView;
             const selection =
             const selection =
                 view.state.selection.main;
                 view.state.selection.main;
Line 684: Line 920:


             const cursor =
             const cursor =
                 cursorPosition === undefined
                 cursorPosition ===
                    undefined
                     ? from + text.length
                     ? from + text.length
                     : cursorPosition;
                     : cursorPosition;
Line 694: Line 931:
                     insert: text
                     insert: text
                 },
                 },
                 selection: {
                 selection: {
                     anchor: cursor
                     anchor: cursor
Line 702: Line 940:
         },
         },


         // V2.3 editor tools
         // =========================================================
        // Editor tools
        // =========================================================


         wrapSelectionWithVariable() {
         wrapSelectionWithVariable() {
Line 710: Line 950:
             if (
             if (
                 !selected ||
                 !selected ||
                 selected.from === selected.to
                 selected.from ===
                    selected.to
             ) {
             ) {
                 this.focusEditor();
                 this.focusEditor();
Line 731: Line 972:


         insertVariable(name) {
         insertVariable(name) {
             name = String(name || '')
             name =
                .trim()
                String(name || '')
                .toUpperCase();
                    .trim()
                    .toUpperCase();


             if (!name) return;
             if (!name) {
                return;
            }


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


             if (!selected) return;
             if (!selected) {
                return;
            }


             const text =
             const text =
Line 754: Line 1,000:
         },
         },


         insertCondition(type, variable) {
         insertCondition(
            type,
            variable
        ) {
             type =
             type =
                 String(type || '')
                 String(type || '')
Line 767: Line 1,016:
                 this.getEditorSelection();
                 this.getEditorSelection();


             if (!selected) return;
             if (!selected) {
                return;
            }


             let text;
             let text;
Line 774: Line 1,025:
             switch (type) {
             switch (type) {
                 case 'if': {
                 case 'if': {
                     if (!variable) return;
                     if (!variable) {
                        return;
                    }


                     const opening =
                     const opening =
Line 811: Line 1,064:


                 case 'elseif':
                 case 'elseif':
                     text = '<!-- @elseif -->';
                     text =
                     cursorOffset = text.length;
                        '<!-- @elseif -->';
                     cursorOffset =
                        text.length;
                     break;
                     break;


                 case 'else':
                 case 'else':
                     text = '<!-- @else -->';
                     text =
                     cursorOffset = text.length;
                        '<!-- @else -->';
                     cursorOffset =
                        text.length;
                     break;
                     break;


                 case 'endif':
                 case 'endif':
                     text = '<!-- @endif -->';
                     text =
                     cursorOffset = text.length;
                        '<!-- @endif -->';
                     cursorOffset =
                        text.length;
                     break;
                     break;


Line 849: Line 1,108:
                     name => {
                     name => {
                         variables.add(
                         variables.add(
                             String(name).toUpperCase()
                             String(
                                name
                            ).toUpperCase()
                         );
                         );
                     }
                     }
Line 855: Line 1,116:
             }
             }


             return Array.from(variables);
             return Array.from(
                variables
            );
         },
         },


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


             if (className) {
             if (className) {
                 select.addClass(className);
                 select.addClass(
                    className
                );
             }
             }


             options.forEach(option => {
             options.forEach(
                $('<option>')
                option => {
                    .attr(
                    $('<option>')
                        'value',
                        .attr(
                        option.value
                            'value',
                    )
                            option.value
                    .text(option.label)
                        )
                    .appendTo(select);
                        .text(
             });
                            option.label
                        )
                        .appendTo(select);
                }
             );


             return select;
             return select;
Line 882: Line 1,155:


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


             const variables =
             const variables =
Line 888: Line 1,163:


             const current =
             const current =
                 this.editorTools.variableSelect.val();
                 this.editorTools
                    .variableSelect
                    .val();


             const currentCondition =
             const currentCondition =
Line 895: Line 1,172:
                     .val();
                     .val();


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


                variables.forEach(name => {
                    if (
                    $('<option>')
                        value &&
                         .attr(
                         variables.includes(
                             'value',
                             value
                            name
                         )
                         )
                        .text(name)
                     ) {
                        .appendTo(select);
                        select.val(
                });
                            value
 
                        );
                if (
                    } else if (
                    value &&
                        variables.length
                     variables.includes(value)
                    ) {
                ) {
                        select.val(
                    select.val(value);
                            variables[0]
                } else if (variables.length) {
                        );
                    select.val(
                    }
                        variables[0]
                };
                    );
                }
            };


             populate(
             populate(
                 this.editorTools.variableSelect,
                 this.editorTools
                    .variableSelect,
                 current
                 current
             );
             );
Line 936: Line 1,225:


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


             const type =
             const type =
Line 945: Line 1,236:
             this.editorTools
             this.editorTools
                 .conditionVariableWrap
                 .conditionVariableWrap
                 .toggle(type === 'if');
                 .toggle(
                    type === 'if'
                );
         },
         },


         renderEditorTools() {
         renderEditorTools() {
             const tools = $('<div>')
             const tools =
                .addClass(
                $('<div>')
                    'preload-editor-tools'
                    .addClass(
                );
                        'preload-editor-tools'
                    );


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


             wrapGroup.append(
             wrapGroup.append(
Line 969: Line 1,264:
             );
             );


             const variableGroup = $('<div>')
             const variableGroup =
                .addClass(
                $('<div>')
                    'preload-editor-tool-group ' +
                    .addClass(
                    'preload-editor-tool-group-variable'
                        'preload-editor-tool-group ' +
                );
                        'preload-editor-tool-group-variable'
                    );


             $('<span>')
             $('<span>')
Line 982: Line 1,278:
                     'Insert existing variable'
                     'Insert existing variable'
                 )
                 )
                 .appendTo(variableGroup);
                 .appendTo(
                    variableGroup
                );


             const variableSelect =
             const variableSelect =
Line 992: Line 1,290:
             variableGroup.append(
             variableGroup.append(
                 variableSelect,
                 variableSelect,
                 this.createButton(
                 this.createButton(
                     'Insert',
                     'Insert',
Line 1,002: Line 1,301:
             );
             );


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


             $('<span>')
             $('<span>')
Line 1,012: Line 1,312:
                     'preload-editor-tool-label'
                     'preload-editor-tool-label'
                 )
                 )
                 .text('Insert condition')
                 .text(
                 .appendTo(conditionGroup);
                    'Insert condition'
                )
                 .appendTo(
                    conditionGroup
                );


             const conditionTypeSelect =
             const conditionTypeSelect =
Line 1,023: Line 1,327:
                         },
                         },
                         {
                         {
                             value: 'elseif',
                             value:
                             label: '@elseif'
                                'elseif',
                             label:
                                '@elseif'
                         },
                         },
                         {
                         {
                             value: 'else',
                             value: 'else',
                             label: '@else'
                             label:
                                '@else'
                         },
                         },
                         {
                         {
                             value: 'endif',
                             value:
                             label: '@endif'
                                'endif',
                             label:
                                '@endif'
                         }
                         }
                     ],
                     ],
Line 1,102: Line 1,411:
         },
         },


        // =========================================================
         // Preload syntax highlighting
         // Preload syntax highlighting
        // =========================================================


         getKnownVariables() {
         getKnownVariables() {
Line 1,114: Line 1,425:
             if (
             if (
                 fields &&
                 fields &&
                 typeof fields === 'object'
                 typeof fields ===
                    'object'
             ) {
             ) {
                 Object.keys(fields).forEach(
                 Object.keys(fields).forEach(
                     name => {
                     name => {
                         known.add(
                         known.add(
                             String(name).toUpperCase()
                             String(
                                name
                            ).toUpperCase()
                         );
                         );
                     }
                     }
Line 1,133: Line 1,447:
         ) {
         ) {
             const decorations = [];
             const decorations = [];
             const source =
             const source =
                 state.doc.toString();
                 state.doc.toString();
Line 1,146: Line 1,461:
             while (
             while (
                 (match =
                 (match =
                     variablePattern.exec(source)) !==
                     variablePattern.exec(
                null
                        source
                    )) !== null
             ) {
             ) {
                 const name =
                 const name =
Line 1,153: Line 1,469:


                 const known =
                 const known =
                     knownVariables.has(name);
                     knownVariables.has(
                        name
                    );


                 decorations.push(
                 decorations.push(
Line 1,176: Line 1,494:
             while (
             while (
                 (match =
                 (match =
                     conditionPattern.exec(source)) !==
                     conditionPattern.exec(
                null
                        source
                    )) !== null
             ) {
             ) {
                 decorations.push(
                 decorations.push(
Line 1,243: Line 1,562:


                     if (
                     if (
                         !knownVariables.has(name)
                         !knownVariables.has(
                            name
                        )
                     ) {
                     ) {
                         continue;
                         continue;
Line 1,272: Line 1,593:
         },
         },


         async installSyntaxHighlighting(view) {
         async installSyntaxHighlighting(
            view
        ) {
             await mw.loader.using(
             await mw.loader.using(
                 'ext.CodeMirror.v6.lib'
                 'ext.CodeMirror.v6.lib'
Line 1,325: Line 1,648:
         },
         },


         // CodeMirror mounting
         // =========================================================
        // Editor state synchronization
        // =========================================================


         async mountCodeMirror(textarea) {
         /*
             return new Promise(
        * Re-analyze the current CodeMirror content,
                (resolve, reject) => {
        * create configuration fields for newly found
                    let settled = false;
        * variables, keep selection valid, and update
        * all live UI.
        */
        syncEditorState() {
             this.reanalyze();


                    const cleanup = () => {
            this.renderVariablesBySelector();
                        mw.hook(
                            'dev.CCM.ready'
                        ).remove(onReady);


                        mw.hook(
            this.renderVariableConfigurationBySelector();
                            'dev.CCM.load'
                        ).remove(onLoad);
                    };


                    const onReady = async cm => {
            this.renderAnalysisBySelector();
                        if (
                            settled ||
                            !cm ||
                            cm.textarea !==
                                textarea ||
                            !cm.view
                        ) {
                            return;
                        }


                        settled = true;
            this.renderConfigPreviewBySelector();
                        cleanup();


                        const view = cm.view;
            this.updateEditorTools();
        },


                        const inputHandler =
        // =========================================================
                            () => {
        // CodeMirror mounting
                                this.state
        // =========================================================
                                    .preloadContent =
                                    view.state.doc.toString();


                                this.state.dirty =
        async mountCodeMirror(
                                    true;
            textarea
        ) {
            return new Promise(
                (resolve, reject) => {
                    let settled = false;


                                this.reanalyze();
                    const cleanup =
                                this.renderVariablesBySelector();
                        () => {
                                 this.renderVariableConfigurationBySelector();
                            mw.hook(
                                this.renderAnalysisBySelector();
                                 'dev.CCM.ready'
                                this.renderConfigPreviewBySelector();
                            ).remove(
                                 this.updateEditorTools();
                                 onReady
                             };
                             );


                        view.dom.addEventListener(
                            mw.hook(
                            'input',
                                'dev.CCM.load'
                             inputHandler
                             ).remove(
                         );
                                onLoad
                            );
                         };


                         view.dom.classList.add(
                    const onReady =
                             'preload-editor-codemirror'
                         async cm => {
                        );
                            if (
                                settled ||
                                !cm ||
                                cm.textarea !==
                                    textarea ||
                                !cm.view
                             ) {
                                return;
                            }


                        const wrapper =
                            settled = true;
                             view.dom.closest(
                             cleanup();
                                '.ext-codemirror-wrapper'
                            );


                        if (wrapper) {
                             const view =
                             wrapper.classList.add(
                                 cm.view;
                                 'preload-editor-codemirror-wrapper'
                            );
                        }


                        this.editor = {
                            const inputHandler =
                            cm,
                                () => {
                            view,
                                    this.state
                            textarea,
                                        .preloadContent =
                            inputHandler
                                        view.state.doc.toString();
                        };


                        this.editorView = view;
                                    this.state.dirty =
                                        true;


                        this.state.preloadContent =
                                    /*
                            view.state.doc.toString();
                                    * This now performs the
                                    * entire live synchronization.
                                    *
                                    * So typing %%ZHS%% will:
                                    *
                                    * - detect ZHS
                                    * - create config.fields.ZHS
                                    * - add ZHS to the variable list
                                    * - make ZHS selectable
                                    * - update its configuration
                                    * - update config preview
                                    * - update editor tools
                                    */
                                    this.syncEditorState();
                                };


                        try {
                             view.dom.addEventListener(
                             await this.installSyntaxHighlighting(
                                 'input',
                                 view
                                inputHandler
                             );
                             );


                             resolve(view);
                             view.dom.classList.add(
                        } catch (error) {
                                'preload-editor-codemirror'
                             reject(error);
                             );
                        }
                    };


                    const onLoad = loader => {
                            const wrapper =
                        if (
                                view.dom.closest(
                            settled ||
                                    '.ext-codemirror-wrapper'
                            typeof loader !==
                                );
                                'function'
                        ) {
                            return;
                        }


                        try {
                             if (wrapper) {
                             loader(textarea);
                                wrapper.classList.add(
                        } catch (error) {
                                    'preload-editor-codemirror-wrapper'
                            settled = true;
                                );
                            cleanup();
                             }
                             reject(error);
                        }
                    };


                    mw.hook(
                            this.editor = {
                        'dev.CCM.ready'
                                cm,
                    ).add(onReady);
                                view,
                                textarea,
                                inputHandler
                            };


                    mw.hook(
                            this.editorView =
                        'dev.CCM.load'
                                view;
                    ).add(onLoad);
                }
            );
        },


        // Main UI
                            this.state
                                .preloadContent =
                                view.state.doc.toString();


        init() {
                            try {
            const root =
                                await this.installSyntaxHighlighting(
                $('#mw-content-text');
                                    view
                                );


            if (!root.length) return;
                                /*
                                * Run one initial synchronization
                                * after CodeMirror is mounted so the
                                * UI is based on the actual document.
                                */
                                this.syncEditorState();


            this.root = $('<div>')
                                resolve(
                .attr(
                                    view
                    'id',
                                );
                    'preload-editor'
                            } catch (
                )
                                error
                .addClass(
                            ) {
                    'preload-editor'
                                reject(
                );
                                    error
                                );
                            }
                        };
 
                    const onLoad =
                        loader => {
                            if (
                                settled ||
                                typeof loader !==
                                    'function'
                            ) {
                                return;
                            }
 
                            try {
                                loader(
                                    textarea
                                );
                            } catch (
                                error
                            ) {
                                settled = true;
                                cleanup();
                                reject(
                                    error
                                );
                            }
                        };


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


            this.renderShell();
                    mw.hook(
                        'dev.CCM.load'
                    ).add(
                        onLoad
                    );
                }
            );
         },
         },


         renderShell() {
         // =========================================================
            this.destroyEditor();
        // Main UI
            this.root.empty();
        // =========================================================


             const loader =
        init() {
                 this.createSection(
             const root =
                    'Preload'
                 $('#mw-content-text');
                );


             const titleInput =
            if (!root.length) {
                 this.textInput(
                return;
                     this.state.preloadTitle,
            }
 
            this.root =
                $('<div>')
                    .attr(
                        'id',
                        'preload-editor'
                    )
                    .addClass(
                        'preload-editor'
                    );
 
            root.append(
                this.root
            );
 
            this.renderShell();
        },
 
        renderShell() {
            this.destroyEditor();
 
            this.root.empty();
 
            const loader =
                this.createSection(
                    'Preload'
                );
 
             const titleInput =
                 this.textInput(
                     this.state.preloadTitle,
                     'Template:Example/Preload'
                     'Template:Example/Preload'
                 ).attr(
                 ).attr(
Line 1,501: Line 1,902:
                     'New',
                     'New',
                     '',
                     '',
                     () => this.startNew()
                     () =>
                        this.startNew()
                 );
                 );


Line 1,525: Line 1,927:
             );
             );


             this.root.append(loader);
             this.root.append(
                loader
            );


             this.status =
             this.status =
Line 1,562: Line 1,966:
                     'Load an existing preload, create a new one, or start from a sample wiki page.'
                     'Load an existing preload, create a new one, or start from a sample wiki page.'
                 )
                 )
                 .appendTo(section);
                 .appendTo(
                    section
                );


             const tips =
             const tips =
Line 1,574: Line 1,980:
                 'New preload: starts with the standard Preload Template structure.',
                 'New preload: starts with the standard Preload Template structure.',
                 'Sample page: loads normal wiki page content so you can turn it into a preload.'
                 'Sample page: loads normal wiki page content so you can turn it into a preload.'
             ].forEach(text => {
             ].forEach(
                $('<li>')
                text => {
                    .text(text)
                    $('<li>')
                    .appendTo(tips);
                        .text(text)
             });
                        .appendTo(
                            tips
                        );
                }
            );
 
            section.append(
                tips
             );


            section.append(tips);
             this.workspace.append(
             this.workspace.append(section);
                section
            );
         },
         },


         setStatus(message, type) {
         setStatus(
             if (!this.status) return;
            message,
            type
        ) {
             if (!this.status) {
                return;
            }


             this.status.removeClass(
             this.status.removeClass(
Line 1,605: Line 2,025:
         },
         },


        // =========================================================
         // Load / create
         // Load / create
        // =========================================================


         async loadExisting(title) {
         async loadExisting(
            title
        ) {
             title =
             title =
                 String(title || '').trim();
                 String(title || '')
                    .trim();


             if (!title) {
             if (!title) {
                 mw.notify(
                 mw.notify(
                     'Enter a preload page title first.',
                     'Enter a preload page title first.',
                     { type: 'warn' }
                     {
                        type: 'warn'
                    }
                 );
                 );


Line 1,627: Line 2,054:
             try {
             try {
                 const page =
                 const page =
                     await this.getPage(title);
                     await this.getPage(
                        title
                    );


                 if (!page) {
                 if (!page) {
Line 1,657: Line 2,086:
                     'success'
                     'success'
                 );
                 );
             } catch (error) {
             } catch (
                error
            ) {
                 console.error(
                 console.error(
                     'Preload Editor:',
                     'Preload Editor:',
Line 1,671: Line 2,102:
         },
         },


         async findSimilarPreloads(title) {
         async findSimilarPreloads(
            title
        ) {
             let query =
             let query =
                 title
                 title
Line 1,696: Line 2,129:
                         )
                         )
                 );
                 );
             } catch (error) {
             } catch (
                error
            ) {
                 console.error(
                 console.error(
                     'Preload Editor search:',
                     'Preload Editor search:',
Line 1,723: Line 2,158:
                     '".'
                     '".'
                 )
                 )
                 .appendTo(section);
                 .appendTo(
                    section
                );


             if (suggestions.length) {
             if (
                suggestions.length
            ) {
                 $('<h3>')
                 $('<h3>')
                     .text('Similar preloads')
                     .text(
                     .appendTo(section);
                        'Similar preloads'
                    )
                     .appendTo(
                        section
                    );


                 const list =
                 const list =
Line 1,751: Line 2,194:
                 );
                 );


                 section.append(list);
                 section.append(
                    list
                );
             }
             }


Line 1,759: Line 2,204:
                     'preload-editor-button-primary',
                     'preload-editor-button-primary',
                     () =>
                     () =>
                         this.startNew(title)
                         this.startNew(
                            title
                        )
                 )
                 )
             );
             );


             this.workspace.append(section);
             this.workspace.append(
                section
            );
         },
         },


         async startNew(title) {
         async startNew(
            title
        ) {
             title =
             title =
                 String(title || '').trim();
                 String(title || '')
                    .trim();


             this.state = {
             this.state = {
Line 1,779: Line 2,231:
                 variables: ['PAGE'],
                 variables: ['PAGE'],
                 conditions: [],
                 conditions: [],
                 selectedVariable: 'PAGE',
                 selectedVariable:
                    'PAGE',
                 mode: 'new',
                 mode: 'new',
                 dirty: true
                 dirty: true
Line 1,799: Line 2,252:
                 );
                 );


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


             this.setStatus(
             this.setStatus(
Line 1,808: Line 2,263:
             try {
             try {
                 const page =
                 const page =
                     await this.getPage(result);
                     await this.getPage(
                        result
                    );


                 if (!page) {
                 if (!page) {
Line 1,825: Line 2,282:
                     preloadTitle: '',
                     preloadTitle: '',
                     preloadSource: '',
                     preloadSource: '',
                     preloadContent: sample,
                     preloadContent:
                        sample,
                     config:
                     config:
                         this.emptyConfig(),
                         this.emptyConfig(),
                     variables: ['PAGE'],
                     variables: ['PAGE'],
                     conditions: [],
                     conditions: [],
                     selectedVariable: 'PAGE',
                     selectedVariable:
                        'PAGE',
                     mode: 'new',
                     mode: 'new',
                     dirty: true
                     dirty: true
Line 1,842: Line 2,301:
                     'success'
                     'success'
                 );
                 );
             } catch (error) {
             } catch (
                error
            ) {
                 console.error(
                 console.error(
                     'Preload Editor sample:',
                     'Preload Editor sample:',
Line 1,856: Line 2,317:
         },
         },


         convertSampleToPreload(source) {
         convertSampleToPreload(
            source
        ) {
             let content =
             let content =
                 String(source || '').trim();
                 String(source || '')
                    .trim();


             const include =
             const include =
Line 1,906: Line 2,370:
                     $('<h2>')
                     $('<h2>')
                         .text(title)
                         .text(title)
                         .appendTo(box);
                         .appendTo(
                            box
                        );


                     $('<p>')
                     $('<p>')
                         .text(message)
                         .text(message)
                         .appendTo(box);
                         .appendTo(
                            box
                        );


                     const input =
                     const input =
Line 1,927: Line 2,395:
                             () => {
                             () => {
                                 overlay.remove();
                                 overlay.remove();
                                 resolve(null);
                                 resolve(
                                    null
                                );
                             }
                             }
                         );
                         );
Line 1,942: Line 2,412:
                                     ).trim();
                                     ).trim();


                                 if (!value) return;
                                 if (!value) {
                                    return;
                                }


                                 overlay.remove();
                                 overlay.remove();
                                 resolve(value);
                                 resolve(
                                    value
                                );
                             }
                             }
                         );
                         );
Line 1,959: Line 2,433:
                     );
                     );


                     overlay.append(box);
                     overlay.append(
                        box
                    );
 
                     $(document.body)
                     $(document.body)
                         .append(overlay);
                         .append(
                            overlay
                        );


                     input.trigger('focus');
                     input.trigger(
                        'focus'
                    );


                     input.on(
                     input.on(
Line 2,004: Line 2,485:


             const configTitle =
             const configTitle =
                 title.replace(/\/+$/, '') +
                 title.replace(
                    /\/+$/,
                    ''
                ) +
                 '/config.json';
                 '/config.json';


Line 2,018: Line 2,502:
                             configPage.content
                             configPage.content
                         );
                         );
                 } catch (error) {
                 } catch (
                    error
                ) {
                     throw new Error(
                     throw new Error(
                         'The existing config.json contains invalid JSON: ' +
                         'The existing config.json contains invalid JSON: ' +
Line 2,038: Line 2,524:


             this.state = {
             this.state = {
                 preloadTitle: title,
                 preloadTitle:
                 preloadSource: source,
                    title,
                 preloadContent: source,
 
                 preloadSource:
                    source,
 
                 preloadContent:
                    source,
 
                 config,
                 config,
                 variables:
                 variables:
                     analysis.variables,
                     analysis.variables,
                 conditions:
                 conditions:
                     analysis.conditions,
                     analysis.conditions,
                 selectedVariable:
                 selectedVariable:
                     analysis.variables.includes('PAGE')
                     analysis.variables.includes(
                        'PAGE'
                    )
                         ? 'PAGE'
                         ? 'PAGE'
                         : (
                         : (
Line 2,053: Line 2,550:
                             'PAGE'
                             'PAGE'
                         ),
                         ),
                 mode: 'edit',
                 mode: 'edit',
                 dirty: false
                 dirty: false
Line 2,060: Line 2,558:
         },
         },


        // =========================================================
         // Editor
         // Editor
        // =========================================================


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


Line 2,101: Line 2,602:
                     'New preload'
                     'New preload'
                 )
                 )
                 .appendTo(meta);
                 .appendTo(
                    meta
                );


             if (
             if (
Line 2,112: Line 2,615:
                     )
                     )
                     .text('NEW')
                     .text('NEW')
                     .appendTo(meta);
                     .appendTo(
                        meta
                    );
             }
             }


             editorSection.append(meta);
             editorSection.append(
                meta
            );


             editorSection.append(
             editorSection.append(
Line 2,138: Line 2,645:
                         'preload-editor-codemirror-mount'
                         'preload-editor-codemirror-mount'
                     )
                     )
                     .append(textarea);
                     .append(
                        textarea
                    );


             editorSection.append(
             editorSection.append(
Line 2,144: Line 2,653:
             );
             );


             left.append(editorSection);
             left.append(
                editorSection
            );


             /*
             /*
             * RIGHT PANEL
             * RIGHT PANEL
            *
            * Analysis is intentionally first.
            * Variables are now only a live list.
            * One configuration is displayed at a time.
             */
             */


Line 2,224: Line 2,731:
                     'Save Preload + Config',
                     'Save Preload + Config',
                     'preload-editor-button-primary',
                     'preload-editor-button-primary',
                     () => this.saveAll()
                     () =>
                        this.saveAll()
                 ),
                 ),
                 this.createButton(
                 this.createButton(
                     'Save Preload Only',
                     'Save Preload Only',
                     '',
                     '',
                     () => this.savePreloadOnly()
                     () =>
                        this.savePreloadOnly()
                 )
                 )
             );
             );
Line 2,272: Line 2,782:
             this.mountCodeMirror(
             this.mountCodeMirror(
                 textarea.get(0)
                 textarea.get(0)
             ).catch(error => {
             ).catch(
                console.error(
                error => {
                    'Preload Editor CodeMirror:',
                    console.error(
                    error
                        'Preload Editor CodeMirror:',
                );
                        error
                    );


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


Line 2,309: Line 2,821:
                 analysis.conditions;
                 analysis.conditions;


            /*
            * This is where newly typed variables
            * become configuration fields.
            *
            * For example:
            *
            * %%ZHS%%
            *
            * becomes:
            *
            * config.fields.ZHS = {
            *    label: 'Zhs',
            *    description: '',
            *    type: 'text'
            * };
            */
             this.state.config =
             this.state.config =
                 this.prepareConfigForVariables(
                 this.prepareConfigForVariables(
Line 2,321: Line 2,849:
             /*
             /*
             * PAGE is always available.
             * PAGE is always available.
            * If the selected variable disappears,
            * move the focus to PAGE or the first
            * currently-used variable.
             */
             */
             const available =
             const available =
                 ['PAGE'].concat(
                 ['PAGE'].concat(
                     analysis.variables.filter(
                     analysis.variables.filter(
                         name => name !== 'PAGE'
                         name =>
                            name !== 'PAGE'
                     )
                     )
                 );
                 );


            /*
            * If the selected variable
            * disappeared from the preload,
            * move selection to PAGE or
            * the first available variable.
            */
             if (
             if (
                 !available.includes(
                 !available.includes(
                     this.state.selectedVariable
                     this.state
                        .selectedVariable
                 )
                 )
             ) {
             ) {
                 this.state.selectedVariable =
                 this.state
                     available[0] || 'PAGE';
                    .selectedVariable =
                     available[0] ||
                    'PAGE';
             }
             }
         },
         },


         renderAnalysis(container) {
         renderAnalysis(
            container
        ) {
             container.empty();
             container.empty();


Line 2,374: Line 2,911:


             const createMetric =
             const createMetric =
                 (value, label) =>
                 (
                    value,
                    label
                ) =>
                     $('<div>')
                     $('<div>')
                         .addClass(
                         .addClass(
Line 2,385: Line 2,925:
                                 )
                                 )
                                 .text(value),
                                 .text(value),
                             $('<span>')
                             $('<span>')
                                 .addClass(
                                 .addClass(
Line 2,399: Line 2,940:
                         : 'variables'
                         : 'variables'
                 ),
                 ),
                 createMetric(
                 createMetric(
                     conditionCount,
                     conditionCount,
Line 2,405: Line 2,947:
                         : 'condition markers'
                         : 'condition markers'
                 ),
                 ),
                 createMetric(
                 createMetric(
                     configuredCount,
                     configuredCount,
Line 2,418: Line 2,961:


             if (
             if (
                 analysis.conditionVariables.length
                 analysis.conditionVariables
                    .length
             ) {
             ) {
                 container.append(
                 container.append(
Line 2,453: Line 2,997:
                         $('<li>')
                         $('<li>')
                             .text(error)
                             .text(error)
                             .appendTo(errors);
                             .appendTo(
                                errors
                            );
                     }
                     }
                 );
                 );
Line 2,475: Line 3,021:
                         $('<li>')
                         $('<li>')
                             .text(warning)
                             .text(warning)
                             .appendTo(warnings);
                             .appendTo(
                                warnings
                            );
                     }
                     }
                 );
                 );
Line 2,517: Line 3,065:
         },
         },


         /*
         // =========================================================
        * Variable list
        // Variables
        *
        // =========================================================
        * This replaces the old behaviour where every
 
        * variable rendered its entire configuration card.
         renderVariables(
        */
            container
         renderVariables(container) {
        ) {
             container.empty();
             container.empty();


Line 2,537: Line 3,085:
                 analysis.variables.slice();
                 analysis.variables.slice();


            /*
            * PAGE is always shown first.
            */
             const ordered = ['PAGE'];
             const ordered = ['PAGE'];


             variables.forEach(name => {
             variables.forEach(
                if (
                name => {
                    name !== 'PAGE' &&
                    if (
                    !ordered.includes(name)
                        name !== 'PAGE' &&
                 ) {
                        !ordered.includes(
                     ordered.push(name);
                            name
                        )
                    ) {
                        ordered.push(name);
                    }
                }
            );
 
            /*
            * Existing configured fields which
            * are no longer used are also shown.
            *
            * This preserves the previous behaviour
            * where unused configuration is visible.
            */
            const fields =
                 this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};
 
            Object.keys(fields).forEach(
                name => {
                     const upper =
                        name.toUpperCase();
 
                    if (
                        upper !== 'PAGE' &&
                        !ordered.includes(
                            upper
                        )
                    ) {
                        ordered.push(
                            upper
                        );
                    }
                 }
                 }
             });
             );


             const list =
             const list =
Line 2,554: Line 3,140:
                     );
                     );


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


                const isUsed =
                    const isUsed =
                    analysis.variables.includes(
                        analysis.variables.includes(
                        name
                            name
                    );
                        );


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


                const nameLabel =
                    const nameLabel =
                    $('<span>')
                        $('<span>')
                        .addClass(
                            .addClass(
                            'preload-editor-variable-name'
                                'preload-editor-variable-name'
                        )
                            )
                        .text(name);
                            .text(name);
 
                item.append(
                    nameLabel
                );


                if (name === 'PAGE') {
                     item.append(
                     item.append(
                         $('<span>')
                         nameLabel
                            .addClass(
                                'preload-editor-badge'
                            )
                            .text('AUTOMATIC')
                     );
                     );
                } else if (!isUsed) {
 
                    item.append(
                    if (
                        $('<span>')
                        name === 'PAGE'
                            .addClass(
                    ) {
                                'preload-editor-badge is-warning'
                        item.append(
                            )
                            $('<span>')
                            .text('UNUSED')
                                .addClass(
                                    'preload-editor-badge'
                                )
                                .text(
                                    'AUTOMATIC'
                                )
                        );
                    } else if (
                        !isUsed
                    ) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text(
                                    'UNUSED'
                                )
                        );
                    }
 
                    list.append(
                        item
                     );
                     );
                 }
                 }
            );


                list.append(item);
             container.append(
            });
                list
 
             );
             container.append(list);
         },
 
             if (!ordered.length) {
                container.append(
                    $('<div>')
                        .addClass(
                            'preload-editor-empty'
                        )
                        .text(
                            'No variables found in the preload.'
                        )
                );
            }
         },


         renderVariablesBySelector() {
         renderVariablesBySelector() {
Line 2,649: Line 3,239:
         },
         },


         /*
         // =========================================================
        * Select a variable from the list and display
        // Variable selection / configuration
        * only its configuration.
        // =========================================================
        */
 
         selectVariable(name) {
         selectVariable(
            name
        ) {
             name =
             name =
                 String(name || '')
                 String(name || '')
Line 2,659: Line 3,251:
                     .toUpperCase();
                     .toUpperCase();


             if (!name) return;
             if (!name) {
                return;
            }


             const available =
             const available =
Line 2,670: Line 3,264:
                     ).filter(
                     ).filter(
                         variable =>
                         variable =>
                             variable !== 'PAGE'
                             variable !==
                            'PAGE'
                     )
                     )
                 );
                 );


             if (!available.includes(name)) {
            /*
            * Also allow selecting an existing
            * unused configuration field.
            */
            const fields =
                this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};
 
            Object.keys(fields).forEach(
                fieldName => {
                    const upper =
                        fieldName.toUpperCase();
 
                    if (
                        !available.includes(
                            upper
                        )
                    ) {
                        available.push(
                            upper
                        );
                    }
                }
            );
 
             if (
                !available.includes(
                    name
                )
            ) {
                 return;
                 return;
             }
             }
Line 2,682: Line 3,308:


             this.renderVariablesBySelector();
             this.renderVariablesBySelector();
             this.renderVariableConfigurationBySelector();
             this.renderVariableConfigurationBySelector();
         },
         },


Line 2,703: Line 3,329:
         },
         },


         renderVariableConfiguration(container) {
         renderVariableConfiguration(
            container
        ) {
             container.empty();
             container.empty();


Line 2,731: Line 3,359:
                 );
                 );


             if (name === 'PAGE') {
             if (
                name === 'PAGE'
            ) {
                 container.append(
                 container.append(
                     this.createAutomaticPageCard(
                     this.createAutomaticPageCard(
Line 2,750: Line 3,380:
         },
         },


         /*
         // =========================================================
        * PAGE is precompiled/automatic.
        // Automatic PAGE
        *
        // =========================================================
        * It remains selectable so the user can inspect
 
        * what PAGE represents, but its configuration
         createAutomaticPageCard(
        * cannot be edited.
            field
        */
        ) {
         createAutomaticPageCard(field) {
             const card =
             const card =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-variable preload-editor-variable-automatic'
                         'preload-editor-variable ' +
                        'preload-editor-variable-automatic'
                     )
                     )
                     .attr(
                     .attr(
Line 2,775: Line 3,405:


             header.append(
             header.append(
                 $('<h3>').text('PAGE'),
                 $('<h3>').text(
                    'PAGE'
                ),
 
                 $('<span>')
                 $('<span>')
                     .addClass(
                     .addClass(
                         'preload-editor-badge'
                         'preload-editor-badge'
                     )
                     )
                     .text('AUTOMATIC')
                     .text(
                        'AUTOMATIC'
                    )
             );
             );


             card.append(header);
             card.append(
                header
            );


             $('<p>')
             $('<p>')
Line 2,792: Line 3,429:
                     'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                     'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                 )
                 )
                 .appendTo(card);
                 .appendTo(
                    card
                );


             const label =
             const label =
                 this.textInput(
                 this.textInput(
                     field.label || 'Page Name'
                     field.label ||
                        'Page Name'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 2,804: Line 3,444:
             const description =
             const description =
                 this.textarea(
                 this.textarea(
                     field.description || 'Name of the page.'
                     field.description ||
                        'Name of the page.'
                 )
                 )
                     .addClass(
                     .addClass(
Line 2,816: Line 3,457:
             const type =
             const type =
                 this.textInput(
                 this.textInput(
                     field.type || 'page'
                     field.type ||
                        'page'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 2,824: Line 3,466:
             const required =
             const required =
                 this.checkbox(
                 this.checkbox(
                     field.required !== false
                     field.required !==
                        false
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 2,832: Line 3,475:
             const namespace =
             const namespace =
                 this.textInput(
                 this.textInput(
                     field.namespace || 'Main'
                     field.namespace ||
                        'Main'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 2,840: Line 3,484:
             const allowNew =
             const allowNew =
                 this.checkbox(
                 this.checkbox(
                     field['allow-new'] === true
                     field['allow-new'] ===
                        true
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 2,851: Line 3,496:
                     label
                     label
                 ),
                 ),
                 this.fieldRow(
                 this.fieldRow(
                     'Description',
                     'Description',
                     description
                     description
                 ),
                 ),
                 this.fieldRow(
                 this.fieldRow(
                     'Type',
                     'Type',
                     type
                     type
                 ),
                 ),
                 this.fieldRow(
                 this.fieldRow(
                     'Required',
                     'Required',
                     required
                     required
                 ),
                 ),
                 this.fieldRow(
                 this.fieldRow(
                     'Namespace',
                     'Namespace',
                     namespace
                     namespace
                 ),
                 ),
                 this.fieldRow(
                 this.fieldRow(
                     'Allow new page',
                     'Allow new page',
Line 2,875: Line 3,525:
             return card;
             return card;
         },
         },
        // =========================================================
        // Normal variable card
        // =========================================================


         createVariableCard(
         createVariableCard(
Line 2,898: Line 3,552:


             header.append(
             header.append(
                 $('<h3>').text(name)
                 $('<h3>').text(
                    name
                )
             );
             );


             if (name === 'PAGE') {
             if (
                header.append(
                 !analysis.variables.includes(
                    $('<span>')
                    name
                        .addClass(
                )
                            'preload-editor-badge'
                        )
                        .text('AUTOMATIC')
                );
            } else if (
                 !analysis.variables.includes(name)
             ) {
             ) {
                 header.append(
                 header.append(
Line 2,917: Line 3,567:
                             'preload-editor-badge is-warning'
                             'preload-editor-badge is-warning'
                         )
                         )
                         .text('UNUSED')
                         .text(
                            'UNUSED'
                        )
                 );
                 );
             }
             }


             card.append(header);
             card.append(
                header
            );
 
            // Label


             const label =
             const label =
Line 2,935: Line 3,591:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 2,945: Line 3,602:
                 )
                 )
             );
             );
            // Description


             const description =
             const description =
                 this.textarea(
                 this.textarea(
                     field.description || ''
                     field.description ||
                        ''
                 ).addClass(
                 ).addClass(
                     'preload-editor-small-textarea'
                     'preload-editor-small-textarea'
Line 2,960: Line 3,620:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 2,970: Line 3,631:
                 )
                 )
             );
             );
            // Type


             const types = [
             const types = [
                ['text', 'Text'],
                ['textarea', 'Textarea'],
                ['number', 'Number'],
                ['boolean', 'Boolean'],
                ['select', 'Select'],
                 [
                 [
                     'multiselect',
                     'text',
                     'Multi-select'
                     'Text'
                 ],
                 ],
                 ['page', 'Page'],
                 [
                 ['user', 'User'],
                    'textarea',
                 ['date', 'Date']
                    'Textarea'
                ],
                [
                    'number',
                    'Number'
                ],
                [
                    'boolean',
                    'Boolean'
                ],
                [
                    'select',
                    'Select'
                ],
                [
                    'multiselect',
                    'Multi-select'
                ],
                [
                    'page',
                    'Page'
                ],
                 [
                    'user',
                    'User'
                ],
                 [
                    'date',
                    'Date'
                ]
             ];
             ];


Line 2,989: Line 3,676:
                 this.select(
                 this.select(
                     types.map(
                     types.map(
                         ([value, label]) => ({
                         (
                            [
                                value,
                                label
                            ]
                        ) => ({
                             value,
                             value,
                             label
                             label
                         })
                         })
                     ),
                     ),
                     field.type || 'text'
                     field.type ||
                        'text'
                 );
                 );


Line 3,006: Line 3,699:


                     this.renderVariableConfigurationBySelector();
                     this.renderVariableConfigurationBySelector();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,016: Line 3,710:
                 )
                 )
             );
             );
            // Required


             const required =
             const required =
                 this.checkbox(
                 this.checkbox(
                     field.required === true
                     field.required ===
                        true
                 );
                 );


Line 3,031: Line 3,728:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,041: Line 3,739:
                 )
                 )
             );
             );
            // Placeholder


             const placeholder =
             const placeholder =
                 this.textInput(
                 this.textInput(
                     field.placeholder || ''
                     field.placeholder ||
                        ''
                 );
                 );


Line 3,061: Line 3,762:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,071: Line 3,773:
                 )
                 )
             );
             );
            // Default


             const defaultInput =
             const defaultInput =
                 this.textInput(
                 this.textInput(
                     field.default === undefined
                     field.default ===
                        undefined
                         ? ''
                         ? ''
                         : field.default
                         : field.default
Line 3,093: Line 3,798:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,104: Line 3,810:
                 )
                 )
             );
             );
            // Textarea options


             if (
             if (
                 field.type === 'textarea'
                 field.type ===
                'textarea'
             ) {
             ) {
                 const maxRows =
                 const maxRows =
                     this.textInput(
                     this.textInput(
                         field.maxRows || ''
                         field.maxRows ||
                            ''
                     ).attr(
                     ).attr(
                         'type',
                         'type',
Line 3,138: Line 3,848:


                         this.markDirty();
                         this.markDirty();
                         this.renderConfigPreviewBySelector();
                         this.renderConfigPreviewBySelector();
                     }
                     }
Line 3,149: Line 3,860:
                 );
                 );
             }
             }
            // Select / multiselect


             if (
             if (
                 field.type === 'select' ||
                 field.type ===
                 field.type === 'multiselect'
                    'select' ||
                 field.type ===
                    'multiselect'
             ) {
             ) {
                 this.renderSelectConfiguration(
                 this.renderSelectConfiguration(
Line 3,162: Line 3,877:
             return card;
             return card;
         },
         },
        // =========================================================
        // Select configuration
        // =========================================================


         renderSelectConfiguration(
         renderSelectConfiguration(
Line 3,169: Line 3,888:
             const source =
             const source =
                 this.textInput(
                 this.textInput(
                     field.source || '',
                     field.source ||
                        '',
                     'category:Example'
                     'category:Example'
                 );
                 );
Line 3,180: Line 3,900:


                     if (value) {
                     if (value) {
                         field.source = value;
                         field.source =
                            value;
                     } else {
                     } else {
                         delete field.source;
                         delete field.source;
Line 3,186: Line 3,907:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,200: Line 3,922:
             const sourcePrefix =
             const sourcePrefix =
                 this.textInput(
                 this.textInput(
                     field['source-prefix'] || ''
                     field[
                        'source-prefix'
                    ] || ''
                 );
                 );


Line 3,210: Line 3,934:


                     if (value) {
                     if (value) {
                         field['source-prefix'] =
                         field[
                            value;
                            'source-prefix'
                        ] = value;
                     } else {
                     } else {
                         delete field[
                         delete field[
Line 3,219: Line 3,944:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,235: Line 3,961:
                     field.exclude &&
                     field.exclude &&
                     field.exclude.category ||
                     field.exclude.category ||
                    ''
                        ''
                 );
                 );


Line 3,248: Line 3,974:
                             Object.assign(
                             Object.assign(
                                 {},
                                 {},
                                 field.exclude || {},
                                 field.exclude ||
                                    {},
                                 {
                                 {
                                     category:
                                     category:
Line 3,257: Line 3,984:
                         field.exclude
                         field.exclude
                     ) {
                     ) {
                         delete field.exclude.category;
                         delete field
                            .exclude
                            .category;


                         if (
                         if (
Line 3,269: Line 3,998:


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,286: Line 4,016:
                     'Value transformation'
                     'Value transformation'
                 ),
                 ),
                 this.createTransformControls(
                 this.createTransformControls(
                     field,
                     field,
Line 3,294: Line 4,025:


             const optionsValue =
             const optionsValue =
                 Array.isArray(field.options)
                 Array.isArray(
                    field.options
                )
                     ? field.options
                     ? field.options
                         .map(option => {
                         .map(
                            if (
                            option => {
                                typeof option ===
                                if (
                                'string'
                                    typeof option ===
                            ) {
                                    'string'
                                 return option;
                                ) {
                                    return option;
                                }
 
                                 return (
                                    String(
                                        option.value ||
                                        ''
                                    ) +
                                    ' | ' +
                                    String(
                                        option.label ||
                                        option.value ||
                                        ''
                                    )
                                );
                             }
                             }
 
                         )
                            return (
                                String(
                                    option.value ||
                                    ''
                                ) +
                                ' | ' +
                                String(
                                    option.label ||
                                    option.value ||
                                    ''
                                )
                            );
                         })
                         .join('\n')
                         .join('\n')
                     : '';
                     : '';
Line 3,338: Line 4,073:
                                     line.trim()
                                     line.trim()
                             )
                             )
                             .filter(Boolean);
                             .filter(
                                Boolean
                            );


                     if (!lines.length) {
                     if (!lines.length) {
Line 3,344: Line 4,081:
                     } else {
                     } else {
                         field.options =
                         field.options =
                             lines.map(line => {
                             lines.map(
                                const parts =
                                line => {
                                    line.split(
                                    const parts =
                                        /\s*\|\s*/
                                        line.split(
                                     );
                                            /\s*\|\s*/
                                        );
 
                                    if (
                                        parts.length ===
                                        1
                                     ) {
                                        return parts[0];
                                    }


                                if (
                                    return {
                                    parts.length ===
                                        value:
                                    1
                                            parts[0],
                                ) {
 
                                     return parts[0];
                                        label:
                                            parts
                                                .slice(
                                                    1
                                                )
                                                .join(
                                                    ' | '
                                                )
                                     };
                                 }
                                 }
 
                             );
                                return {
                                    value:
                                        parts[0],
                                    label:
                                        parts
                                            .slice(1)
                                            .join(
                                                ' | '
                                            )
                                };
                             });
                     }
                     }


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 }
                 }
Line 3,383: Line 4,126:
             );
             );
         },
         },
        // =========================================================
        // Transformation controls
        // =========================================================


         createTransformControls(
         createTransformControls(
Line 3,397: Line 4,144:
             $('<h4>')
             $('<h4>')
                 .text(title)
                 .text(title)
                 .appendTo(wrapper);
                 .appendTo(
                    wrapper
                );


             const existing =
             const existing =
Line 3,427: Line 4,176:
                         },
                         },
                         {
                         {
                             value: 'replace',
                             value:
                             label: 'Replace'
                                'replace',
                             label:
                                'Replace'
                         }
                         }
                     ],
                     ],
                     existing.transform || ''
                     existing.transform ||
                        ''
                 );
                 );


             const prefix =
             const prefix =
                 this.textInput(
                 this.textInput(
                     existing.prefix || ''
                     existing.prefix ||
                        ''
                 );
                 );


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


             const from =
             const from =
                 this.textInput(
                 this.textInput(
                     existing.from || ''
                     existing.from ||
                        ''
                 );
                 );


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


Line 3,500: Line 4,256:


                     fromRow.toggle(
                     fromRow.toggle(
                         type === 'replace'
                         type ===
                            'replace'
                     );
                     );


                     toRow.toggle(
                     toRow.toggle(
                         type === 'replace'
                         type ===
                            'replace'
                     );
                     );
                 };
                 };
Line 3,513: Line 4,271:
                         transform.val();
                         transform.val();


                     let object = null;
                     let object =
                        null;


                     if (
                     if (
Line 3,521: Line 4,280:
                     ) {
                     ) {
                         object = {
                         object = {
                             transform: type,
                             transform:
                                type,
                             prefix:
                             prefix:
                                 prefix.val()
                                 prefix.val()
Line 3,531: Line 4,291:
                     ) {
                     ) {
                         object = {
                         object = {
                             transform: type,
                             transform:
                                type,
                             suffix:
                             suffix:
                                 suffix.val()
                                 suffix.val()
                         };
                         };
                     } else if (
                     } else if (
                         type === 'replace' &&
                         type ===
                            'replace' &&
                         from.val()
                         from.val()
                     ) {
                     ) {
                         object = {
                         object = {
                             transform: type,
                             transform:
                             from: from.val(),
                                type,
                             to: to.val()
                             from:
                                from.val(),
                             to:
                                to.val()
                         };
                         };
                     }
                     }
Line 3,550: Line 4,315:
                             object;
                             object;
                     } else {
                     } else {
                         delete field[property];
                         delete field[
                            property
                        ];
                     }
                     }


                     this.markDirty();
                     this.markDirty();
                     this.renderConfigPreviewBySelector();
                     this.renderConfigPreviewBySelector();
                 };
                 };
Line 3,570: Line 4,338:
                 from,
                 from,
                 to
                 to
             ].forEach(input => {
             ].forEach(
                input.on(
                input => {
                    'input.preloadEditor',
                    input.on(
                    update
                        'input.preloadEditor',
                );
                        update
             });
                    );
                }
             );


             controls.append(
             controls.append(
Line 3,597: Line 4,367:
         },
         },


        // =========================================================
         // Config output
         // Config output
        // =========================================================


         getCleanConfig() {
         getCleanConfig() {
Line 3,612: Line 4,384:
             const orderedFields = {};
             const orderedFields = {};


             // PAGE is always first and automatic.
             /*
            * PAGE is always first.
            */
             orderedFields.PAGE =
             orderedFields.PAGE =
                 this.getAutomaticPageField();
                 this.getAutomaticPageField();
Line 3,622: Line 4,396:
                         'PAGE'
                         'PAGE'
                 )
                 )
                 .forEach(name => {
                 .forEach(
                    orderedFields[name] =
                    name => {
                        fields[name];
                        orderedFields[name] =
                 });
                            fields[name];
                    }
                 );


             config.fields =
             config.fields =
Line 3,641: Line 4,417:
         },
         },


         renderConfigPreview(container) {
         renderConfigPreview(
            container
        ) {
             if (container) {
             if (container) {
                 container.text(
                 container.text(
Line 3,669: Line 4,447:


         markDirty() {
         markDirty() {
             this.state.dirty = true;
             this.state.dirty =
                true;
         },
         },


        // =========================================================
         // Save
         // Save
        // =========================================================


         async savePreloadOnly() {
         async savePreloadOnly() {
Line 3,678: Line 4,459:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                    ''
                        ''
                 ).trim();
                 ).trim();


Line 3,688: Line 4,469:
                     );
                     );


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


                 title = result;
                 title = result;
                 this.state.preloadTitle =
                 this.state.preloadTitle =
                     title;
                     title;
Line 3,748: Line 4,532:
                     }
                     }
                 );
                 );
             } catch (error) {
             } catch (
                error
            ) {
                 console.error(
                 console.error(
                     'Preload Editor save:',
                     'Preload Editor save:',
Line 3,774: Line 4,560:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                    ''
                        ''
                 ).trim();
                 ).trim();


Line 3,784: Line 4,570:
                     );
                     );


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


                 title = result;
                 title = result;
                 this.state.preloadTitle =
                 this.state.preloadTitle =
                     title;
                     title;
Line 3,793: Line 4,582:
             this.state.preloadContent =
             this.state.preloadContent =
                 this.getEditorContent();
                 this.getEditorContent();
            /*
            * Ensure the latest editor content is
            * represented in the config before saving.
            */
            this.reanalyze();


             const validation =
             const validation =
Line 3,868: Line 4,663:
                     }
                     }
                 );
                 );
             } catch (error) {
             } catch (
                error
            ) {
                 console.error(
                 console.error(
                     'Preload Editor save:',
                     'Preload Editor save:',
Line 3,897: Line 4,694:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                    ''
                        ''
                 ).trim();
                 ).trim();


Line 3,908: Line 4,705:
             if (
             if (
                 title &&
                 title &&
                 !/\/Preload$/i.test(title)
                 !/\/Preload$/i.test(
                    title
                )
             ) {
             ) {
                 warnings.push(
                 warnings.push(
Line 3,952: Line 4,751:
                         content
                         content
                     )
                     )
                );
            /*
            * Synchronize configuration one last
            * time before validation.
            */
            this.state.config =
                this.prepareConfigForVariables(
                    this.state.config ||
                        this.emptyConfig(),
                    analysis
                 );
                 );


Line 3,983: Line 4,793:
                         depth--;
                         depth--;


                         if (depth < 0) {
                         if (
                            depth < 0
                        ) {
                             errors.push(
                             errors.push(
                                 'Found @endif without a matching @if.'
                                 'Found @endif without a matching @if.'
Line 3,996: Line 4,808:
                             'else'
                             'else'
                     ) {
                     ) {
                         if (depth === 0) {
                         if (
                            depth === 0
                        ) {
                             errors.push(
                             errors.push(
                                 '@' +
                                 '@' +
Line 4,027: Line 4,841:


             this.setStatus(
             this.setStatus(
                 validation.errors.join(' '),
                 validation.errors.join(
                    ' '
                ),
                 'error'
                 'error'
             );
             );


             mw.notify(
             mw.notify(
                 validation.errors.join('\n'),
                 validation.errors.join(
                    '\n'
                ),
                 {
                 {
                     type: 'error'
                     type: 'error'
Line 4,045: Line 4,863:
     $(function () {
     $(function () {
         if (
         if (
             mw.config.get('wgPageName') !==
             mw.config.get(
                'wgPageName'
            ) !==
             'Petit_Planet:Preload_Editor'
             'Petit_Planet:Preload_Editor'
         ) {
         ) {