Jump to content

User:L235/formFiller.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by L235 (talk | contribs) at 04:12, 27 May 2025 (updates). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*  **JSON schema (examples)**

    Object‑key style:
    {
      "Special:BlankPage/form": {
        "title": "Demo survey",
        "instructions": "<p>Please fill the survey. Fields marked required.</p>",
        "targetPage": "User:L235/sandbox2",
        "template": { "name": "User:L235/TestTemplate", "subst": true },
        "questions": [
          { "label": "Question A", "name": "q1", "type": "text", "templateParam": "1", "default": "foo" },
          { "label": "Question B", "name": "q2", "type": "textarea", "required": true, "templateParam": "2" },
          { "type": "heading", "text": "Choices" },
          { "label": "Question C", "name": "q3", "type": "dropdown", "options": ["apples", "bananas"], "templateParam": "3", "default": "bananas" },
          { "label": "Question D", "name": "q4", "type": "checkbox", "options": ["cats", "dogs"], "templateParam": "4", "default": ["cats"] },
          { "label": "Question E", "name": "q5", "type": "radio", "options": ["noun", "verb"], "required": true, "templateParam": "5", "default": "verb" },
          { "label": "Section title", "name": "q6", "type": "text", "required": true, "templateParam": "6" },
          { "type": "static", "html": "<em>Thanks for participating!</em>" }
        ]
      }
    }
