Jump to content

User:Fotis A./FastEdit.js

From Meta, a Wikimedia project coordination 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.
// 
$(() => {
    if (
        !['view', 'edit', 'history'].includes(mw.config.get('wgAction')) ||
        mw.config.get('wgNamespaceNumber') < 0
    ) {
        return;
    }

    if (mw.config.get('wgAction') === 'edit') {
        return;
    }

    const CODE_MIRROR_MODE_MAP = {
        wikitext: 'mediawiki',
        javascript: 'javascript',
        json: 'json',
        css: 'css',
        'sanitized-css': 'css',
        Scribunto: 'lua'
    };

    async function loadCodeMirrorForWikitext(textareaEl) {
        const requireModule = await mw.loader.using(['ext.CodeMirror', 'ext.CodeMirror.mode.mediawiki']);
        const CodeMirrorClass = requireModule('ext.CodeMirror');
        const { mediawiki } = requireModule('ext.CodeMirror.mode.mediawiki');
        const instance = new CodeMirrorClass(textareaEl, mediawiki());
        instance.initialize();
        return instance;
    }

    async function loadCodeMirrorForMode(textareaEl, modeKey) {
        const requireModule = await mw.loader.using(['ext.CodeMirror', 'ext.CodeMirror.modes']);
        const CodeMirrorClass = requireModule('ext.CodeMirror');
        const modes = requireModule('ext.CodeMirror.modes');
        const modeFn = modes[modeKey];
        if (!modeFn) {
            return null;
        }
        const instance = new CodeMirrorClass(textareaEl, modeFn());
        instance.initialize();
        return instance;
    }

    function computeMinimalChange(oldStr, newStr) {
        const minLen = Math.min(oldStr.length, newStr.length);
        let start = 0;
        while (start < minLen && oldStr[start] === newStr[start]) {
            start++;
        }
        let endOld = oldStr.length;
        let endNew = newStr.length;
        while (endOld > start && endNew > start && oldStr[endOld - 1] === newStr[endNew - 1]) {
            endOld--;
            endNew--;
        }
        return { from: start, to: endOld, insert: newStr.slice(start, endNew) };
    }

    const FastEdit = {
        pagename: mw.config.get('wgPageName'),
        version: '2.2',
        contentmodels: ['wikitext', 'text', 'sanitized-css', 'json', 'javascript', 'css', 'Scribunto', 'MassMessageListContent'],
        predefinedSummaries: [
            { value: 'copyedit', label: 'Επιμέλεια' },
            { value: 'grammar', label: 'Διορθώσεις' },
            { value: 'expand', label: 'Επέκταση' },
            { value: 'update', label: 'Ενημέρωση' },
            { value: 'citation needed', label: 'Προσθήκη πηγής' },
            { value: 'remove', label: 'Μορφοποίηση' },
	    { value: 'style', label: 'Διαμόρφωση' },
            { value: 'changes cat', label: 'Αναδιάρθρωση κατηγοριών' }
        ],
        icons: {},
        undoIconHtml: null,
        isEditorOpen: false,
        hiddenElements: null,
        Vue: null,
        Codex: null,
        vueApp: null,

        prefs: {
            PREFIX: 'userjs-FastEdit-',
            _optsCache: {},
            get(key, fallback) {
                const fullKey = this.PREFIX + key;
                let raw = this._optsCache[fullKey];
                if (raw === undefined) {
                    try {
                        raw = window.localStorage.getItem(fullKey);
                    } catch (error) {
                        raw = null;
                    }
                }
                if (raw === undefined || raw === null) {
                    raw = mw.user.options.get(fullKey);
                }
                if (raw === undefined || raw === null) {
                    return fallback;
                }
                try {
                    return JSON.parse(raw);
                } catch (error) {
                    return fallback;
                }
            },
            async set(key, value) {
                const fullKey = this.PREFIX + key;
                const fullValue = JSON.stringify(value);
                this._optsCache[fullKey] = fullValue;
                try {
                    window.localStorage.setItem(fullKey, fullValue);
                } catch (error) { /* empty */ }
                try {
                    await new mw.Api().postWithEditToken({
                        action: 'options',
                        optionname: fullKey,
                        optionvalue: fullValue,
                        formatversion: 2
                    });
                } catch (error) { /* empty */ }
            }
        },

        init() {
            mw.loader.using([
                'vue',
                '@wikimedia/codex',
                'mediawiki.util',
                'mediawiki.api',
                'mediawiki.user',
                'mediawiki.diff',
                'mediawiki.diff.styles',
                'jquery.textSelection'
            ]).then((require) => {
                this.Vue = require('vue');
                this.Codex = require('@wikimedia/codex');
                this.run();
            });
        },

        async run() {
            this.addStyle();
            await this.loadIcons();
            this.setupTriggers();
        },

        async loadIcons() {
            try {
                const api = new mw.Api();
                const data = await api.get({
                    action: 'query',
                    list: 'codexicons',
                    names: 'cdxIconCheck|cdxIconClose|cdxIconUndo|cdxIconSettings|cdxIconAdd|cdxIconTrash',
                    formatversion: 2
                });
                this.icons = (data && data.query && data.query.codexicons) || {};
            } catch (error) {
                this.icons = {};
            }
            this.undoIconHtml = this.renderCdxIcon(this.icons.cdxIconUndo);
        },

        renderCdxIcon(icon) {
            if (!icon || !this.Vue || !this.Codex) {
                return null;
            }
            const container = document.createElement('div');
            const app = this.Vue.createApp({
                render: () => this.Vue.h(this.Codex.CdxIcon, { icon })
            });
            app.mount(container);
            const html = container.innerHTML;
            app.unmount();
            return html || null;
        },

        addStyle() {
            mw.util.addCSS(`
                #FastEdit-main {
                    margin-top: 12px;
                }
                .FastEdit-editor {
                    background: var(--background-color-base, #ced2d3);
                    border: 1px solid var(--border-color-base, #a2a9b1);
                    border-radius: 3px;
                    padding: 14px;
                    color: var(--color-base, #202122);
                }
                .FastEdit-header {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    gap: 10px;
                    margin-bottom: 14px;
                }
                .FastEdit-header-title {
                    font-size: 16px;
                    font-weight: bold;
                }
                .FastEdit-version {
                    font-size: 16px;
                    font-weight: normal;
                    color: var(--color-subtle, #fff);
                    margin-left: 4px;
                }
                .FastEdit-settings-btn {
                    flex-shrink: 0;
                }
                .FastEdit-header-meta {
                    font-size: 12px;
                    color: var(--color-subtle, #54595d);
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    flex-wrap: wrap;
                }
                .FastEdit-chip {
                    background: var(--background-color-neutral-subtle, #f8f9fa);
                    border: 1px solid var(--border-color-subtle, #eaecf0);
                    border-radius: 12px;
                    padding: 1px 8px;
                    font-size: 11px;
                }
                .FastEdit-chip--warning {
                    border-color: var(--border-color-warning, #fc3);
                    color: var(--color-warning, #000);
                }
                .FastEdit-banner {
                    margin-bottom: 12px;
                }
                .FastEdit-tabs {
                    margin-bottom: 14px;
                }
                .FastEdit-field {
                    margin: 10px 0;
                }
                .FastEdit-textarea textarea {
                    min-height: 320px;
                    font-family: monospace, monospace;
                    background-color: #cee7f3; color: black;
                    font-size: 17px;
                    width: 100%;
                    resize: vertical;
                }
                .FastEdit-textarea .cm-editor {
                    border: 1px solid var(--border-color-base, #aaa7f3);
                    border-radius: 2px;
                }
                .FastEdit-textarea .cm-editor,
                .FastEdit-textarea .cm-scroller {
                    min-height: 320px;
                }
                .FastEdit-textarea .cm-content {
                    background:#fff; color:black;
                    font-family: monospace, monospace;
                    font-size: 17px;
                }
                .FastEdit-editor-toggle {
                    margin-top: 4px;
                    text-align: right;
                    font-size: 12px;
                }
                .FastEdit-editor-toggle a {
                    cursor: pointer;
                }
                .FastEdit-summary-accordion {
                    margin: 10px 0;
                }
                .FastEdit-summary-buttons {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 6px;
                    padding: 8px 2px 2px;
                }
                .FastEdit-preview-content,
                .FastEdit-diff-content {
                    padding: 4px 2px;
                    font-size: 0.95em;
                    line-height: 1.6;
                }
                .FastEdit-diff-content .diff > tbody > tr {
                    position: relative;
                }
                .FastEdit-diffundo-btn {
                    position: absolute;
                    inset-inline-end: 2px;
                    bottom: 2px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    width: 18px;
                    height: 18px;
                    border: none;
                    background: var(--background-color-base, #cee7f3);
                    border-radius: 2px;
                    padding: 2px;
                    cursor: pointer;
                    opacity: 0;
                    line-height: 1;
                    color: var(--color-subtle, #54595d);
                }
                .FastEdit-diffundo-btn svg {
                    width: 12px;
                    height: 12px;
                }
                .FastEdit-diff-content .diff > tbody > tr:hover .FastEdit-diffundo-btn {
                    opacity: 0.7;
                }
                .FastEdit-diffundo-btn:hover {
                    opacity: 1 !important;
                }
                .FastEdit-diffundo-undone {
                    text-decoration: line-through;
                    opacity: 0.5;
                }
                .FastEdit-actions {
                    display: flex;
                    gap: 8px;
                    margin-top: 14px;
                    padding-top: 14px;
                    border-top: 1px solid var(--border-color-subtle, #aaacf0);
                }
                .FastEdit-actions .cdx-icon {
                    margin-right: 4px;
                }
                #FastEdit-Edit-TopBtn a {
                    cursor: pointer;
                }
                .FastEdit-prefs-field {
                    margin-bottom: 16px;
                }
                .FastEdit-prefs-summary-list {
                    display: flex;
                    flex-direction: column;
                    gap: 6px;
                    margin-bottom: 8px;
                }
                .FastEdit-prefs-summary-row {
                    display: flex;
                    align-items: center;
                    gap: 6px;
                }
                .FastEdit-prefs-summary-row .cdx-text-input {
                    flex: 1;
                }
                .FastEdit-prefs-summary-add {
                    display: flex;
                    align-items: center;
                    gap: 6px;
                }
                .FastEdit-prefs-summary-add .cdx-text-input {
                    flex: 1;
                }
            `);
        },

        getRevisionFromURL() {
            const params = new URLSearchParams(window.location.search);
            const revId = params.get('oldid') || params.get('diff');
            if (revId && /^\d+$/.test(revId)) {
                return revId;
            }
            return null;
        },

        scrollToElement(element, offset) {
            offset = offset || 0;
            if (!element) {
                return;
            }
            const top = element.getBoundingClientRect().top + window.pageYOffset + offset;
            window.scrollTo({ top, behavior: 'smooth' });
        },

        getHeadingLevel($el) {
            const $heading = $el.hasClass('mw-heading') ? $el.find('h1, h2, h3, h4, h5, h6').first() : $el;
            const tag = $heading.prop('tagName');
            return tag ? Number(tag.charAt(1)) : 999;
        },

        isHeadingElement($el) {
            return $el.hasClass('mw-heading') || /^H[1-6]$/.test($el.prop('tagName') || '');
        },

        getSectionElements($heading) {
            const level = this.getHeadingLevel($heading);
            const elements = [$heading.get(0)];
            let $next = $heading.next();
            while ($next.length) {
                if (this.isHeadingElement($next) && this.getHeadingLevel($next) <= level) {
                    break;
                }
                elements.push($next.get(0));
                $next = $next.next();
            }
            return elements;
        },

        openEditor(section, sectionName, $sectionHeading) {
            if (this.isEditorOpen) {
                mw.notify('An editor is already open.', { type: 'warn' });
                return;
            }
            this.isEditorOpen = true;

            const revisionId = this.getRevisionFromURL();
            const queryParams = {
                action: 'query',
                titles: this.pagename,
                prop: ['info', 'revisions'],
                rvprop: ['ids', 'content', 'contentmodel', 'timestamp', 'user'],
                rvlimit: 1,
                formatversion: 2
            };
            if (revisionId) {
                queryParams.rvstartid = revisionId;
            }
            if (section !== undefined && section !== null) {
                queryParams.rvsection = section;
            }

            new mw.Api().get(queryParams).then((response) => {
                const page = response.query.pages[0];
                let content = '';
                let contentmodel = 'wikitext';
                let isOldRevision = false;
                let revisionInfo = null;
                let isMissingPage = false;

                if (page.revisions && page.revisions.length) {
                    const rev = page.revisions[0];
                    content = rev.content;
                    contentmodel = rev.contentmodel;
                    if (revisionId) {
                        isOldRevision = String(rev.revid) !== String(page.lastrevid);
                        revisionInfo = { timestamp: rev.timestamp, user: rev.user, revid: rev.revid };
                    }
                } else if (page.missing) {
                    isMissingPage = true;
                } else {
                    mw.notify('Failed to load page content.', { type: 'error' });
                    this.isEditorOpen = false;
                    return;
                }

                if (!this.contentmodels.includes(contentmodel)) {
                    mw.notify('Page content model is not a simple text-based one.', {
                        title: 'Unsupported content model',
                        type: 'error',
                        autoHide: true,
                        autoHideSeconds: 5,
                        tag: 'FastEdit-notification'
                    });
                    this.isEditorOpen = false;
                    return;
                }

                this.mountEditor({
                    content,
                    contentmodel,
                    section,
                    sectionName,
                    sectionHeading: $sectionHeading,
                    isOldRevision,
                    revisionInfo,
                    isMissingPage,
                    revisionId,
                    initialLastRevId: page.missing ? null : page.lastrevid
                });
            }, () => {
                mw.notify('API error while loading the page.', { type: 'error' });
                this.isEditorOpen = false;
            });
        },

        mountEditor(state) {
            const hasSectionHeading = state.section !== undefined && state.section !== null &&
                state.sectionHeading && state.sectionHeading.length;

            if (hasSectionHeading) {
                const elements = this.getSectionElements(state.sectionHeading);
                this.hiddenElements = $(elements);
                this.hiddenElements.hide();
                $('
').attr('id', 'FastEdit-main').insertAfter(this.hiddenElements.last()); } else { this.hiddenElements = null; $('#mw-content-text').hide(); $('
').attr('id', 'FastEdit-main').insertAfter('#mw-content-text'); } const EditorComponent = this.getEditorComponent(state); this.vueApp = this.Vue.createMwApp(EditorComponent); this.vueApp.mount('#FastEdit-main'); setTimeout(() => { this.scrollToElement(document.getElementById('FastEdit-main'), -50); }, 100); }, closeEditor() { if (this.vueApp) { this.vueApp.unmount(); this.vueApp = null; } $('#FastEdit-main').remove(); if (this.hiddenElements && this.hiddenElements.length) { this.hiddenElements.show(); } else { $('#mw-content-text').show(); } this.hiddenElements = null; this.isEditorOpen = false; }, getEditorComponent(state) { const Codex = this.Codex; const pagename = this.pagename; const version = this.version; const icons = this.icons; const undoIconHtml = this.undoIconHtml; const predefinedSummaries = this.predefinedSummaries; const prefsStore = this.prefs; const closeEditor = () => this.closeEditor(); return { components: { CdxButton: Codex.CdxButton, CdxTextArea: Codex.CdxTextArea, CdxTextInput: Codex.CdxTextInput, CdxCheckbox: Codex.CdxCheckbox, CdxRadio: Codex.CdxRadio, CdxField: Codex.CdxField, CdxMessage: Codex.CdxMessage, CdxProgressBar: Codex.CdxProgressBar, CdxIcon: Codex.CdxIcon, CdxTabs: Codex.CdxTabs, CdxTab: Codex.CdxTab, CdxAccordion: Codex.CdxAccordion, CdxDialog: Codex.CdxDialog }, data() { return { pagename, version, predefinedSummaries, contentmodel: state.contentmodel, section: state.section !== undefined ? state.section : null, sectionName: state.sectionName || '', isOldRevision: state.isOldRevision, revisionInfo: state.revisionInfo, isMissingPage: state.isMissingPage, revisionId: state.revisionId, initialLastRevId: state.initialLastRevId, newerRevisionAvailable: false, originalContent: state.content, textareaValue: state.content, summaryValue: '', minorChecked: false, summaryPickerOpen: false, activeTab: 'edit', useCodeMirror: true, codeMirrorInstance: null, previewLoading: false, previewError: '', previewHtml: '', previewStale: true, previewLoadedOnce: false, reviewLoading: false, reviewError: '', reviewEmpty: false, reviewHtml: '', reviewStale: true, reviewLoadedOnce: false, diffLineOffset: 0, diffRevId: null, saving: false, justSaved: false, saveStatus: null, conflictDialogOpen: false, conflictMessage: '', checkIcon: icons.cdxIconCheck || null, closeIcon: icons.cdxIconClose || null, settingsIcon: icons.cdxIconSettings || null, addIcon: icons.cdxIconAdd || null, trashIcon: icons.cdxIconTrash || null, undoIconSvg: undoIconHtml || '↺', customSummaries: [], summaryMode: 'both', currentEditorModePref: 'codemirror', prefsDialogOpen: false, prefsDraft: { editorMode: 'codemirror', customSummaries: [], summaryMode: 'both' }, newSummaryDraft: '' }; }, computed: { pagenameDisplay() { return this.pagename.replace(/_/g, ' '); }, formattedRevisionDate() { if (!this.revisionInfo) { return ''; } return new Date(this.revisionInfo.timestamp).toLocaleString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', day: 'numeric', month: 'long', year: 'numeric' }); }, userPageUrl() { return this.revisionInfo ? mw.util.getUrl('User:' + this.revisionInfo.user) : '#'; }, userTalkUrl() { return this.revisionInfo ? mw.util.getUrl('User talk:' + this.revisionInfo.user) : '#'; }, userContribsUrl() { return this.revisionInfo ? mw.util.getUrl('Special:Contributions/' + this.revisionInfo.user) : '#'; }, textareaEl() { const wrap = this.$refs.textareaWrap; return wrap && wrap.$el ? wrap.$el.querySelector('textarea') : null; }, codeMirrorSupported() { return CODE_MIRROR_MODE_MAP[this.contentmodel] !== undefined; }, hasUnsavedChanges() { if (this.justSaved) { return false; } return this.textareaValue !== this.originalContent || this.summaryValue.trim() !== ''; }, editorModeOptions() { return [ { value: 'codemirror', label: 'Syntax highlighted (CodeMirror)' }, { value: 'plain', label: 'Plain text' } ]; }, effectiveSummaries() { const customs = this.customSummaries.map((label, idx) => ({ value: 'FastEdit-custom-' + idx, label })); if (this.summaryMode === 'custom' && customs.length) { return customs; } return predefinedSummaries.concat(customs); } }, watch: { activeTab(newTab) { if (newTab === 'preview' && (this.previewStale || !this.previewLoadedOnce)) { this.loadPreview(); } else if (newTab === 'changes') { if (this.reviewStale || !this.reviewLoadedOnce) { this.loadReview(); } else { this.$nextTick(() => this.setupDiffUndo()); } } }, textareaValue(newVal) { this.previewStale = true; this.reviewStale = true; this.syncCodeMirrorContent(newVal); }, useCodeMirror(enabled) { if (enabled) { this.initCodeMirror(); } else { this.destroyCodeMirror(); } }, newerRevisionAvailable(val) { if (val && !this.justSaved) { this.reviewStale = true; if (this.activeTab === 'changes') { this.loadReview(); } } } }, mounted() { this.loadPreferences(); this.checkForNewerRevision(); this._pollTimer = setInterval(() => this.checkForNewerRevision(), 25000); this._beforeUnloadHandler = (e) => { if (this.hasUnsavedChanges) { e.preventDefault(); e.returnValue = ''; } }; window.addEventListener('beforeunload', this._beforeUnloadHandler); this._keyHandler = (e) => { if (this.conflictDialogOpen) { return; } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!this.saving) { this.save(); } } else if (e.key === 'Escape') { this.close(); } }; window.addEventListener('keydown', this._keyHandler); if (this.useCodeMirror && this.codeMirrorSupported) { this.initCodeMirror(); } }, beforeUnmount() { this.destroyCodeMirror(); clearInterval(this._pollTimer); window.removeEventListener('beforeunload', this._beforeUnloadHandler); window.removeEventListener('keydown', this._keyHandler); }, methods: { close() { if (this.hasUnsavedChanges && !window.confirm('Discard your unsaved changes?')) { return; } closeEditor(); }, async initCodeMirror() { if (this.codeMirrorInstance || !this.codeMirrorSupported) { return; } await this.$nextTick(); const textareaEl = this.textareaEl; if (!textareaEl) { return; } const modeKey = CODE_MIRROR_MODE_MAP[this.contentmodel]; try { const instance = modeKey === 'mediawiki' ? await loadCodeMirrorForWikitext(textareaEl) : await loadCodeMirrorForMode(textareaEl, modeKey); if (!instance) { this.useCodeMirror = false; return; } this.codeMirrorInstance = instance; this._cmInputHandler = ({ view }) => { if (view !== instance.view) { return; } const newContent = view.state.sliceDoc(); if (newContent !== this.textareaValue) { this.textareaValue = newContent; } }; mw.hook('ext.CodeMirror.input').add(this._cmInputHandler); } catch (e) { mw.notify('Could not load syntax highlighting; using plain text instead.', { type: 'warn' }); this.useCodeMirror = false; } }, destroyCodeMirror() { if (this._cmInputHandler) { mw.hook('ext.CodeMirror.input').remove(this._cmInputHandler); this._cmInputHandler = null; } if (this.codeMirrorInstance && this.codeMirrorInstance.isActive) { this.codeMirrorInstance.destroy(); } this.codeMirrorInstance = null; }, syncCodeMirrorContent(newVal) { const instance = this.codeMirrorInstance; if (!instance || !instance.view) { return; } const view = instance.view; const current = view.state.sliceDoc(); if (current === newVal) { return; } const { from, to, insert } = computeMinimalChange(current, newVal); const transaction = view.state.update({ changes: { from, to, insert } }); view.dispatch(transaction); }, async checkForNewerRevision() { if (this.saving || this.justSaved) { return; } if (this.initialLastRevId === null || this.initialLastRevId === undefined) { return; } try { const res = await new mw.Api().get({ action: 'query', titles: this.pagename, prop: 'info', formatversion: 2 }); if (this.saving || this.justSaved) { return; } const page = res.query.pages[0]; if (page && !page.missing && String(page.lastrevid) !== String(this.initialLastRevId)) { this.newerRevisionAvailable = true; } } catch (e) { /* empty */ } }, appendSummary(label) { this.summaryValue = this.summaryValue ? `${this.summaryValue}; ${label}` : label; }, async loadPreview() { this.previewLoading = true; this.previewError = ''; try { const res = await new mw.Api().post({ action: 'parse', text: this.textareaValue, title: this.pagename, contentmodel: this.contentmodel, pst: true, formatversion: 2 }); this.previewHtml = res.parse.text; this.previewStale = false; this.previewLoadedOnce = true; } catch (e) { this.previewError = 'Error generating preview.'; } this.previewLoading = false; }, async computeDiffLineOffset() { if (this.section === null || this.section === undefined) { this.diffLineOffset = 0; return true; } try { const res = await new mw.Api().get({ action: 'parse', page: this.pagename, prop: 'revid|sections|wikitext', formatversion: 2 }); const revid = res.parse.revid; if (revid === this.diffRevId) { return true; } const sectionInfo = res.parse.sections.find( (s) => String(s.index) === String(this.section) ); if (!sectionInfo) { mw.notify("Couldn't get the section offset.", { tag: 'FastEdit-diffundo', type: 'error' }); return false; } const charOffset = sectionInfo.byteoffset; const wikitext = res.parse.wikitext; this.diffLineOffset = charOffset ? wikitext.slice(0, charOffset).split('\n').length - 1 : 0; this.diffRevId = revid; return true; } catch (e) { this.diffLineOffset = 0; return true; } }, async loadReview() { this.reviewLoading = true; this.reviewError = ''; this.reviewEmpty = false; this.reviewHtml = ''; const offsetOk = await this.computeDiffLineOffset(); if (!offsetOk) { this.reviewLoading = false; this.reviewError = "Couldn't get the section offset."; return; } try { const compareParams = { action: 'compare', fromtitle: this.pagename, toslots: 'main', 'totext-main': this.textareaValue, formatversion: 2 }; if (this.section !== null && this.section !== undefined) { compareParams.fromsection = this.section; } if (this.revisionId) { compareParams.fromrev = this.revisionId; } const res = await new mw.Api().get(compareParams); if (!res.compare || res.compare.body === '') { this.reviewEmpty = true; } else { this.reviewHtml = '' + '' + '' + '' + res.compare.body + '
'
;
await this.$nextTick(); if (this.$refs.diffOutput) { mw.hook('wikipage.diff').fire($(this.$refs.diffOutput)); } this.setupDiffUndo(); } this.reviewStale = false; this.reviewLoadedOnce = true; } catch (e) { this.reviewError = 'Error generating diff.'; } this.reviewLoading = false; }, setupDiffUndo() { const diffEl = this.$refs.diffOutput; if (!diffEl) { return; } const $diff = $(diffEl); const $rows = $diff.find('table.diff > tbody > tr'); if (!$rows.length) { return; } const offset = this.diffLineOffset; const normalize = (text) => (text === '\u00A0' ? '' : text); let lineIdx = null; const contentRows = []; $rows.each((_, row) => { const $row = $(row); const $lineNoCell = $row.children('.diff-lineno').last(); if ($lineNoCell.length) { const num = $lineNoCell.text().replace(/\D/g, ''); if (num) { lineIdx = Number(num) - 1 - offset; } return; } if (lineIdx === null) { return; } const hasAdded = $row.children('.diff-addedline').length > 0; const hasDeleted = $row.children('.diff-deletedline').length > 0; const hasContext = $row.children('.diff-context').length > 0; if (!hasAdded && !hasDeleted && !hasContext) { return; } if (hasAdded || hasDeleted) { $row.data('FastEdit-line-idx', lineIdx); contentRows.push($row); } const isUndone = $row.hasClass('FastEdit-diffundo-undone'); const isPresentNow = hasContext ? true : (isUndone ? hasDeleted : hasAdded); if (isPresentNow) { lineIdx += 1; } }); const shiftFollowingRows = ($row, delta) => { if (!delta) { return; } let found = false; contentRows.forEach(($r) => { if (found) { const current = $r.data('FastEdit-line-idx'); if (current !== undefined) { $r.data('FastEdit-line-idx', current + delta); } return; } if ($r.is($row)) { found = true; } }); }; const handleUndo = ($row) => { const rowLineIdx = $row.data('FastEdit-line-idx'); if (rowLineIdx === undefined || rowLineIdx === null) { mw.notify("Couldn't get the line number.", { tag: 'FastEdit-diffundo', type: 'error' }); return; } const isUndone = $row.hasClass('FastEdit-diffundo-undone'); const $toReplace = $row.children(isUndone ? '.diff-deletedline' : '.diff-addedline'); const $toRestore = $row.children(isUndone ? '.diff-addedline' : '.diff-deletedline'); const isInsert = !$toReplace.length; const isRemove = !$toRestore.length; const lines = this.textareaValue.split('\n'); let delta = 0; if (isInsert) { if (rowLineIdx > lines.length) { mw.notify('The line has been modified since the diff.', { tag: 'FastEdit-diffundo', type: 'warn' }); return; } lines.splice(rowLineIdx, 0, $toRestore.text()); delta = 1; } else { const expected = normalize($toReplace.text()); if (normalize(lines[rowLineIdx]) !== expected) { mw.notify('The line has been modified since the diff.', { tag: 'FastEdit-diffundo', type: 'warn' }); return; } if (isRemove) { lines.splice(rowLineIdx, 1); delta = -1; } else { lines.splice(rowLineIdx, 1, $toRestore.text()); delta = 0; } } this.textareaValue = lines.join('\n'); $row.toggleClass('FastEdit-diffundo-undone', !isUndone); shiftFollowingRows($row, delta); }; contentRows.forEach(($row) => { const $target = $row.children('.diff-addedline').length ? $row.children('.diff-addedline') : $row.children('.diff-empty.diff-side-added'); if (!$target.length) { return; } const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'FastEdit-diffundo-btn'; btn.title = 'Undo this line'; btn.innerHTML = this.undoIconSvg; btn.addEventListener('click', () => handleUndo($row)); $target.get(0).appendChild(btn); }); }, loadPreferences() { const editorMode = prefsStore.get('editor-mode', 'codemirror'); const customSummaries = prefsStore.get('custom-summaries', []); const summaryMode = prefsStore.get('summary-mode', 'both'); this.currentEditorModePref = editorMode === 'plain' ? 'plain' : 'codemirror'; this.customSummaries = Array.isArray(customSummaries) ? customSummaries.slice(0, 10) : []; this.summaryMode = summaryMode === 'custom' ? 'custom' : 'both'; this.useCodeMirror = this.currentEditorModePref !== 'plain'; }, openPrefs() { this.prefsDraft = { editorMode: this.currentEditorModePref, customSummaries: this.customSummaries.slice(), summaryMode: this.summaryMode }; this.newSummaryDraft = ''; this.prefsDialogOpen = true; }, closePrefs() { this.prefsDialogOpen = false; }, addCustomSummary() { const value = this.newSummaryDraft.trim(); if (!value || this.prefsDraft.customSummaries.length >= 10) { return; } this.prefsDraft.customSummaries.push(value); this.newSummaryDraft = ''; }, removeCustomSummary(idx) { this.prefsDraft.customSummaries.splice(idx, 1); }, async savePrefs() { const draft = this.prefsDraft; const customSummaries = draft.customSummaries .map((s) => s.trim()) .filter(Boolean) .slice(0, 10); this.customSummaries = customSummaries; this.summaryMode = draft.summaryMode === 'custom' ? 'custom' : 'both'; this.currentEditorModePref = draft.editorMode === 'plain' ? 'plain' : 'codemirror'; if (this.currentEditorModePref === 'plain') { this.useCodeMirror = false; } else if (this.codeMirrorSupported) { this.useCodeMirror = true; } this.prefsDialogOpen = false; await Promise.all([ prefsStore.set('editor-mode', this.currentEditorModePref), prefsStore.set('custom-summaries', customSummaries), prefsStore.set('summary-mode', this.summaryMode) ]); mw.notify('Preferences saved.', { type: 'success', autoHide: true, autoHideSeconds: 5, tag: 'FastEdit-notification' }); }, save() { this.saving = true; this.saveStatus = { type: 'notice', message: 'Αποθήκευση…' }; let summary = this.summaryValue || ''; if (this.sectionName) { summary = `/* ${this.sectionName} */ ${summary}`; } summary += ' (By [[:m:User:Fotis A./FastEdit.js|FastEdit]])'; const editParams = { action: 'edit', title: this.pagename, text: this.textareaValue, discussiontoolsautosubscribe: 'no', summary, formatversion: 2 }; if (this.section !== null && this.section !== undefined) { editParams.section = this.section; } if (this.minorChecked) { editParams.minor = true; } new mw.Api().postWithEditToken(editParams).then( (res) => { this.saving = false; if (res.edit && res.edit.result === 'Success') { this.justSaved = true; this.newerRevisionAvailable = false; this.originalContent = this.textareaValue; this.summaryValue = ''; clearInterval(this._pollTimer); window.removeEventListener('beforeunload', this._beforeUnloadHandler); this.saveStatus = { type: 'success', message: 'Η σελίδα αποθηκεύτηκε με επιτυχία! Γίνεται επαναφόρτωση…' }; mw.notify('ΟΚ!', { title: '', type: 'success', autoHide: true, autoHideSeconds: 5, tag: 'FastEdit-notification' }); location.reload(); return; } this.saveStatus = { type: 'error', message: 'Error saving page.' }; }, (code, data) => { this.saving = false; if (code === 'editconflict') { this.saveStatus = null; this.conflictMessage = (data && data.error && data.error.info) || 'An edit conflict occurred. Please resolve it manually.'; this.conflictDialogOpen = true; return; } const info = (data && data.error && data.error.info) || 'Error saving page.'; this.saveStatus = { type: 'error', message: info }; } ); } }, template: `
FastEditv{{ version }}
{{ pagenameDisplay }} {{ sectionName }} Old revision
⚙
Προειδοποίηση: επεξεργάζεστε μια μη ενημερωμένη αναθεώρηση αυτής της σελίδας, από {{ formattedRevisionDate }} τον {{ revisionInfo.user }} (talk | contribs). Αν το δημοσιεύσετε, τυχόν αλλαγές που έγιναν μετά από αυτή την αναθεώρηση θα χαθούν. Προειδοποίηση: Αυτή η σελίδα έχει τροποποιηθεί από κάποιον άλλον χρήστη αφότου ανοίξατε αυτόν τον επεξεργαστή. Ελέγξτε την καρτέλα «Αλλαγές» πριν αποθηκεύσετε, ώστε να μην αντικαταστήσετε κατά λάθος την τροποποίησή του. Προειδοποίηση: Αυτή η σελίδα δεν υπάρχει ακόμα — η αποθήκευση θα τη δημιουργήσει. Προειδοποίηση: Επεξεργάζεστε την ενότητα "{{ sectionName }}".
{{ useCodeMirror ? 'Switch to plain text editor' : 'Switch to syntax-highlighted editor' }}
v-for="s in effectiveSummaries" :key="s.value" size="small" @click="appendSummary(s.label)" >{{ s.label }}
Minor edit {{ previewError }}
{{ reviewError }} No changes to show.
{{ saveStatus.message }}
Αποθήκευση Ακύρωση
v-model:open="conflictDialogOpen" title="Edit conflict" close-button-label="Close" :use-close-button="true" > {{ conflictMessage }} v-model:open="prefsDialogOpen" title="Προτιμήσεις" close-button-label="Close" :use-close-button="true" :primary-action="{ label: 'Αποθήκευση', actionType: 'progressive' }" :default-action="{ label: 'Ακύρωση' }" @primary="savePrefs" @default="closePrefs" > v-for="option in editorModeOptions" :key="option.value" v-model="prefsDraft.editorMode" name="FastEdit-editor-mode" :input-value="option.value" >{{ option.label }}
×
+
Προεπιλεγμένες και προσαρμοσμένες περιλήψεις Custom summaries only
` }; }, setupTriggers() { $(document).ready(() => { this.checkForEditAction(); this.addTopButton(); this.addSectionButtons(); }); $(window).on('popstate', () => this.checkForEditAction()); $(window).on('beforeunload', () => { this.isEditorOpen = false; }); }, checkForEditAction() { if (mw.config.get('wgAction') === 'edit' && this.isEditorOpen) { this.closeEditor(); } }, addTopButton() { if ($('#FastEdit-Edit-TopBtn').length) { return; } if (!($('#ca-edit').length || mw.config.get('wgArticleId') === 0)) { return; } const skin = mw.config.get('skin'); let $btn; if (skin === 'vector') { $btn = $('
  • ') .attr('id', 'FastEdit-Edit-TopBtn') .addClass('vector-tab-noicon mw-list-item') .append($('').attr('href', '#').append($('').text('FastEdit'))); } else if (skin === 'minerva') { $btn = $('
  • ') .attr('id', 'FastEdit-Edit-TopBtn') .css({ 'align-items': 'center', display: 'flex' }) .append( $('').addClass('page-actions-menu__list-item').append( $('') .attr('href', '#') .addClass('mw-ui-icon mw-ui-icon-element mw-ui-icon-wikimedia-edit-base20 mw-ui-icon-with-label-desktop') .css('vertical-align', 'middle') .text('FastEdit') ) ); } else { $btn = $('
  • ') .attr('id', 'FastEdit-Edit-TopBtn') .append($('').append($('').attr('href', '#').text('FastEdit'))); } if (skin === 'minerva') { if ($('#ca-edit').length) { $('#ca-edit').parent().after($btn); } else { $('#page-actions ul').append($btn); } } else if ($('#ca-edit').length) { $('#ca-edit').after($btn); } else { $('#ca-talk').after($btn); } $btn.find('a').on('click', (e) => { e.preventDefault(); this.openEditor(); }); }, addSectionButtons() { $('.mw-editsection').each((_, el) => { const $section = $(el); if ($section.find('.FastEdit-section-edit, .FastEdit-minerva-section-edit').length) { return; } const $editLink = $section.find('a[href*="action=edit"]').first(); if (!$editLink.length) { return; } const href = $editLink.attr('href'); const urlParams = new URLSearchParams(href.split('?')[1]); const section = urlParams.get('section'); if (section === null) { return; } const $headingWrapper = $section.closest('.mw-heading'); const $header = $headingWrapper.length ? $headingWrapper.find('h1, h2, h3, h4, h5, h6').first() : $section.closest('h1, h2, h3, h4, h5, h6'); const $headerClone = $header.length ? $header.clone() : null; if ($headerClone) { $headerClone.find('.ext-checkuser-tempaccount-reveal-ip').remove(); } const sectionName = ($headerClone && $headerClone.text().trim()) || ('Section ' + section); const $sectionHeading = $headingWrapper.length ? $headingWrapper : $header; if (mw.config.get('skin') === 'minerva') { const $drButton = $('') .addClass('FastEdit-minerva-section-edit cdx-button cdx-button--size-large cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet') .css('margin-left', '8px') .attr('href', '#') .attr('title', `Edit section with FastEdit: ${sectionName}`) .text('FastEdit') .on('click', (e) => { e.preventDefault(); this.openEditor(section, sectionName, $sectionHeading); }); $editLink.after($drButton); } else { const $btn = $('') .addClass('FastEdit-section-edit') .attr('href', '#') .text('FastEdit') .on('click', (e) => { e.preventDefault(); this.openEditor(section, sectionName, $sectionHeading); }); $section.find('.mw-editsection-bracket').last().before(', ').before($btn); } }); } }; FastEdit.init(); }); //