MediaWiki:Gadget-CargoHelperCore.js

From Angelina Jordan Wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Gadget-CargoHelperCore.js
   Heavy logic: dialog creation, Cargo queries, validation,
   insertion, form logic.
   Only loaded on demand by Gadget-CargoHelperLoader.js.
*/
(function () {
'use strict';

// Top-level CargoHelper object
window.CargoHelper = window.CargoHelper || {};

// ---------------------- Top-level helpers ----------------------

// Insert text at cursor in textarea
function insertAtCursor(textarea, newText) {
    var start = textarea.selectionStart;
    var end = textarea.selectionEnd;
    var original = textarea.value;
    textarea.value = original.slice(0, start) + newText + original.slice(end);
    textarea.selectionStart = textarea.selectionEnd = start + newText.length;
    textarea.scrollTop = textarea.scrollHeight;
}

// Sanitize a string for Lua-friendly ID
function luaCompatibleSanitize(title) {
    title = String(title || '');
    title = title.replace(/&[#\w]+;/g, '_');
    title = title.replace(/[^\w_.-]/g, '_'); // keep alnum, underscore, hyphen, dot
    return title;
}

// Compute next local numeric performance ID
function computeNextLocalId(perfs, page) {
    var base = luaCompatibleSanitize(page) + '_';
    var max = 0;
    perfs.forEach(function (p) {
        var id = p.perfID || '';
        if (id.indexOf(base) !== 0) return;
        var tail = id.slice(base.length);
        var m = tail.match(/^(\d+)/);
        if (!m) return;
        var n = parseInt(m[1], 10);
        if (!isNaN(n) && n > max) max = n;
    });
    return String(max + 1);
}

// Validate ISO-leading date
function isValidIsoLeadingDate(s) {
    if (!s) return false;
    var m = s.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:\s.*)?$/);
    if (!m) return false;
    if (m[2]) { var mm = parseInt(m[2], 10); if (mm < 1 || mm > 12) return false; }
    if (m[3]) { var dd = parseInt(m[3], 10); if (dd < 1 || dd > 31) return false; }
    return true;
}

// Validate URL
function isValidUrl(s) {
    if (!s) return false;
    return /^(https?:\/\/|\/\/).+/i.test(s.trim());
}

// ---------------------- Main initialization ----------------------

window.CargoHelper.ready = new Promise(function(resolve, reject) {

    mw.loader.using([
        'oojs-ui-core',
        'oojs-ui-windows',
        'oojs-ui-widgets',
        'mediawiki.util',
        'mediawiki.api'
    ]).then(function () {

        // --- shared helpers ---
        function isValidIsoLeadingDate(s) {
            if (!s) return false;
            var m = s.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:\s.*)?$/);
            if (!m) return false;
            if (m[2]) {
                var mm = parseInt(m[2], 10);
                if (mm < 1 || mm > 12) return false;
            }
            if (m[3]) {
                var dd = parseInt(m[3], 10);
                if (dd < 1 || dd > 31) return false;
            }
            return true;
        }
        function isValidUrl(s) {
            if (!s) return false;
            return /^(https?:\/\/|\/\/).+/i.test(s.trim());
        }
        function clearInvalid(widget) {
            if (!widget) return;
            if (widget.$input) widget.$input.css('outline', '').css('box-shadow', '');
            else if (widget.$element) widget.$element.css('outline', '').css('box-shadow', '');
        }
        function markInvalid(widget) {
            if (!widget) return;
            if (widget.$input) {
                widget.$input.css('outline', '2px solid #d33').css('box-shadow', '0 0 0 2px rgba(211,51,51,0.08)');
                widget.$input.one('input', function () { widget.$input.css('outline', '').css('box-shadow', ''); });
            } else if (widget.$element) {
                widget.$element.css('outline', '2px solid #d33').css('box-shadow', '0 0 0 2px rgba(211,51,51,0.08)');
                widget.$element.one('input', function () { widget.$element.css('outline', '').css('box-shadow', ''); });
            }
        }
        function ValidationDialog(config) { ValidationDialog.super.call(this, config); }
        function loadSections() {
            var cfg = mw.config.get('wgPageSections');
            if (Array.isArray(cfg) && cfg.length) return Promise.resolve(cfg);
            var api = new mw.Api();
            return api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'sections' })
                .then(function (data) {
                    var out = [];
                    if (data && data.parse && Array.isArray(data.parse.sections)) {
                        data.parse.sections.forEach(function (s) {
                            out.push(s);
                        });
                    }
                    return out;
                }).catch(function () { return []; });
        }

        //---------------------------------------------------------
        // 1) Submit handler
        //---------------------------------------------------------
        window.CargoHelper.handleSubmit = function (
            modeSelect,
            existingPerfDropdown,
            existingPerfs,
            eventInput,
            dateInput,
            contextCombo,
            typeInput,
            posInput,
            withInput,
            commentPInput,
            urlInput,
            durationInput,
            qualityInput,
            commentVInput,
            dlg,
            wm
        ) 

{
    try {
    	var DEFAULT_NEW_SECTION_TITLE = 'Latest added video/performance (unsorted)';

        [ eventInput, dateInput, contextCombo, urlInput, posInput, withInput, commentPInput, commentVInput ].forEach(clearInvalid);

        // gather values
        var page = mw.config.get('wgTitle');
        var mode = modeSelect.getValue();
        var pidLocal;

        if (mode === 'existing') {
            if (!existingPerfDropdown) { alert('No existing performance to choose.'); return; }
            var fullPerfID = existingPerfDropdown.getValue();
            if (!fullPerfID) { alert('Please select an existing performance.'); return; }
            var lastUnd = fullPerfID.lastIndexOf('_');
            pidLocal = (lastUnd >= 0) ? fullPerfID.slice(lastUnd + 1) : fullPerfID;
        } else {
            pidLocal = computeNextLocalId(existingPerfs || [], page);
        }

        // Validation
        var errors = [];
        var urlVal = urlInput.getValue() || '';
        if (!isValidUrl(urlVal)) { errors.push('Video URL is required and must start with "http://", "https://" or "//".'); markInvalid(urlInput); }

        var perfDate = dateInput.getValue() || '';
        if (mode === 'new') {
            if (!perfDate) { errors.push('Date is required when creating a new performance.'); markInvalid(dateInput); }
            else if (!isValidIsoLeadingDate(perfDate)) { errors.push('Date must start with an ISO date (YYYY, YYYY-MM or YYYY-MM-DD).'); markInvalid(dateInput); }
        }

        var ctxVal = contextCombo.getValue() || '';
        if (mode === 'new' && !ctxVal) { errors.push('Context is required when creating a new performance.'); markInvalid(contextCombo); }

        if (errors.length) {
            // show dialog with errors
            OO.inheritClass(ValidationDialog, OO.ui.MessageDialog);
            ValidationDialog.static.name = 'ValidationDialog';
            ValidationDialog.static.title = 'Validation error';
            ValidationDialog.static.actions = [ { action: 'close', label: 'Close', flags: ['primary'] } ];
            var wmVal = new OO.ui.WindowManager();
            $(document.body).append(wmVal.$element);
            var dlgVal = new ValidationDialog();
            wmVal.addWindows([dlgVal]);
            wmVal.openWindow(dlgVal, { message: errors.join('\n') });
            // focus first invalid
            var toFocus = null;
            if (!isValidUrl(urlVal)) toFocus = urlInput;
            else if (mode === 'new' && (!perfDate || !isValidIsoLeadingDate(perfDate))) toFocus = dateInput;
            else if (mode === 'new' && !ctxVal) toFocus = contextCombo;
            if (toFocus) { setTimeout(function () { if (toFocus.focus) toFocus.focus(); else if (toFocus.$input) toFocus.$input.focus(); }, 50); }
            return;
        }

        // Build perf & video blocks single-line
        var perfBlock = '';
        if (mode === 'new') {
            var perfFields = [];
            if (page) perfFields.push('song=' + page);
            if (eventInput.getValue()) perfFields.push('event=' + eventInput.getValue());
            if (contextCombo.getValue()) perfFields.push('context=' + contextCombo.getValue());
            if (dateInput.getValue()) perfFields.push('date=' + dateInput.getValue());
            if (typeInput.getValue()) perfFields.push('type=' + typeInput.getValue());
            if (posInput.getValue()) perfFields.push('pos=' + posInput.getValue());
            if (withInput.getValue()) perfFields.push('with=' + withInput.getValue());
            if (commentPInput.getValue()) perfFields.push('comment=' + commentPInput.getValue());
            if (pidLocal) perfFields.push('id=' + pidLocal);
            perfBlock = '{{' + 'Performance|' + perfFields.join('|') + '}}';
        }

        var videoFields = [];
        if (pidLocal) videoFields.push('pid=' + pidLocal);
        if (urlInput.getValue()) videoFields.push('url=' + urlInput.getValue());
        if (durationInput.getValue()) videoFields.push('duration=' + durationInput.getValue());
        if (qualityInput.getValue()) videoFields.push('quality=' + qualityInput.getValue());
        if (commentVInput.getValue()) videoFields.push('comment=' + commentVInput.getValue());
        var videoBlock = '{{' + 'Video|' + videoFields.join('|') + '}}';

        var combinedForInsert = (mode === 'new' ? '* ' + perfBlock + '\n*: ' : '') + videoBlock;

        // If edit textarea present -> insert at cursor
        var $textarea = $('#wpTextbox1');
        if ($textarea.length) {
            var ta = $textarea[0];
            var start = ta.selectionStart;
            var textBefore = ta.value.slice(0, start);
            var atLineStart = textBefore.endsWith('\n') || textBefore === '';
            var insertText;
            if (atLineStart) insertText = (mode === 'new' ? '* ' + perfBlock + '\n*: ' : '') + videoBlock;
            else insertText = (mode === 'new' ? perfBlock : '') + videoBlock;
            insertAtCursor(ta, insertText);
            wm.closeWindow(dlg);
            return;
        }

        // Otherwise on view page -> ask for section, then API insert
        loadSections().then(function (sections) {

			// ------------------------- Section Selection Dialog -------------------------
			function openSectionSelectDialog(combinedForInsert,avdlg) {
			    var DEFAULT_NEW_SECTION_TITLE = 'Latest added video/performance (unsorted)';
			    var page = mw.config.get('wgPageName');
			
			    // Fetch sections
			    function loadSections() {
			        var cfg = mw.config.get('wgPageSections');
			        if (Array.isArray(cfg) && cfg.length) return Promise.resolve(cfg);
			        var api = new mw.Api();
			        return api.get({
			            action: 'parse',
			            page: page,
			            prop: 'sections'
			        }).then(function(data) {
			            if (data && data.parse && Array.isArray(data.parse.sections)) {
			                return data.parse.sections;
			            }
			            return [];
			        }).catch(() => []);
			    }
			
			    loadSections().then(function(sections) {
			
			        // -------------------
			        // UI widgets
			        // -------------------
			        var selectWidget = new OO.ui.DropdownInputWidget({
			            options: sections.map(function(s) {
			                var n = String(s.index || s.number || s.section || '');
			                var label = (s.line || s.heading || '').toString() || ('Section ' + n);
			                return { data: n, label: n + ' — ' + label };
			            }).concat([
			                { data: 'new', label: 'End of article (create new section)' }
			            ]),
			            value: 'new'
			        });
			
			        var titleInput = new OO.ui.TextInputWidget({
			            value: DEFAULT_NEW_SECTION_TITLE
			        });
			
			        var fieldset = new OO.ui.FieldsetLayout({ label: 'Insert at end of' });
			
			        fieldset.addItems([
			            new OO.ui.FieldLayout(selectWidget, {
			                label: 'Target section'
			            }),
			            new OO.ui.FieldLayout(titleInput, {
			                label: 'New section title (only used for new section)',
			                align: 'top'
			            })
			        ]);
			
			        // -----------------------
			        // Button row (identical structure as AddVideoDialog)
			        // -----------------------
			        var cancelButton = new OO.ui.ButtonWidget({
			            label: 'Cancel',
			            flags: ['destructive']
			        });
			
			        var insertButton = new OO.ui.ButtonWidget({
			            label: 'Insert',
			            flags: ['primary']
			        });
			
			        var buttonRow = $('<div>').css({
			            marginTop: '1em',
			            marginBottom: '1em',
			            textAlign: 'right'
			        })
			        .append(cancelButton.$element)
			        .append(' ')
			        .append(insertButton.$element);
			
			        fieldset.$element.append(buttonRow);
			
			        // -----------------------
			        // Panel (this gives the correct left/right padding)
			        // -----------------------
			        var panel = new OO.ui.PanelLayout({
			            expanded: true,
			            padded: true,
			            content: [ fieldset ]
			        });
			
			        // -----------------------
			        // Dialog definition
			        // -----------------------
			        function SectionDialog(config) {
			            SectionDialog.super.call(this, config);
			        }
			        OO.inheritClass(SectionDialog, OO.ui.ProcessDialog);
			
			        SectionDialog.static.name  = 'SectionDialog';
			        SectionDialog.static.title = 'Choose insertion location';
			        SectionDialog.static.size  = 'medium';
			
			        // No actions -> we use manual footer identical to AddVideoDialog
			        SectionDialog.static.actions = [];
			
			        SectionDialog.prototype.initialize = function () {
			            SectionDialog.super.prototype.initialize.call(this);
			            this.$body.append(panel.$element);
			
			            // Same CSS override as AddVideoDialog
			            this.$element.css({
			                width: '900px',
			                maxWidth: '95%',
			                left: '50%',
			                transform: 'translateX(-50%)'
			            });
			        };
			
			        var wm = new OO.ui.WindowManager();
			        $(document.body).append(wm.$element);
			
			        var dlg = new SectionDialog();
			        wm.addWindows([dlg]);
			        wm.openWindow(dlg);
			
			        // -----------------------
			        // Button actions
			        // -----------------------
			        cancelButton.on('click', function () {
			            wm.closeWindow(dlg);
			        });
			
					insertButton.on('click', function () {
					    var chosen = selectWidget.getValue();
					    var title = (titleInput.getValue() || '').trim() || DEFAULT_NEW_SECTION_TITLE;
					    var api = new mw.Api();
					    var appendText = combinedForInsert;
					
					    api.postWithToken('csrf', {
					        action: 'edit',
					        title: page,
					        section: chosen === 'new' ? 'new' : chosen,
					        appendtext: '\n' + appendText + '\n',
					        summary: chosen === 'new' ? title : 'Add video/performance entry via CargoHelper'
					    }).then(function () {
					        // Close Section dialog
					        wm.closeWindow(dlg);
					
					        // Close AddVideo dialog (passed as avdlg)
					        if (avdlg && avdlg.manager) {
					            avdlg.manager.closeWindow(avdlg);
					        }
					    }).catch(function () {
					        // Still close Section dialog on failure
					        wm.closeWindow(dlg);
					
					        // Still close AddVideo dialog
					        if (avdlg && avdlg.manager) {
					            avdlg.manager.closeWindow(avdlg);
					        }
					    });
					});
			    });
			}

		openSectionSelectDialog(combinedForInsert,dlg);
        }).catch(function (err) {
            console.error('Failed to load sections:', err);
            alert('Failed to load sections (see console).');
        });

    } catch (err) {
        console.error('Submit handler failed:', err);
        alert('Error while preparing insertion (see console).');
    }
};

        //---------------------------------------------------------
        // 2) Open dialog function
        //---------------------------------------------------------
        window.CargoHelper.openDialog = function () {

            //---------------------------------------------------------
            // Helpers: fetch Cargo data
            //---------------------------------------------------------
            function fetchPerformancesForPage() {
                var page = mw.config.get('wgPageName') || '';
                page = page.replace(/_/g, ' ');
                page = $('<textarea/>').html(page).text();
                var api = new mw.Api();
                return api.get({
                    action: 'cargoquery',
                    tables: 'Performances',
                    fields: 'perfID,event,date,contextstr',
                    where: 'song="' + page.replace(/"/g, '\\"') + '"',
                    order_by: 'perfID',
                    limit: 5000
                }).then(function (data) {
                    if (!data.cargoquery) return [];
                    return data.cargoquery.map(r => r.title || {}).map(t => ({
                        perfID: t.perfID || '',
                        event: t.event || '',
                        date: t.date || '',
                        contextstr: t.contextstr || ''
                    }));
                }).catch(function (err) { console.error('fetchPerformancesForPage failed:', err); return []; });
            }

            function fetchAllContexts() {
                var api = new mw.Api();
                return api.get({
                    action: 'cargoquery',
                    tables: 'Performances',
                    fields: 'contextstr',
                    group_by: 'contextstr',
                    order_by: 'contextstr',
                    limit: 5000
                }).then(function (data) {
                    if (!data.cargoquery) return [];
                    return data.cargoquery.map(r => (r.title && r.title.contextstr) ? r.title.contextstr : '').filter(Boolean);
                }).catch(function (err) { console.error('fetchAllContexts failed:', err); return []; });
            }

            //---------------------------------------------------------
            // 3) Open dialog after fetching data
            //---------------------------------------------------------
            Promise.all([fetchPerformancesForPage(), fetchAllContexts()])
            .then(function (res) {
                var existingPerfsArray = res[0] || [];
                var allContexts = res[1] || [];
                openDialog(existingPerfsArray, allContexts);
            }).catch(function (err) {
                console.error('CargoHelperCore: failed to load data: ', err);
                alert('Failed to load data (see console).');
            });

            //---------------------------------------------------------
            // 4) Actual OOUI dialog UI
            //---------------------------------------------------------

			function openDialog(existingPerfs, allContexts) {
			
			    /************************************************************
			     *  WIDGET DEFINITIONS
			     ************************************************************/
			
			    var modeSelect = new OO.ui.DropdownInputWidget({
			        options: [
			            { data: 'existing', label: 'Use existing performance' },
			            { data: 'new', label: 'Create new performance' }
			        ],
			        value: existingPerfs.length ? 'existing' : 'new'
			    });
			
			    var existingPerfDropdown = null;
			    if (existingPerfs.length) {
			        existingPerfDropdown = new OO.ui.DropdownInputWidget({
			            options: existingPerfs.map(function (p) {
			                var localID = p.perfID.replace(/^.*_/, '');
			                return { data: p.perfID, label: localID + ' — ' + (p.event || '') + ' (' + (p.date || '') + ')' };
			            }),
			            placeholder: 'Choose existing performance'
			        });
			    }
			
			    // PERFORMANCE FIELDS
			    var eventInput    = new OO.ui.TextInputWidget();
			    var dateInput     = new OO.ui.TextInputWidget();
			
			    // CONTEXT: ComboBox with dynamic substring filter
			    var contextCombo = new OO.ui.ComboBoxInputWidget({
			        options: allContexts.map(function (c) { return { data: c, label: c }; }),
			        placeholder: 'Type or pick a full context string'
			    });
			
			    // Keep original as the master list
			    var originalContextOptions = allContexts.map(function (c) {
			        return { data: c, label: c };
			    });
			
				function restoreAllContextOptions() {
				    // If ComboBox supports clearOptions/addOption (some OOUI versions), use them
				    if (typeof contextCombo.clearOptions === 'function' && typeof contextCombo.addOption === 'function') {
				        contextCombo.clearOptions();
				        originalContextOptions.forEach(function (opt) {
				            contextCombo.addOption({ data: opt.data, label: opt.label });
				        });
				    } else {
				        // Fallback: rebuild the internal menu only (works across OOUI versions)
				        var menu = contextCombo.getMenu();
				        if (menu && typeof menu.clearItems === 'function' && typeof menu.addItems === 'function') {
				            menu.clearItems();
				            originalContextOptions.forEach(function (opt) {
				                menu.addItems([
				                    new OO.ui.MenuOptionWidget({
				                        data: opt.data,
				                        label: opt.label
				                    })
				                ]);
				            });
				        }
				    }
				
				    // Ensure the visible input text still matches the underlying value (if any)
				    var cur = contextCombo.getValue();
				    if (cur) {
				        // sometimes ComboBox needs the raw input set as well
				        if (contextCombo.$input && contextCombo.$input.length) {
				            contextCombo.$input.val(cur);
				        }
				    }
				}
			
			    // TYPE
			    var typeInput = new OO.ui.DropdownInputWidget({
			        options: [
			            { data: '', label: '—' },
			            { data: 'recording', label: 'recording' },
			            { data: 'track', label: 'track' },
			            { data: 'music-video', label: 'music-video' },
			            { data: 'live', label: 'live' },
			            { data: 'rehearsal', label: 'rehearsal' },
			            { data: 'soundcheck', label: 'soundcheck' },
			            { data: 'sing-along', label: 'sing-along' },
			            { data: 'lip-sync', label: 'lip-sync' }
			        ]
			    });
			
			    var posInput      = new OO.ui.TextInputWidget();
			    var withInput     = new OO.ui.TextInputWidget({ multiline: true });
			    var commentPInput = new OO.ui.TextInputWidget({ multiline: true });
			
			    // VIDEO
			    var urlInput      = new OO.ui.TextInputWidget();
			    var durationInput = new OO.ui.DropdownInputWidget({
			        options: [
			            { data: '', label: '—' },
			            { data: 'full', label: 'full' },
			            { data: 'short', label: 'short' },
			            { data: 'fragment', label: 'fragment' }
			        ]
			    });
			    var qualityInput  = new OO.ui.DropdownInputWidget({
			        options: [
			            { data: '', label: '—' },
			            { data: 'best', label: 'best' },
			            { data: 'good', label: 'good' },
			            { data: 'acceptable', label: 'acceptable' },
			            { data: 'poor', label: 'poor' }
			        ]
			    });
			    var commentVInput = new OO.ui.TextInputWidget({ multiline: true });
			
			    /************************************************************
			     *  CONTEXT FILTERING (fix)
			     ************************************************************/
				// Use the real input event and rebuild the internal OOUI menu reliably
				contextCombo.$input.on('input', function () {
				    var value = (contextCombo.getValue() || '').toLowerCase();
				    var menu = contextCombo.getMenu();
				
				    // Rebuild menu items from the master list
				    menu.clearItems();
				
				    originalContextOptions.forEach(function (opt) {
				        if (opt.data.toLowerCase().indexOf(value) !== -1) {
				            menu.addItems([
				                new OO.ui.MenuOptionWidget({
				                    data: opt.data,
				                    label: opt.label
				                })
				            ]);
				        }
				    });
				
				    // Ensure the dropdown is visible so the user sees the filtered items
				    if (!menu.isVisible()) {
				        menu.toggle(true);
				    }
				});
			
			    /************************************************************
			     *  FORM LAYOUT
			     ************************************************************/
			
			    var form = new OO.ui.FieldsetLayout({ label: 'Add Video / Performance' });
			
			    form.addItems([ new OO.ui.FieldLayout(modeSelect, { label: 'Mode' }) ]);
			
			    if (existingPerfDropdown) {
			        form.addItems([
			            new OO.ui.FieldLayout(existingPerfDropdown, { label: 'Existing performance', align: 'top' })
			        ]);
			    }
			
			    form.addItems([
			        new OO.ui.FieldLayout(eventInput,   { label: 'Event' }),
			        new OO.ui.FieldLayout(dateInput,    { label: 'Date' }),
			        new OO.ui.FieldLayout(contextCombo, { label: 'Context (complete string, # separated)', align: 'top' }),
			        new OO.ui.FieldLayout(typeInput,    { label: 'Type' }),
			        new OO.ui.FieldLayout(posInput,     { label: 'Position' }),
			        new OO.ui.FieldLayout(withInput,    { label: 'With (partners)' }),
			        new OO.ui.FieldLayout(commentPInput,{ label: 'Performance comment' }),
			
			        // Separator
			        new OO.ui.LabelWidget({ label: $('<hr>') }),
			
			        new OO.ui.FieldLayout(urlInput,     { label: 'Video URL' }),
			        new OO.ui.FieldLayout(durationInput,{ label: 'Duration' }),
			        new OO.ui.FieldLayout(qualityInput, { label: 'Quality' }),
			        new OO.ui.FieldLayout(commentVInput,{ label: 'Video comment' })
			    ]);
			
			    var submitButton = new OO.ui.ButtonWidget({ label: 'Insert', flags: ['primary'] });
			    var cancelButton = new OO.ui.ButtonWidget({ label: 'Cancel', flags: ['destructive'] });
			
			    form.$element.append(
			        $('<div>').css({ marginTop: '1em', marginBottom: '1em', textAlign: 'right' })
			            .append(cancelButton.$element)
			            .append(' ')
			            .append(submitButton.$element)
			    );
			
			    var panel = new OO.ui.PanelLayout({ expanded: true, padded: true, content: [ form ] });
			
			    /************************************************************
			     *  DIALOG CONSTRUCTION
			     ************************************************************/
			
			    function AddVideoDialog(config) {
			        AddVideoDialog.super.call(this, config);
			    }
			    OO.inheritClass(AddVideoDialog, OO.ui.ProcessDialog);
			
			    AddVideoDialog.static.name  = 'AddVideoDialog';
			    AddVideoDialog.static.title = 'Add Video / Performance';
			    AddVideoDialog.static.size  = 'medium';
			
			    AddVideoDialog.prototype.initialize = function () {
			        AddVideoDialog.super.prototype.initialize.call(this);
			        this.$body.append(panel.$element);
			        this.$element.css({
			            width: '900px',
			            maxWidth: '95%',
			            left: '50%',
			            transform: 'translateX(-50%)'
			        });
			    };
			
			    var wm = new OO.ui.WindowManager();
			    $(document.body).append(wm.$element);
			    var dlg = new AddVideoDialog();
			    wm.addWindows([dlg]);
			    wm.openWindow(dlg);
			
			    cancelButton.on('click', function () {
			        wm.closeWindow(dlg);
			    });
			
			    /************************************************************
			     *  MODE SWITCHING (fixed with context restore/readOnly)
			     ************************************************************/
			
			    function updateFields() {
			        var isExisting = (modeSelect.getValue() === 'existing');
			
			        // PERFORMANCE FIELDS read-only
			        eventInput.setReadOnly(isExisting);
			        dateInput.setReadOnly(isExisting);
			        posInput.setReadOnly(isExisting);
			        withInput.setReadOnly(isExisting);
			        commentPInput.setReadOnly(isExisting);
			        typeInput.setDisabled(isExisting); // dropdown cannot be readonly
			
			        // CONTEXT special handling
			        if (isExisting) {
			            contextCombo.setReadOnly(true);
			
			            if (existingPerfDropdown) {
			                var id   = existingPerfDropdown.getValue();
			                var perf = existingPerfs.find(p => p.perfID === id) || {};
			
			                eventInput.setValue(perf.event || '');
			                dateInput.setValue(perf.date || '');
			
			                restoreAllContextOptions();
			                contextCombo.setValue(perf.contextstr || '');
			
			                typeInput.setValue(perf.type || '');
			                posInput.setValue(perf.pos || '');
			                withInput.setValue(perf.with || '');
			                commentPInput.setValue(perf.comment || '');
			            }
			        } else {
					    // NEW PERFORMANCE MODE
					
					    // Make fields editable again
					    eventInput.setReadOnly(false);
					    dateInput.setReadOnly(false);
					    posInput.setReadOnly(false);
					    withInput.setReadOnly(false);
					    commentPInput.setReadOnly(false);
					    typeInput.setDisabled(false);
					
					    // Restore context dropdown and allow typing/filtering
					    contextCombo.setReadOnly(false);
					    restoreAllContextOptions();
					
					    // CLEAR ALL PERFORMANCE-RELATED FIELDS (new behavior)
					    eventInput.setValue('');
					    dateInput.setValue('');
					    contextCombo.setValue('');
					    contextCombo.$input.val('');   // must clear raw input too
					    typeInput.setValue('');
					    posInput.setValue('');
					    withInput.setValue('');
					    commentPInput.setValue('');
					}
			
			        // Disable/enable existing perf dropdown
			        if (existingPerfDropdown) {
			            existingPerfDropdown.setDisabled(!isExisting);
			        }
			    }
			
			    modeSelect.on('change', updateFields);
			    if (existingPerfDropdown) {
			        existingPerfDropdown.on('change', updateFields);
			    }
			
			    updateFields();
			
			    /************************************************************
			     *  SUBMIT HANDLER (unchanged)
			     ************************************************************/
			    submitButton.on('click', function () {
			        try {
			            window.CargoHelper.handleSubmit(
			                modeSelect,
			                existingPerfDropdown,
			                existingPerfs,
			                eventInput,
			                dateInput,
			                contextCombo,
			                typeInput,
			                posInput,
			                withInput,
			                commentPInput,
			                urlInput,
			                durationInput,
			                qualityInput,
			                commentVInput,
			                dlg,
			                wm
			            );
			        } catch (err) {
			            console.error('Submit handler failed:', err);
			            alert('Error while preparing insertion (see console).');
			        }
			    });
			
			} // end openDialog
        };

        // Initialization done
        resolve(window.CargoHelper);

    }).catch(function(err) { reject(err); });

}); // mw.loader.using

}());