*/
/* global mw, $ */
(function () {
    var CONFIG_PAGE = 'User:L235/form-config.json';

    // ------------------------------------------------------------------
    // Cross‑browser CSS.escape polyfill (only if missing)
    if (!window.CSS || !CSS.escape) {
        (function (global) {
            var ESC_RE = /[\0-\u001F\u007F-\u009F\u00A0-\uFFFF]/g;
            var REPL = function (c) { return '\\' + c.charCodeAt(0).toString(16) + ' '; };
            global.CSS = global.CSS || {};
            global.CSS.escape = function (val) {
                return String(val).replace(ESC_RE, REPL).replace(/^(?:-?\d)/, '\\$&');
            };
        })(window);
    }

    mw.loader.using(['mediawiki.api', 'oojs-ui']).then(function () {
        var api = new mw.Api();

        /* ---------- helper: parse wikitext -> safe HTML -------------- */
        function parseWikitext(wt) {
            return api.post({
                action: 'parse',
                text: wt || '',
                contentmodel: 'wikitext',
                wrapoutputclass: ''
            }).then(function (d) {
                return d.parse.text['*'];
            }).catch(function () {
                // Never inject raw fallback – escape instead
                return $('<div>').text(wt || '').prop('outerHTML');
            });
        }

        /* ---------- helper: escape template parameters --------------- */
        function encodeParam(val) {
            // Ensure string, escape HTML special chars, preserve newlines
            return mw.html.escape(String(val || '')).replace(/\n/g, '&#10;');
        }

        /* ---------- 1. Load JSON config ------------------------------ */
        api.get({
            action: 'query', prop: 'revisions', titles: CONFIG_PAGE,
            rvprop: 'content', formatversion: 2
        }).then(function (data) {
            var page = data.query.pages[0];
            if (!page.revisions) {
                console.error('[MultiForm] Config page missing or empty');
                return;
            }
            var raw = page.revisions[0].content;
            var cfg;
            try { cfg = JSON.parse(raw); }
            catch (e) { console.error('[MultiForm] JSON parse error:', e); return; }

            var current = mw.config.get('wgPageName').replace(/_/g, ' ');
            var formCfg = matchForm(cfg, current);
            if (formCfg) renderForm(formCfg);
        }).fail(function (err) { console.error('[MultiForm] API error:', err); });

        /* ---------- helper: find config for this page ---------------- */
        function matchForm(cfg, page) {
            if (Array.isArray(cfg)) return cfg.find(function (f) { return f.formPage === page; });
            if (cfg[page]) return cfg[page];
            return Object.values(cfg).find(function (f) { return f.formPage === page; });
        }

        /* ---------- 2. Render form ----------------------------------- */
        function renderForm(cfg) {
            var $content = $('#mw-content-text').empty();
            if (cfg.title) $content.append($('<h2>').text(cfg.title));

            var promises = [];
            if (cfg.instructions) {
                promises.push(parseWikitext(cfg.instructions).then(function (html) { $content.append($(html)); }));
            }

            Promise.all(promises).then(function () {
                var $form = $('<form>').appendTo($content);
                (cfg.questions || []).forEach(function (q) { insertItem($form, q); });

                $form.append('<br>');
                var $submit = $('<input>').attr({ type: 'submit', value: 'Submit' });
                $form.append($submit);
                $form.on('submit', function (e) {
                    e.preventDefault();
                    submit($form, cfg, $submit);
                });
            });
        }

        /* ---------- insert question or static block ------------------ */
        function insertItem($form, q) {
            switch (q.type) {
                case 'heading':
                    $form.append($('<h3>').text(q.text));
                    return;
                case 'static':
                case 'html':
                    var $ph = $('<div class="multiform-placeholder"></div>');
                    $form.append($ph); // preserves ordering
                    parseWikitext(q.html || q.text || '').then(function (html) {
                        $ph.replaceWith($(html));
                    });
                    return;
            }

            var $label = $('<label>').text(q.label + (q.required ? ' (required)' : ''));
            var $field;

            switch (q.type) {
                case 'text':
                    $field = $('<input>').attr({ type: 'text', name: q.name, size: 40, value: q.default || '' });
                    break;
                case 'textarea':
                    $field = $('<textarea>').attr({ name: q.name, rows: 4, cols: 60 }).val(q.default || '');
                    break;
                case 'dropdown':
                    $field = $('<select>').attr('name', q.name);
                    (q.options || []).forEach(function (opt) {
                        var $o = $('<option>').val(opt).text(opt);
                        if (opt === q.default) $o.prop('selected', true);
                        $field.append($o);
                    });
                    break;
                case 'checkbox':
                    $field = $('<span>');
                    var defs = Array.isArray(q.default) ? q.default : (q.default ? [q.default] : []);
                    (q.options || []).forEach(function (opt) {
                        var $l = $('<label>');
                        var $cb = $('<input>').attr({ type: 'checkbox', name: q.name, value: opt });
                        if (defs.includes(opt)) $cb.prop('checked', true);
                        $l.append($cb, ' ', opt, '\u00A0');
                        $field.append($l);
                    });
                    break;
                case 'radio':
                    $field = $('<span>');
                    (q.options || []).forEach(function (opt) {
                        var $l = $('<label>');
                        var $rb = $('<input>').attr({ type: 'radio', name: q.name, value: opt });
                        if (q.required) $rb.attr('required', true);
                        if (opt === q.default) $rb.prop('checked', true);
                        $l.append($rb, ' ', opt, '\u00A0');
                        $field.append($l);
                    });
                    break;
                default:
                    console.warn('[MultiForm] Unsupported field type:', q.type);
                    return;
            }
            if (q.required && !['radio', 'checkbox'].includes(q.type)) $field.attr('required', true);
            $form.append($label.append($field)).append('<br><br>');
        }

        /* ---------- 3. Submission ------------------------------------ */
        function valueOf($form, q) {
            var sel = '[name="' + CSS.escape(q.name) + '"]';
            switch (q.type) {
                case 'checkbox':
                    return $form.find(sel + ':checked').map(function () { return this.value; }).get().join(', ');
                case 'radio':
                    return $form.find(sel + ':checked').val() || '';
                default:
                    return ($form.find(sel).val() || '').trim();
            }
        }

        function submit($form, cfg, $submit) {
            // Custom validation for required checkbox groups
            var missing = (cfg.questions || []).filter(function (q) {
                if (!q.required || !q.name) return false;
                var val = valueOf($form, q);
                return !val; // empty string means nothing selected
            });

            if (missing.length) {
                alert('Please complete required fields: ' + missing.map(function (q) { return q.label; }).join(', '));
                return;
            }

            var params = (cfg.questions || []).filter(function (q) { return q.templateParam; })
                .map(function (q) { return '|' + q.templateParam + '=' + encodeParam(valueOf($form, q)); }).join('');
            var tpl = cfg.template.name || cfg.template;
            if (cfg.template && cfg.template.subst) tpl = 'subst:' + tpl;
            var wikitext = '\n{{' + tpl + params + '}}\n';

            $submit.prop('disabled', true).val('Submitting…');
            api.postWithToken('csrf', {
                action: 'edit', title: cfg.targetPage, appendtext: wikitext,
                summary: cfg.editSummary || 'Append answers via multi‑form script'
            }).done(function () {
                mw.notify('Saved!', { type: 'success' });
                $form[0].reset();
            }).fail(function (err) {
                console.error('[MultiForm] Edit error:', err);
                mw.notify('Error: ' + err, { type: 'error', autoHide: false });
            }).always(function () {
                $submit.prop('disabled', false).val('Submit');
            });
        }
    });
})();