Jump to content

User:DreamRimmer/DR Editor.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by DreamRimmer (talk | contribs) at 08:48, 8 March 2025 (create). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
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.
// <nowiki>
mw.loader.using(['mediawiki.diff', 'mediawiki.diff.styles', 'oojs-ui-core'], function () {
    if (
        !['view', 'edit', 'history'].includes(mw.config.get('wgAction')) ||
        mw.config.get('wgNamespaceNumber') < 0 ||
        mw.config.get('wgArticleId') < 1
    ) {
        return;
    }
    var DR = {};
    DR.pagename = mw.config.get('wgPageName');
    DR.contentmodel = null;
    DR.contentmodels = ['wikitext', 'text', 'sanitized-css', 'json', 'javascript', 'css', 'Scribunto'];
    function initEditor(section) {
        var queryParams = {
            action: 'query',
            titles: DR.pagename,
            prop: 'revisions',
            rvprop: ['content', 'contentmodel'],
            rvlimit: 1,
            format: 'json',
            formatversion: 2
        };
        if (typeof section !== 'undefined') {
            queryParams.rvsection = section;
        }
        (new mw.Api()).get(queryParams).done(function (response) {
            if (!response.query.pages[0].revisions) {
                mw.notify('Failed to load section content.', { type: 'error' });
                return;
            }
            DR.content = response.query.pages[0].revisions[0].content;
            DR.contentmodel = response.query.pages[0].revisions[0].contentmodel;
            if (!DR.contentmodels.includes(DR.contentmodel)) {
                mw.notify('Page content model is not a simple text-based one.', {
                    title: 'Unallowed content model',
                    type: 'error',
                    autoHide: true,
                    autoHideSeconds: 5,
                    tag: 'DR-notification'
                });
                return;
            }
            $('#mw-content-text').hide();
            DR.textarea = new OO.ui.MultilineTextInputWidget({
                value: DR.content,
                type: 'text',
                id: 'DR-textarea-div',
                inputId: 'DR-textarea'
            });
            DR.summaryInput = new OO.ui.TextInputWidget({
                placeholder: 'Edit summary',
                id: 'DR-summary',
                inputId: 'DR-summary-input'
            });

            DR.summaryField = new OO.ui.FieldLayout(DR.summaryInput, {
                label: 'Edit summary:',
                align: 'top'
            });
            DR.minorCheckbox = new OO.ui.CheckboxInputWidget({
                selected: false,
                id: 'DR-minor'
            });
            DR.minorField = new OO.ui.FieldLayout(DR.minorCheckbox, {
                label: 'Mark edit as minor',
                align: 'inline'
            });
            DR.saveButton = new OO.ui.ButtonWidget({
                label: 'Save',
                flags: ['primary', 'progressive'],
                classes: 'DR-buttons',
                id: 'DR-save'
            });
            DR.previewButton = new OO.ui.ButtonWidget({
                label: 'Preview',
                classes: 'DR-buttons',
                id: 'DR-preview'
            });
            DR.reviewButton = new OO.ui.ButtonWidget({
                label: 'Review Changes',
                classes: 'DR-buttons',
                id: 'DR-review'
            });
            DR.cancel = new OO.ui.ButtonWidget({
                label: 'Cancel',
                flags: ['destructive'],
                classes: 'DR-buttons',
                id: 'DR-cancel'
            });
            var $editorContainer = $('<div>')
                .attr('id', 'DR-main')
                .append(
                    $('<div>')
                        .attr('id', 'DR-output')
                        .css({
                            border: '1px solid #A2A9B1',
                            padding: '5px',
                            'margin-bottom': '10px',
                            display: 'none'
                        }),
                    DR.textarea.$element,
                    DR.summaryField.$element,
                    DR.minorField.$element,
                    $('<div>')
                        .attr('id', 'DR-buttons')
                        .css({
                            display: 'flex',
                            padding: '5px',
                            'justify-content': 'space-between',
                            'margin-top': '3px'
                        })
                );
            $('#mw-content-text').after($editorContainer);
            $('#DR-buttons').prepend(
                $('<div>').append(
                    DR.saveButton.$element,
                    DR.previewButton.$element,
                    DR.reviewButton.$element,
                    DR.cancel.$element
                )
            );
            $('#DR-textarea-div').css({ margin: 0, 'max-width': '100%' });
            $('#DR-textarea').css({
                'min-height': '300px',
                'min-width': '100%',
                resize: 'vertical',
                'font-size': 'small',
                'font-family': 'monospace, monospace'
            });
            $('#DR-summary').css({
                'margin-top': '3px',
                'max-width': '100%',
                width: '100%'
            });
            DR.saveButton.$element.click(function () {
                var newContent = $('#DR-textarea').val();
                var summary = (DR.summaryInput.getValue() || "") + " (using [[User:DreamRimmer/DR Editor.js|DR Editor.js]])";
                var editParams = {
                    action: 'edit',
                    title: DR.pagename,
                    text: newContent,
                    summary: summary
                };
                if (typeof section !== 'undefined') {
                    editParams.section = section;
                }
                if (DR.minorCheckbox.isSelected()) {
                    editParams.minor = true;
                }
                (new mw.Api())
                    .postWithEditToken(editParams)
                    .done(function (response) {
                        if (response.error && response.error.code === 'editconflict') {
                            var dialog = new OO.ui.MessageDialog();
                            var windowManager = new OO.ui.WindowManager();
                            $(document.body).append(windowManager.$element);
                            windowManager.addWindows([dialog]);
                            dialog.open({
                                title: 'Edit Conflict',
                                message: response.error.info || 'An edit conflict occurred. Please resolve it manually.'
                            });
                        } else if (response.edit && response.edit.result === 'Success') {
                            mw.notify('Page saved successfully!', {
                                title: 'Saved',
                                type: 'success',
                                autoHide: true,
                                autoHideSeconds: 5,
                                tag: 'DR-notification'
                            });
                            location.reload();
                        } else {
                            mw.notify('Error saving page.', {
                                title: 'Error',
                                type: 'error',
                                autoHide: true,
                                autoHideSeconds: 5,
                                tag: 'DR-notification'
                            });
                        }
                    })
                    .fail(function () {
                        mw.notify('Error saving page.', {
                            title: 'Error',
                            type: 'error',
                            autoHide: true,
                            autoHideSeconds: 5,
                            tag: 'DR-notification'
                        });
                    });
            });
            DR.previewButton.$element.click(function () {
                $('#DR-output')
                    .show()
                    .html('<img src="/media/wikipedia/commons/5/51/Ajax-loader4.gif" width="30" height="30" alt="Loading...">');
                var previewContent = $('#DR-textarea').val();
                new mw.Api()
                    .post({
                        action: 'parse',
                        text: previewContent,
                        title: 'Preview',
                        contentmodel: DR.contentmodel,
                        pst: true,
                        format: 'json'
                    })
                    .done(function (response) {
                        $('#DR-output').html(response.parse.text['*']);
                    })
                    .fail(function () {
                        $('#DR-output').html('<div style="color: red;">Error generating preview.</div>');
                    });
            });
            DR.reviewButton.$element.click(function () {
                $('#DR-output')
                    .show()
                    .html('<img src="/media/wikipedia/commons/5/51/Ajax-loader4.gif" width="30" height="30" alt="Loading...">');
                $.ajax({
                    url: mw.config.get('wgScriptPath') + '/api.php',
                    data: {
                        action: 'compare',
                        fromtitle: DR.pagename,
                        toslots: 'main',
                        'totext-main': $('#DR-textarea').val(),
                        format: 'json',
                        formatversion: 2
                    },
                    type: 'POST',
                    dataType: 'json',
                    success: function (response) {
                        var diffHtml = response.compare.body === ''
                            ? '<div>(No changes)</div>'
                            : '<table class="diff diff-editfont-monospace" style="margin: auto; font-size: small;">' +
                                '<colgroup>' +
                                '<col class="diff-marker">' +
                                '<col class="diff-content">' +
                                '<col class="diff-marker">' +
                                '<col class="diff-content">' +
                                '</colgroup>' +
                                '<tbody>' + response.compare.body + '</tbody>' +
                                '</table>';
                        $('#DR-output').html(diffHtml);
                        mw.hook('wikipage.diff').fire($('#DR-output'));
                    },
                    error: function () {
                        $('#DR-output').html('<div style="color: red;">Error generating diff.</div>');
                    }
                });
            });
            DR.cancel.$element.click(function () {
                $('#mw-content-text, #DR-main').toggle();
                $('#DR-main').remove();
            });
        }).fail(function (error) {
            mw.notify('API error: ' + error, { type: 'error' });
        });
    }
    $(document).ready(function () {
        var topBtn = $('<li>')
            .attr('id', 'DR-Edit-TopBtn')
            .append(
                $('<span>').append(
                    $('<a>')
                        .attr('href', '#')
                        .text('DR Editor')
                ).data({ number: -1, target: DR.pagename })
            );
        if (mw.config.get('skin') === 'minerva') {
            $(topBtn).css({ 'align-items': 'center', display: 'flex' });
            $(topBtn).find('span').addClass('page-actions-menu__list-item');
            $(topBtn)
                .find('a')
                .addClass('mw-ui-icon mw-ui-icon-element mw-ui-icon-wikimedia-edit-base20 mw-ui-icon-with-label-desktop')
                .css('vertical-align', 'middle');
        }
        if ($('#ca-edit').length > 0 && $('#DR-Edit-TopBtn').length === 0) {
            if (mw.config.get('skin') === 'minerva') {
                $('#ca-edit').parent().after(topBtn);
            } else {
                $('#ca-edit').after(topBtn);
            }
            $('#DR-Edit-TopBtn').click(function () {
                initEditor();
            });
        } else if ($('#ca-edit').length === 0) {
            console.error('fail_to_init_quickedit');
        }
    });
    $(async function () {
    	// Diff undo functionality from [[User:Nardog/DiffUndo.js]]
        let section = null;
        let dependencies = [
            'jquery.textSelection', 'oojs-ui-core',
            'oojs-ui.styles.icons-editing-core'
        ];
        await mw.loader.using(dependencies);
        mw.loader.addStyleTag('.diff > tbody > tr{position:relative} .diffundo{position:absolute;inset-inline-end:0;bottom:0} tr:not(:hover) > td > .diffundo:not(:focus-within){opacity:0} .diffundo-undone{text-decoration:line-through;opacity:0.5}');
        let idxMap = new WeakMap(), offset = 0, rev;
        let handler = button => {
            let $row = button.$element.closest('tr');
            let numRow = $row.prevAll().toArray().find(row => idxMap.has(row));
            if (!numRow) {
                mw.notify("Couldn't get the line number.", {
                    tag: 'diffundo',
                    type: 'error'
                });
                return;
            }
            let isUndone = $row.hasClass('diffundo-undone');
            let $toReplace = $row.children(isUndone ? '.diff-deletedline' : '.diff-addedline');
            let $toRestore = $row.children(isUndone ? '.diff-addedline' : '.diff-deletedline');
            let isInsert = !$toReplace.length;
            let isRemove = !$toRestore.length;
            let $midLines = $row.prevUntil(numRow).map(function () {
                return this.querySelector(
                    this.classList.contains('diffundo-undone')
                        ? ':scope > .diff-deletedline'
                        : ':scope > .diff-context, :scope > .diff-addedline'
                );
            });
            let lineIdx = idxMap.get(numRow) + $midLines.length;
            let $textarea = $('#DR-textarea');
            let lines = $textarea.textSelection('getContents').split('\n');
            let canUndo;
            if (isInsert) {
                canUndo = !$midLines.length ||
                    lines[lineIdx - 1] === $midLines[0].textContent;
            } else {
                canUndo = lines[lineIdx] === $toReplace.text();
            }
            if (!canUndo) {
                mw.notify('The line has been modified since the diff.', {
                    tag: 'diffundo',
                    type: 'warn'
                });
                return;
            }
            let coords = [window.scrollX, window.scrollY];
            let [start, end] = $textarea.textSelection('getCaretPosition', { startAndEnd: true });
            let beforeLen = lines.slice(0, lineIdx).join('').length + lineIdx;
            if (isRemove) {
                let toReplaceLen = lines[lineIdx].length;
                lines.splice(lineIdx, 1);
                [start, end] = [start, end].map(idx => {
                    if (idx > beforeLen + toReplaceLen) {
                        return idx - toReplaceLen - 1;
                    } else if (idx > beforeLen) {
                        return beforeLen;
                    }
                    return idx;
                });
                $row.nextAll().each(function () {
                    if (idxMap.has(this)) {
                        idxMap.set(this, idxMap.get(this) - 1);
                    }
                });
            } else if (isInsert) {
                let text = $toRestore.text();
                lines.splice(lineIdx, 0, text);
                [start, end] = [start, end].map(idx => {
                    if (idx > beforeLen) {
                        return idx + text.length + 1;
                    }
                    return idx;
                });
                $row.nextAll().each(function () {
                    if (idxMap.has(this)) {
                        idxMap.set(this, idxMap.get(this) + 1);
                    }
                });
            } else {
                let toReplaceLen = lines[lineIdx].length;
                let text = $toRestore.text();
                lines.splice(lineIdx, 1, text);
                [start, end] = [start, end].map(idx => {
                    if (idx > beforeLen + toReplaceLen) {
                        return idx - (toReplaceLen - text.length);
                    } else if (idx > beforeLen) {
                        return beforeLen;
                    }
                    return idx;
                });
            }
            $textarea.textSelection('setContents', lines.join('\n'));
            $textarea.textSelection('setSelection', { start, end })
                .textSelection('scrollToCaretPosition');
            $row.toggleClass('diffundo-undone', !isUndone);
            window.scrollTo(...coords);
            setTimeout(() => {
                button.focus();
            });
        };
        let updateOffset = async () => {
            if (rev) {
                let { query } = await new mw.Api().get({
                    action: 'query',
                    titles: mw.config.get('wgPageName'),
                    prop: 'info',
                    formatversion: 2
                });
                if (query.pages[0].lastrevid === rev) return;
            }
            let { parse } = await new mw.Api().get({
                action: 'parse',
                page: mw.config.get('wgPageName'),
                prop: 'revid|sections|wikitext',
                formatversion: 2
            });
            let charOffset = section ? parse.sections.find(s => s.index === section)?.byteoffset : 0;
            if (section && (charOffset === undefined || charOffset === null)) {
                mw.notify("Couldn't get the section offset.", {
                    tag: 'diffundo',
                    type: 'error'
                });
                return false;
            }
            offset = charOffset
                ? [...parse.wikitext].slice(0, charOffset - 1).join('').split('\n').length
                : 0;
            rev = parse.revid;
        };
        mw.hook('wikipage.diff').add(async $diff => {
            let $lineNums = $diff.find('.diff-lineno:last-child');
            if (!$lineNums.length || (section && (await updateOffset() === false || !$diff[0].isConnected))) {
                return;
            }
            $lineNums.each(function () {
                let num = this.textContent.replace(/\D/g, '');
                if (!num) return;
                idxMap.set(this.parentElement, num - 1 - offset);
            });
            $diff.find('.diff-addedline, .diff-empty.diff-side-added').append(() => {
                let button = new OO.ui.ButtonWidget({
                    classes: ['diffundo'],
                    framed: false,
                    icon: 'undo',
                    title: 'Undo this line'
                });
                return button.on('click', handler, [button]).$element;
            });
        });
    });
});
// </nowiki>