User:Jack who built the house/editHere.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.
/** Edit Here **/
/ Edit sections of a page without leaving the article
/ Based on [[en:w:User:BrandonXLF/QuickEdit]] by [[en:w:User:BrandonXLF]]
(function () {
let mobile = mw.config.get('skin') === 'minerva';
let titleRegexp = new RegExp(
mw.config.get('wgArticlePath').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\$1/, '([^?]+)') +
'|[?&]title=([^&#]*)'
);
let apiSingleton;
function api(func, params) {
if (!apiSingleton) {
apiSingleton = new mw.Api();
}
$.extend(params, {
errorformat: 'html',
errorlang: mw.config.get('wgUserLanguage'),
errorsuselocal: true
});
return apiSingleton[func](params).fail(function (_, data) {
mw.notify(apiSingleton.getErrorMessage(data), {
type: 'error',
tag: 'edithere'
});
});
}
function getPageInfo(title, sectionID) {
return api('get', {
action: 'query',
curtimestamp: 1,
prop: 'revisions',
indexpageids: 1,
titles: title,
rvprop: ['timestamp', 'content'],
rvslots: 'main',
rvsection: sectionID
}).then(function (res) {
let rev = res.query.pages[res.query.pageids[0]].revisions[0];
return {
start: res.curtimestamp,
base: rev.timestamp,
full: rev.slots.main['*']
};
});
}
function getPreviewCallback(editor) {
editor.children('.preview').remove();
new OO.ui.ProgressBarWidget().$element.css({
maxWidth: '100%',
borderRadius: '0',
boxShadow: 'none',
margin: '8px 0'
}).addClass('preview').appendTo(editor);
return function (html) {
editor.children('.preview').remove();
const $preview = $('<div>')
.html(html)
.addClass('editHere-borderColorBase').css({
borderWidth: '1px',
borderStyle: 'solid',
padding: '8px',
overflowX: 'hidden'
})
.addClass('preview')
.appendTo(editor);
mw.hook('wikipage.content').fire($preview);
};
}
function showCompare(editor, title, from, to) {
mw.loader.load('mediawiki.diff.styles');
api('post', {
action: 'compare',
fromslots: 'main',
'fromtext-main': from,
fromtitle: title,
frompst: 'true',
toslots: 'main',
'totext-main': to,
totitle: title,
topst: 'true'
}).then(function (r) {
return r.compare['*'] ? $('<table>').addClass('diff').append(
$('<colgroup>').append(
$('<col>').addClass('diff-marker'),
$('<col>').addClass('diff-content'),
$('<col>').addClass('diff-marker'),
$('<col>').addClass('diff-content')
)
).append(r.compare['*']) : 'No differences.';
}).then(getPreviewCallback(editor));
}
/ Parts taken from EditPage::extractSectionTitle and Parser::stripSectionName
function getSectionSummary(text) {
let match = text.match(/^(=+)(.+)\1\s*(\n|$)/);
return !match ? '' : '/* ' + match[2].trim()
/ Strip internal link markup
.replace(/\[\[:?([^[|]+)\|([^[]+)\]\]/g, '$2')
.replace(/\[\[:?([^[]+)\|?\]\]/g, '$1')
/ Strip external link markup
.replace(new RegExp('\\[(?:' + mw.config.get('wgUrlProtocols') + ')([^ ]+?) ([^\\[]+)\\]', 'ig'), '$2')
/ Remove wikitext quotes
.replace(/(''|'''|''''')(?!')/g, '')
/ Strip HTML tags
.replace(/<[^>]+?>/g, '') + ' */ ';
}
function showEditor($element) {
let progress = new OO.ui.ProgressBarWidget();
/ https://www.mediawiki.org/wiki/Heading_HTML_changes
/ Cannot use .closest() because DiscussionTools nests an h2 within a .mw-heading
let heading = $element.parents(':header, .mw-heading').last();
let matcher = heading.nextUntil.bind(heading);
let inserter = heading.after.bind(heading);
let targetEl = $element.siblings('.edithere-target').last();
let titleMatch = targetEl.attr('href').match(titleRegexp);
let title = decodeURIComponent(titleMatch[1] || titleMatch[2]);
let sectionID = /[?&]v?e?section=T?-?(\d*)/.exec(targetEl.attr('href'))[1];
if (!heading.closest('.mw-parser-output').length) {
let $articleContent = $('#mw-content-text .mw-parser-output');
matcher = function (selector) {
let $child = $articleContent.children(selector).first();
if ($child.length) {
return $child.prevAll();
}
return $articleContent.children();
};
inserter = $articleContent.prepend.bind($articleContent);
}
inserter(progress.$element.css({
maxWidth: '100%',
borderRadius: '0',
boxShadow: 'none'
}));
$element.addClass('edithere-loading');
$('.edithere-hide').removeClass('edithere-hide');
$('.edithere-heading').removeClass('edithere-heading');
$('#edithere-editor').remove();
getPageInfo(title, sectionID).then(function (r) {
var start = r.start,
base = r.base,
full = r.full,
saving = false,
expanded = false,
remainderStart = full.match(/\n=+.+=+(?:\n|$)/),
part = remainderStart ? full.substring(0, remainderStart.index) : full,
remainder = remainderStart ? full.substring(remainderStart.index) : '',
level = 0,
editor;
full.replace(/^(=+).+?(=+)(?:\n|$)/, function (m, a, b) {
level = Math.min(a.length, b.length);
return m;
});
var levelMatch = 'h1';
for (var i = 2; i <= level; i++)
levelMatch += ', h' + i + ':has(*), .mw-heading' + i;
var partSection = matcher(':header:has(*), .mw-heading'),
fullSection = matcher(levelMatch),
textarea = new OO.ui.MultilineTextInputWidget({
rows: 1,
maxRows: 20,
autosize: true,
value: part
}),
summary = new OO.ui.TextInputWidget({
value: getSectionSummary(part)
}),
minor = new OO.ui.CheckboxInputWidget({
accessKey: 'i',
}),
save = new OO.ui.ButtonInputWidget({
label: 'Save',
title: 'Save your changes',
flags: ['primary', 'progressive'],
accessKey: 's'
}),
preview = new OO.ui.ButtonInputWidget({
label: 'Preview',
title: 'Preview the new wikitext'
}),
compare = new OO.ui.ButtonInputWidget({
label: 'Compare',
title: 'View the difference between the current revision and your revision'
}),
cancel = new OO.ui.ButtonInputWidget({
useInputTag: true,
label: 'Cancel',
title: 'Close the edit form and discard changes',
flags: ['secondary', 'destructive']
}),
more = new OO.ui.ButtonInputWidget({
label: '+',
title: 'Edit the entire section (including subsections)'
}),
buttons = new OO.ui.HorizontalLayout({
items: [save, preview, compare, cancel]
});
if (part != full) {
buttons.addItems([more], 3);
}
partSection.addClass('edithere-hide');
heading.addClass('edithere-heading');
$element.removeClass('edithere-loading');
progress.$element.remove();
textarea.$input.css({
borderRadius: '0'
});
summary.on('enter', function () {
save.emit('click');
});
save.on('click', function () {
if (saving) return;
var fullText = textarea.getValue() + (expanded ? '' : remainder);
saving = true;
save.setLabel('Saving...');
compare.setDisabled(true);
preview.setDisabled(true);
cancel.setDisabled(true);
more.setDisabled(true);
api('postWithEditToken', {
action: 'edit',
title: title,
section: sectionID,
summary: summary.getValue(),
text: fullText,
minor: minor.isSelected() ? true : undefined,
notminor: minor.isSelected() ? undefined : true,
starttimestamp: start,
basetimestamp: base
}).then(function () {
api('get', {
action: 'parse',
page: mw.config.get('wgPageName'),
prop: ['text', 'categorieshtml']
}).then(function (r) {
var contentText = $('#mw-content-text'),
catLinks = $('#catlinks');
contentText.find('.mw-parser-output').replaceWith(r.parse.text['*']);
mw.hook('wikipage.content').fire(contentText);
catLinks.replaceWith(r.parse.categorieshtml['*']);
mw.hook('wikipage.categories').fire(catLinks);
saving = false;
});
}, function (code) {
if (code == 'editconflict') {
showEditConflict(editor, title, sectionID, fullText).then(function (r) {
start = r.start;
base = r.base;
textarea = r.textarea;
expanded = true;
});
}
compare.setDisabled(false);
preview.setDisabled(false);
cancel.setDisabled(false);
more.setDisabled(expanded);
saving = false;
save.setLabel('Save');
});
});
preview.on('click', function () {
api('post', {
action: 'parse',
title: title,
prop: 'text',
pst: 'true',
disablelimitreport: 'true',
disableeditsection: 'true',
sectionpreview: 'true',
disabletoc: 'true',
text: textarea.getValue()
}).then(function (r) {
return r.parse.text['*'] + '<div style="clear:both;"></div>';
}).then(getPreviewCallback(editor));
});
compare.on('click', function () {
showCompare(editor, title, part + (expanded ? remainder : ''), textarea.getValue());
});
cancel.on('click', function () {
editor.remove();
heading.removeClass('edithere-heading');
fullSection.removeClass('edithere-hide');
});
more.on('click', function () {
expanded = true;
textarea.setValue(textarea.getValue() + remainder);
fullSection.addClass('edithere-hide');
more.setDisabled(true);
});
editor = $('<div id="edithere-editor">').css({
overflowX: 'hidden'
}).append(
$('<div>').addClass('editHere-backgroundColorInteractive editHere-colorBase editHere-borderColorBase').css({
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
marginBottom: '8px'
}).append(
textarea.$element.css({
width: '100%',
maxWidth: '100%',
fontFamily: 'monospace, monospace'
}).addClass('edithere-textarea'),
$('<div>').addClass('editHere-borderColorBase').css({
borderWidth: '1px',
borderStyle: 'solid',
borderWidth: '0 1px'
}).append(
$('<div>').css({
padding: '8px 4px 8px 8px',
display: 'table-cell',
verticalAlign: 'middle'
}).html('Edit summary:'),
summary.$element.css({
width: '100%',
maxWidth: '100%',
padding: '8px 0px',
display: 'table-cell',
verticalAlign: 'middle'
}),
new OO.ui.FieldLayout(minor, {
label: new OO.ui.HtmlSnippet('Minor edit?'),
align: 'inline'
}).$element.css({
padding: '8px 8px 8px 4px',
display: 'table-cell',
verticalAlign: 'middle'
})
),
buttons.$element.addClass('editHere-borderColorBase').css({
borderStyle: 'solid',
borderWidth: '0 1px',
padding: '0 8px 8px'
}),
title !== mw.config.get('wgPageName') ? $('<div>').addClass('editHere-borderColorBase').css({
borderStyle: 'solid',
borderWidth: '0 1px',
padding: '0 8px 8px'
}).append(
'Editing page: ',
$('<a>').attr('href', mw.config.get('wgArticlePath').replace('$1', title)).css({
fontWeight: 'bold'
}).text(title.replace(/_/g, ' '))
) : undefined
)
);
inserter(editor);
/ Fix cases when editing a section next to two floating images like at
/ https://en.wikipedia.org/w/index.php?title=Elevation&oldid=1235703526#Aviation
textarea.updatePosition();
}, function () {
$element.removeClass('edithere-loading');
progress.$element.remove();
});
}
function showEditConflict(editor, title, sectionID, text) {
return getPageInfo(title, sectionID).then(function (r) {
var textarea = new OO.ui.MultilineTextInputWidget({
rows: 1,
maxRows: 20,
autosize: true,
value: r.full
}),
textarea2 = new OO.ui.MultilineTextInputWidget({
rows: 1,
maxRows: 20,
autosize: true,
value: text,
});
function syncSize() {
textarea.styleHeight = -1;
textarea.adjustSize(true);
textarea2.styleHeight = -1;
textarea2.adjustSize(true);
var height = Math.max(textarea.$input.height(), textarea2.$input.height());
textarea.$input.height(height);
textarea2.$input.height(height);
}
textarea.$input.css({
borderRadius: '0'
});
editor.find('> :first-child > :first-child').remove();
$('<table>').addClass('editHere-borderColorBase').css({
width: '100%',
borderWidth: '1px',
borderStyle: 'solid',
borderBottom: 'none',
borderSpacing: '0',
margin: '0 !important'
}).append(
$('<tr>').append(
$('<th>').css({
width: '50%',
paddingTop: '4px'
}).text('Their version (to be saved)'),
$('<th>').css({
width: '50%',
paddingTop: '4px'
}).text('Your version')
),
$('<tr>').append(
$('<td>').css({
width: '50%',
padding: '4px 4px 0 8px'
}).append(
textarea.$element.css({
width: '100%',
maxWidth: '100%',
fontFamily: 'monospace, monospace'
})
),
$('<td>').css({
width: '50%',
padding: '4px 8px 0 4px'
}).append(
textarea2.$element.css({
width: '100%',
maxWidth: '100%',
fontFamily: 'monospace, monospace'
})
)
)
).prependTo(editor.find('> :first-child'));
textarea.on('change', syncSize);
textarea2.on('change', syncSize);
syncSize();
showCompare(editor, title, text, r.full);
r.textarea = textarea;
return r;
});
}
function clickHandler(event) {
const $element = $(event.target).closest('.edithere-editlink');
if (!$element.length || $element.hasClass('edithere-loading')) return;
event.preventDefault();
showEditor($element);
}
function addLinksToChildren($element) {
$element.find('#edithere-editor, .edithere-section').remove();
const $editHereLink = $('<a>')
.addClass('edithere-section edithere-editlink')
.attr('href', '#');
if (mobile) {
$editHereLink
.addClass('edithere-icon edithere-minerva-icon')
.attr('role', 'button')
.attr('tabindex', '0')
.append(
createSvg(20, 20)
.html('<path d="M20 14.4286L16.4 14.4286L16.4 7L14.6 7L14.6 14.4286L11 14.4286L15.5 20L20 14.4286Z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M16.4 7.63119V7H15.7688L16.4 7.63119ZM14.6 10.1498L15.7094 9.04044L14.6 7.93109V10.1498ZM13.7688 5L18.4 5V6.31934L18.7092 6.00057C18.8955 5.81322 19 5.55978 19 5.2956C19 5.03143 18.8955 4.77799 18.7092 4.59063L15.3694 1.29078C15.182 1.10454 14.9286 1 14.6644 1C14.4002 1 14.1468 1.10454 13.9594 1.29078L11.9995 3.23069L13.7688 5ZM9.27453 15.4753L5.74979 19H1V14.2502L10.9596 4.29065L12.6 5.93109V12.1498L12.3212 12.4286H6.81374L9.27453 15.4753Z" />')
);
} else {
$editHereLink.text('edit here');
}
$element.find('.mw-editsection').each(function () {
$('[href*="section="]', this)
.last()
.after(
mobile ? '' : '<span class="edithere-section"> | </span>',
$editHereLink.clone(true)
)
.addClass('edithere-target');
});
}
function createSvg(
width,
height,
viewBoxWidth = width,
viewBoxHeight = height
) {
return $(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${viewBoxWidth} ${viewBoxHeight}`)
.attr('aria-hidden', 'true')
/ https://en.wikipedia.org/wiki/Project:Dark_mode_(gadget)
.addClass('mw-invert');
}
$.when(mw.loader.using(['oojs-ui-core']), $.ready).done(function () {
const $body = $(document.body);
$body.on('click', clickHandler);
addLinksToChildren($body);
mw.hook('wikipage.content').add(addLinksToChildren);
});
mw.loader.addStyleTag(`
.editHere-backgroundColorInteractive {
background-color: var(--background-color-interactive, #eaecf0);
}
.editHere-colorBase {
color: var(--color-base, #202122);
}
.editHere-borderColorBase {
border-color: var(--border-color-base, #a2a9b1);
}
.skin-minerva .mw-editsection {
white-space: nowrap;
}
.skin-minerva .content .collapsible-heading .edithere-section {
visibility: hidden;
}
.skin-minerva .content .collapsible-heading.open-block .edithere-section {
visibility: visible;
}
.edithere-hide {
display: none !important;
}
.edithere-loading,
.edithere-heading {
color: #777;
}
.edithere-icon {
display: inline-flex;
align-items: center;
justify-content: center;
background-image: none;
svg {
fill: currentcolor;
}
}
.edithere-minerva-icon.edithere-minerva-icon {
color: var(--color-subtle, #54595d);
min-width: 44px;
min-height: 44px;
box-sizing: border-box;
border: 1px solid transparent;
border-radius: 2px;
}
.edithere-minerva-icon.edithere-minerva-icon:hover {
background-color: var(--background-color-button-quiet--hover,rgba(0, 24, 73, .027));
}
.edithere-minerva-icon.edithere-minerva-icon:active {
background-color: var(--background-color-button-quiet--active,rgba(0, 24, 73, .082));
border-color: var(--border-color-interactive, #72777d);
}
.edithere-minerva-icon.edithere-minerva-icon:focus {
outline: 1px solid transparent;
}
.edithere-minerva-icon.edithere-minerva-icon:focus:not(:active) {
border-color: var(--border-color-progressive--focus, #36c);
box-shadow: inset 0 0 0 1px var(--box-shadow-color-progressive--focus, #36c);
}`);
})();