User:Fotis A./FastEdit.js
Appearance
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: `
Προειδοποίηση: επεξεργάζεστε μια μη ενημερωμένη αναθεώρηση αυτής της σελίδας, από {{ formattedRevisionDate }} τον {{ revisionInfo.user }}
(talk | contribs).
Αν το δημοσιεύσετε, τυχόν αλλαγές που έγιναν μετά από αυτή την αναθεώρηση θα χαθούν.
Προειδοποίηση: Αυτή η σελίδα έχει τροποποιηθεί από κάποιον άλλον χρήστη αφότου ανοίξατε αυτόν τον επεξεργαστή. Ελέγξτε την καρτέλα «Αλλαγές» πριν αποθηκεύσετε, ώστε να μην αντικαταστήσετε κατά λάθος την τροποποίησή του.
Προειδοποίηση: Αυτή η σελίδα δεν υπάρχει ακόμα — η αποθήκευση θα τη δημιουργήσει.
Προειδοποίηση: Επεξεργάζεστε την ενότητα "{{ sectionName }}".
Σύντομες περιλήψεις
Σύνοψη επεξεργασίας
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"
>
Default editor mode
v-for="option in editorModeOptions"
:key="option.value"
v-model="prefsDraft.editorMode"
name="FastEdit-editor-mode"
:input-value="option.value"
>{{ option.label }}
Προσαρμοσμένες σύντομες περιλήψεις
Προσθέστε έως και 10 δικά σας κουμπιά γρήγορης σύνοψης.
Κουμπιά για την εμφάνιση σύντομης σύνοψης
Προεπιλεγμένες και προσαρμοσμένες περιλήψεις
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();
});
//