// WikiEditor Tranlate Integration
// Integrates WikiEditor and CodeMirror into [[mw:Help:Extension:Translate]]
// Author: [[User:Iniquity]]
(function() {
'use strict';
// Wait for page load
$(document).ready(function() {
// Fast initialization
initTranslateWikiEditor();
// Track DOM changes for new forms
observeNewForms();
});
function initTranslateWikiEditor() {
// Find all translatewiki.net translation fields
var $translationTextareas = $('textarea.tux-textarea-translation');
if (!$translationTextareas.length) {
// Fast retry
setTimeout(initTranslateWikiEditor, 100);
return;
}
// Initialize only visible fields that haven't been initialized yet
$translationTextareas.each(function() {
var $textarea = $(this);
// Check that textarea is visible and not initialized
if ($textarea.is(':visible') && !$textarea.data('wecm-tux-initialized')) {
initializeForm($textarea);
}
});
}
function initializeForm($textarea) {
// Check if this textarea is already initialized
if ($textarea.data('wecm-tux-initialized')) {
return;
}
// Set ID for compatibility (if not already set)
if (!$textarea.attr('id')) {
$textarea.attr('id', 'wpTextbox1-' + Date.now());
}
// Load resources and setup editor
loadResourcesAndSetup($textarea);
// Mark textarea as initialized
$textarea.data('wecm-tux-initialized', true);
}
function loadResourcesAndSetup($textarea) {
// Check MediaWiki API availability
if (typeof mw === 'undefined' || !mw.loader) {
return;
}
// Load WikiEditor and CodeMirror
mw.loader.using([
'ext.wikiEditor',
'ext.CodeMirror.v6.WikiEditor',
'ext.CodeMirror.v6.mode.mediawiki'
]).then(function(require) {
if (typeof mw.addWikiEditor === 'function') {
try {
mw.addWikiEditor($textarea);
$textarea.wikiEditor({
toolbar: {
gadgets: {
type: 'group',
label: 'Гаджеты'
},
format: {},
insert: {},
advanced: {}
}
});
// Attempt CodeMirror initialization (optional)
try {
const CodeMirrorWikiEditor = require('ext.CodeMirror.v6.WikiEditor');
const mediawikiLang = require('ext.CodeMirror.v6.mode.mediawiki');
if (typeof CodeMirrorWikiEditor === 'function' && typeof mediawikiLang === 'function') {
// Additional textarea checks before CodeMirror initialization
if ($textarea[0] &&
$textarea[0].tagName === 'TEXTAREA' &&
$textarea[0].value !== null &&
$textarea[0].name !== null &&
$textarea[0].id !== null) {
// Save original properties for compatibility
var originalTextarea = $textarea[0];
var originalReadOnly = originalTextarea.readOnly;
var originalDisabled = originalTextarea.disabled;
const cmWe = new CodeMirrorWikiEditor($textarea[0], mediawikiLang());
// Check that initialization was successful
if (!cmWe || typeof cmWe.initialize !== 'function') {
return;
}
// Set mode manually if not set
if (!cmWe.mode) {
cmWe.mode = 'mediawiki';
}
// Additional checks before initialization
if (!cmWe.$textarea) {
cmWe.$textarea = $textarea;
}
if (!cmWe.context) {
// Create minimal context
cmWe.context = {
$ui: $textarea.closest('.wikiEditor-ui'),
modules: {
toolbar: {
$toolbar: $textarea.closest('.wikiEditor-ui').find('.toolbar')
}
}
};
}
try {
cmWe.initialize();
} catch (initError) {
return;
}
// Check that view is created
if (!cmWe.view || !cmWe.view.dom) {
return;
}
// Restore properties for compatibility
Object.defineProperty(cmWe.view.dom, 'readOnly', {
get: function() { return originalReadOnly; },
set: function(value) { originalReadOnly = value; }
});
Object.defineProperty(cmWe.view.dom, 'disabled', {
get: function() { return originalDisabled; },
set: function(value) { originalDisabled = value; }
});
Object.defineProperty(cmWe.view.dom, 'tagName', {
get: function() { return 'TEXTAREA'; }
});
Object.defineProperty(cmWe.view.dom, 'value', {
get: function() { return cmWe.view.state.doc.toString(); },
set: function(value) {
cmWe.view.dispatch({
changes: { from: 0, to: cmWe.view.state.doc.length, insert: value }
});
}
});
// Save reference to editor
$textarea.data('codeMirrorEditor', cmWe);
// Add auto-resize functionality for CodeMirror
setupAutoResize(cmWe, $textarea);
// Add Ctrl+Enter (Cmd+Enter) save shortcut on .cm-content with capture:true
var cmContent = $(cmWe.view.dom).find('.cm-content')[0];
if (cmContent && !$textarea.data('wecm-tux-capture-keydown')) {
$textarea.data('wecm-tux-capture-keydown', true);
cmContent.addEventListener('keydown', function(e) {
const isCmdModifierPressed = $.client.profile().platform === 'mac' ? e.metaKey : e.ctrlKey;
if (isCmdModifierPressed && !e.shiftKey && !e.altKey && e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
var $btn = $('.tux-editor-save-button').filter(function() {
var $el = $(this);
return $el.is(':visible') && !$el.prop('disabled');
});
console.log('[edit-here] All candidate save buttons:', $('.tux-editor-save-button').toArray());
if ($btn.length) {
$btn.click();
console.log('[edit-here] Save button clicked from cm-content capture');
} else {
console.log('[edit-here] Save button not found from cm-content capture');
}
return false;
}
}, true);
}
// Register custom keymap for Ctrl+Enter in CodeMirror (works even if event does not bubble)
mw.loader.using('ext.CodeMirror.v6.lib').then(function(lib) {
if (cmWe && cmWe.view && typeof cmWe.view.applyExtension === 'function' && !$textarea.data('wecm-tux-keymap-registered')) {
$textarea.data('wecm-tux-keymap-registered', true);
var saveKeymap = lib.keymap.of([
{
key: "Ctrl-Enter",
run: function() {
var $btn = $('button[value="save"], input[value="save"], .save-button');
if ($btn.length) {
$btn.click();
console.log('[edit-here] Save button clicked from keymap');
} else {
console.log('[edit-here] Save button not found from keymap');
}
return true;
}
}
]);
cmWe.view.applyExtension(saveKeymap);
}
});
// -- Fallback: MutationObserver for CodeMirror content changes --
if (cmWe && cmWe.view && cmWe.view.dom && !$textarea.data('wecm-tux-mutation-observer')) {
$textarea.data('wecm-tux-mutation-observer', true);
var cmContent = $(cmWe.view.dom).find('.cm-content')[0];
var cmEditor = $(cmWe.view.dom).closest('.cm-editor')[0] || $(cmWe.view.dom).find('.cm-editor')[0];
if (cmContent) {
var lastText = cmWe.view.state.doc.toString();
var syncTimeout = null;
var observer = new MutationObserver(function(mutationsList, observer) {
clearTimeout(syncTimeout);
syncTimeout = setTimeout(function() {
var newText = cmWe.view.state.doc.toString();
if (newText !== lastText) {
lastText = newText;
$textarea.css('display', 'block').css('visibility', 'visible');
// Set direction and language from .cm-editor
if (cmEditor) {
var dir = cmEditor.getAttribute('dir') || 'ltr';
var lang = cmEditor.getAttribute('lang') || 'en';
$textarea.attr('dir', dir);
$textarea.attr('lang', lang);
$(cmEditor).find('.cm-content').attr('dir', dir).attr('lang', lang);
}
var ta = $textarea[0];
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.dispatchEvent(new Event('change', { bubbles: true }));
// Do not change value, focus or blur!
}
}, 50);
});
observer.observe(cmContent, { characterData: true, subtree: true, childList: true });
$textarea.data('wecm-tux-mutation-observer-instance', observer);
}
}
} else {
// Textarea not ready for CodeMirror
}
}
} catch (cmError) {
// CodeMirror unavailable, continue without it
}
// Load wikificator and add to toolbar
loadWikificatorAndSetup($textarea);
// Add keyboard shortcuts
addKeyboardShortcuts($textarea);
// Add syntax button handler
addSyntaxButtonHandler($textarea);
// Remove margin from all group-format on page after initialization
setTimeout(function() {
$('.wikiEditor-ui .group-format').each(function() {
this.style.setProperty('margin-left', '0', 'important');
});
}, 100);
} catch (e) {
// Error initializing WikiEditor
}
}
}).catch(function(error) {
// Failed to load WikiEditor, CodeMirror
});
}
function setupAutoResize(cmWe, $textarea) {
// Function to automatically resize CodeMirror editor based on content
function autoResize() {
if (!cmWe || !cmWe.view || !cmWe.view.dom) {
return;
}
// Get the CodeMirror editor element
var $cmEditor = $(cmWe.view.dom).closest('.cm-editor');
if (!$cmEditor.length) {
return;
}
// Get current content and calculate lines
var content = cmWe.view.state.doc.toString();
var lines = content.split('\n');
var lineCount = lines.length;
// Calculate minimum and maximum heights
var minHeight = 100; // Minimum height in pixels
var maxHeight = 600; // Maximum height in pixels
var lineHeight = 20; // Approximate line height in pixels
var padding = 40; // Padding for editor
// Calculate additional height for long lines that wrap
var editorWidth = $cmEditor.width() || 600; // Get current editor width
var charWidth = 8; // Approximate character width in pixels
var widthPadding = 20; // Account for padding and scrollbars
var charsPerLine = Math.floor((editorWidth - widthPadding) / charWidth);
var totalWrappedLines = 0;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// Calculate actual line width considering different character types
var lineWidth = 0;
for (var j = 0; j < line.length; j++) {
var char = line[j];
// Adjust width for different character types
if (char.charCodeAt(0) > 127) {
// Non-ASCII characters (like Cyrillic) are wider
lineWidth += charWidth * 1.2;
} else if (char === ' ' || char === '\t') {
// Spaces and tabs
lineWidth += charWidth * 0.5;
} else {
// Regular ASCII characters
lineWidth += charWidth;
}
}
// Check if line needs wrapping
if (lineWidth > editorWidth - widthPadding) {
// Calculate how many lines this long line will wrap to
var wrappedLines = Math.ceil(lineWidth / (editorWidth - widthPadding));
totalWrappedLines += (wrappedLines - 1); // Subtract 1 because we already count the original line
}
}
// Add extra height for wrapped lines
var wrappedHeight = totalWrappedLines * lineHeight;
var finalHeight = Math.max(minHeight, Math.min(maxHeight, lineCount * lineHeight + wrappedHeight + padding));
// Apply new height to CodeMirror editor
$cmEditor.css({
'height': finalHeight + 'px',
'min-height': minHeight + 'px',
'max-height': maxHeight + 'px',
'overflow-y': finalHeight >= maxHeight ? 'auto' : 'hidden'
});
// Also update the textarea height for compatibility
$textarea.css({
'height': finalHeight + 'px',
'min-height': minHeight + 'px',
'max-height': maxHeight + 'px'
});
// Force CodeMirror to update its layout
if (cmWe.view.requestMeasure) {
cmWe.view.requestMeasure();
}
}
// Initial resize
setTimeout(autoResize, 100);
// Listen for content changes using CodeMirror's update event
if (cmWe.view && cmWe.view.state) {
try {
// Try to use CodeMirror's built-in update listener
mw.loader.using('@codemirror/view').then(function(viewModule) {
if (cmWe.view && cmWe.view.state) {
// Add update listener
var updateListener = viewModule.EditorView.updateListener.of(function(update) {
if (update.docChanged) {
setTimeout(autoResize, 10);
}
});
// Apply the listener
cmWe.view.dispatch({
effects: updateListener
});
}
}).catch(function() {
// Fallback to MutationObserver if CodeMirror view module not available
setupMutationObserver();
});
} catch (e) {
// Fallback to MutationObserver
setupMutationObserver();
}
} else {
// Fallback to MutationObserver
setupMutationObserver();
}
function setupMutationObserver() {
var $cmContent = $(cmWe.view.dom).find('.cm-content');
if ($cmContent.length) {
var observer = new MutationObserver(function() {
setTimeout(autoResize, 10);
});
observer.observe($cmContent[0], {
childList: true,
subtree: true,
characterData: true
});
$textarea.data('wecm-tux-resize-observer', observer);
}
}
// Also listen for window resize events
$(window).off('resize.wecm-tux-resize').on('resize.wecm-tux-resize', function() {
setTimeout(autoResize, 100);
});
// Listen for input events on the CodeMirror editor for faster response
var $cmEditor = $(cmWe.view.dom).closest('.cm-editor');
if ($cmEditor.length) {
$cmEditor.off('input.wecm-tux-resize keyup.wecm-tux-resize').on('input.wecm-tux-resize keyup.wecm-tux-resize', function() {
setTimeout(autoResize, 50);
});
}
}
function loadWikificatorAndSetup($textarea) {
// Load wikificator directly from ru.wikipedia.org
loadWikificatorFallback($textarea);
}
function loadWikificatorFallback($textarea) {
// Fallback: load via $.getScript
$.getScript('//ru.wikipedia.org/w/index.php?title=MediaWiki:Gadget-wikificator.js&action=raw&ctype=text/javascript')
.done(function() {
addWikificatorToToolbar($textarea);
})
.fail(function(jqXHR, textStatus, errorThrown) {
// Wikificator not loaded
});
}
function addWikificatorToToolbar($textarea) {
// Wait for toolbar readiness (like in edit-here-config.js)
var $wikiEditorUI = $textarea.closest('.wikiEditor-ui');
var toolbarReady = $wikiEditorUI.length && $wikiEditorUI.find('.toolbar').length;
if (toolbarReady) {
try {
var gadgetsTools = {
wikificator: {
label: 'Викификатор — автоматический обработчик текста (Ctrl+Alt+W)',
type: 'button',
icon: 'https://upload.wikimedia.org/wikipedia/commons/0/06/Wikify-toolbutton.png',
action: {
type: 'callback',
execute: function() {
if (typeof window.Wikify === 'function') {
window.Wikify($textarea[0]);
} else {
// Wikify not found
}
}
}
}
};
$textarea.wikiEditor('addToToolbar', {
section: 'main',
groups: {
gadgets: {
tools: gadgetsTools
}
}
});
// Move gadgets group to the beginning of toolbar
$wikiEditorUI.find('.group-gadgets').insertBefore($wikiEditorUI.find('.section-main .group-format'));
// Remove temporary margin from group-format
$wikiEditorUI.find('.group-format').each(function() {
this.style.setProperty('margin-left', '0', 'important');
});
// Remove CSS rule from styles
$('style').each(function() {
var $style = $(this);
var cssText = $style.text();
if (cssText.includes('.wikiEditor-ui .group-format') && cssText.includes('margin-left: 34px !important')) {
var newCssText = cssText.replace(/\.wikiEditor-ui \.group-format\s*\{\s*margin-left:\s*34px\s*!important;\s*\}/g, '');
$style.text(newCssText);
}
});
// Remove margin from all group-format on page
$('.wikiEditor-ui .group-format').each(function() {
this.style.setProperty('margin-left', '0', 'important');
});
} catch (e) {
// Error integrating Wikificator
}
} else {
// Retry after 500ms
setTimeout(function() {
addWikificatorToToolbar($textarea);
}, 500);
}
}
function addKeyboardShortcuts($textarea) {
// Remove previous handlers
$textarea.off('keydown.wecm-tux');
// Add to textarea (in case focus is there)
$textarea.on('keydown.wecm-tux', handleShortcut);
// Add to CodeMirror content if present
var cmWe = $textarea.data('codeMirrorEditor');
if (cmWe && cmWe.view && cmWe.view.dom) {
var $cmContent = $(cmWe.view.dom).find('.cm-content');
$cmContent.off('keydown.wecm-tux');
$cmContent.on('keydown.wecm-tux', handleShortcut);
}
function handleShortcut(e) {
const isCmdModifierPressed = $.client.profile().platform === 'mac' ? e.metaKey : e.ctrlKey;
// Ctrl+Enter for save
if (isCmdModifierPressed && !e.shiftKey && !e.altKey && e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
$('button[value="save"], input[value="save"], .save-button').click();
return false;
}
// Ctrl+Alt+W for wikificator
if (isCmdModifierPressed && !e.shiftKey && e.altKey && e.keyCode === 87) {
e.preventDefault();
e.stopPropagation();
if (typeof window.Wikify === 'function') {
window.Wikify($textarea[0]);
}
return false;
}
// ALT+SHIFT+D for skip
if (!isCmdModifierPressed && e.shiftKey && e.altKey && e.keyCode === 68) {
e.preventDefault();
e.stopPropagation();
$('.tux-editor-skip-button').click();
return false;
}
// ALT+SHIFT+B for edit summary
if (!isCmdModifierPressed && e.shiftKey && e.altKey && e.keyCode === 66) {
e.preventDefault();
e.stopPropagation();
$('.tux-input-editsummary').focus();
return false;
}
}
}
function runWikificator(textareaElement) {
if (typeof window.Wikify === 'function') {
window.Wikify(textareaElement);
showNotification('Wikificator started', 'success');
} else {
showNotification('Wikificator unavailable', 'error');
}
}
// Add CSS styles for proper WikiEditor and CodeMirror display
function addStyles() {
var styles = `
/* Force show toolbar and WikiEditor tabs even for readonly/inactive CodeMirror */
.wikiEditor-ui.ext-codemirror-readonly .wikiEditor-section-secondary,
.wikiEditor-ui:not(.ext-codemirror-mediawiki) .wikiEditor-section-secondary,
.wikiEditor-ui.ext-codemirror-readonly .tabs,
.wikiEditor-ui:not(.ext-codemirror-mediawiki) .tabs,
.wikiEditor-ui.ext-codemirror-readonly .sections,
.wikiEditor-ui:not(.ext-codemirror-mediawiki) .sections {
display: block !important;
}
.wikiEditor-ui:not(.ext-codemirror-mediawiki) .group:not(.group-codemirror):not(.group-codemirror-format):not(.group-codemirror-preferences):not(.group-codemirror-search),
.wikiEditor-ui.ext-codemirror-readonly .group:not(.group-codemirror):not(.group-codemirror-format):not(.group-codemirror-preferences):not(.group-codemirror-search) {
display: flex;
}
/* Proper cursor for text fields */
.wikiEditor-ui textarea,
.wikiEditor-ui .wikiEditor-ui-text textarea,
.wikiEditor-ui .wikiEditor-ui-text .wikiEditor-ui-text textarea {
cursor: text !important;
}
/* Cursor for CodeMirror */
.wikiEditor-ui .cm-editor {
cursor: text !important;
transition: height 0.2s ease-in-out;
}
/* Auto-resize styles for CodeMirror */
.wikiEditor-ui .cm-editor.cm-focused {
outline: none !important;
}
.wikiEditor-ui .cm-editor .cm-scroller {
overflow-x: auto !important;
}
.wikiEditor-ui .cm-editor .cm-content {
white-space: pre-wrap !important;
word-wrap: break-word !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* Cursor for buttons and controls */
.wikiEditor-ui .tool,
.wikiEditor-ui .toolbar .tool,
.wikiEditor-ui button,
.wikiEditor-ui input[type="button"],
.wikiEditor-ui input[type="submit"] {
cursor: pointer !important;
}
/* Reserve space for wikificator button */
.wikiEditor-ui .group-gadgets {
min-width: 34px !important;
}
/* Temporarily move group-format */
.wikiEditor-ui .group-format {
margin-left: 34px !important;
}
`;
var $style = $('<style>').text(styles);
$('head').append($style);
}
function cleanupPreviousEditors() {
// Clean up previous CodeMirror editors and observers
$('textarea.tux-textarea-translation').each(function() {
var $textarea = $(this);
// Clean up resize observer
var resizeObserver = $textarea.data('wecm-tux-resize-observer');
if (resizeObserver && typeof resizeObserver.disconnect === 'function') {
resizeObserver.disconnect();
$textarea.removeData('wecm-tux-resize-observer');
}
// Clean up mutation observer
var mutationObserver = $textarea.data('wecm-tux-mutation-observer-instance');
if (mutationObserver && typeof mutationObserver.disconnect === 'function') {
mutationObserver.disconnect();
$textarea.removeData('wecm-tux-mutation-observer-instance');
}
// Remove event handlers
$textarea.off('keydown.wecm-tux');
$(window).off('resize.wecm-tux-resize');
// Remove CodeMirror editor event handlers
var cmWe = $textarea.data('codeMirrorEditor');
if (cmWe && cmWe.view && cmWe.view.dom) {
var $cmEditor = $(cmWe.view.dom).closest('.cm-editor');
if ($cmEditor.length) {
$cmEditor.off('input.wecm-tux-resize keyup.wecm-tux-resize');
}
}
// Reset textarea styles
$textarea.css({
'height': '',
'min-height': '',
'max-height': ''
});
// Remove CodeMirror editor reference
$textarea.removeData('codeMirrorEditor');
$textarea.removeData('wecm-tux-initialized');
});
}
function observeNewForms() {
// Fast tracking of clicks on links for new form initialization
$(document).on('click', 'a[href*="action=translate"], .tux-editor-link', function() {
// Clean up previous editors before switching
cleanupPreviousEditors();
// Add margin when switching forms
addMarginToGroupFormat();
setTimeout(function() {
initTranslateWikiEditor();
}, 100);
});
// More frequent check for new forms (every 500ms)
setInterval(function() {
initTranslateWikiEditor();
}, 500);
}
function addMarginToGroupFormat() {
// Add margin to all group-format on page
$('.wikiEditor-ui .group-format').each(function() {
this.style.setProperty('margin-left', '34px', 'important');
});
// Add CSS rule back
var $existingStyle = $('style').filter(function() {
return $(this).text().includes('wecm-tux-wikieditor');
}).first();
if ($existingStyle.length) {
var cssText = $existingStyle.text();
if (!cssText.includes('.wikiEditor-ui .group-format') || !cssText.includes('margin-left: 34px !important')) {
var newRule = '\n /* Temporarily move group-format */\n .wikiEditor-ui .group-format {\n margin-left: 34px !important;\n }';
$existingStyle.text(cssText + newRule);
}
}
}
// Add styles on initialization
addStyles();
// Global Ctrl+Enter handler for save (works even if CodeMirror eats the event)
$(document).off('keydown.wecm-tux-global').on('keydown.wecm-tux-global', function(e) {
const isCmdModifierPressed = $.client.profile().platform === 'mac' ? e.metaKey : e.ctrlKey;
if (isCmdModifierPressed && !e.shiftKey && !e.altKey && e.keyCode === 13) {
console.log('[edit-here] Global Ctrl+Enter handler fired');
e.preventDefault();
e.stopPropagation();
var $btn = $('button[value="save"], input[value="save"], .save-button');
console.log('[edit-here] Save button:', $btn[0], 'disabled:', $btn.prop('disabled'), 'opacity:', $btn.css('opacity'), 'class:', $btn.attr('class'));
if ($btn.length) {
$btn.click();
console.log('[edit-here] Save button clicked');
} else {
console.log('[edit-here] Save button not found');
}
return false;
}
});
})();