User:Iniquity/translateEditor.js
Appearance
From mediawiki.org
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.
// 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(); }); functioninitTranslateWikiEditor(){ // 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); } }); } functioninitializeForm($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); } functionloadResourcesAndSetup($textarea){ // Check MediaWiki API availability if(typeofmw==='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(typeofmw.addWikiEditor==='function'){ try{ mw.addWikiEditor($textarea); $textarea.wikiEditor({ toolbar:{ gadgets:{ type:'group', label:'Гаджеты' }, format:{}, insert:{}, advanced:{} } }); // Attempt CodeMirror initialization (optional) try{ constCodeMirrorWikiEditor=require('ext.CodeMirror.v6.WikiEditor'); constmediawikiLang=require('ext.CodeMirror.v6.mode.mediawiki'); if(typeofCodeMirrorWikiEditor==='function'&&typeofmediawikiLang==='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 varoriginalTextarea=$textarea[0]; varoriginalReadOnly=originalTextarea.readOnly; varoriginalDisabled=originalTextarea.disabled; constcmWe=newCodeMirrorWikiEditor($textarea[0],mediawikiLang()); // Check that initialization was successful if(!cmWe||typeofcmWe.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(){returnoriginalReadOnly;}, set:function(value){originalReadOnly=value;} }); Object.defineProperty(cmWe.view.dom,'disabled',{ get:function(){returnoriginalDisabled;}, set:function(value){originalDisabled=value;} }); Object.defineProperty(cmWe.view.dom,'tagName',{ get:function(){return'TEXTAREA';} }); Object.defineProperty(cmWe.view.dom,'value',{ get:function(){returncmWe.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 varcmContent=$(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){ constisCmdModifierPressed=$.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'); } returnfalse; } },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&&typeofcmWe.view.applyExtension==='function'&&!$textarea.data('wecm-tux-keymap-registered')){ $textarea.data('wecm-tux-keymap-registered',true); varsaveKeymap=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'); } returntrue; } } ]); 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); varcmContent=$(cmWe.view.dom).find('.cm-content')[0]; varcmEditor=$(cmWe.view.dom).closest('.cm-editor')[0]||$(cmWe.view.dom).find('.cm-editor')[0]; if(cmContent){ varlastText=cmWe.view.state.doc.toString(); varsyncTimeout=null; varobserver=newMutationObserver(function(mutationsList,observer){ clearTimeout(syncTimeout); syncTimeout=setTimeout(function(){ varnewText=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){ vardir=cmEditor.getAttribute('dir')||'ltr'; varlang=cmEditor.getAttribute('lang')||'en'; $textarea.attr('dir',dir); $textarea.attr('lang',lang); $(cmEditor).find('.cm-content').attr('dir',dir).attr('lang',lang); } varta=$textarea[0]; ta.dispatchEvent(newEvent('input',{bubbles:true})); ta.dispatchEvent(newEvent('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 }); } functionsetupAutoResize(cmWe,$textarea){ // Function to automatically resize CodeMirror editor based on content functionautoResize(){ 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 varcontent=cmWe.view.state.doc.toString(); varlines=content.split('\n'); varlineCount=lines.length; // Calculate minimum and maximum heights varminHeight=150;// Minimum height in pixels varmaxHeight=600;// Maximum height in pixels varlineHeight=20;// Approximate line height in pixels varpadding=40;// Padding for editor // Calculate additional height for long lines that wrap vareditorWidth=$cmEditor.width()||600;// Get current editor width varcharWidth=8;// Approximate character width in pixels varwidthPadding=20;// Account for padding and scrollbars varcharsPerLine=Math.floor((editorWidth-widthPadding)/charWidth); vartotalWrappedLines=0; for(vari=0;i<lines.length;i++){ varline=lines[i]; // Calculate actual line width considering different character types varlineWidth=0; for(varj=0;j<line.length;j++){ varchar=line[j]; // Adjust width for different character types if(char.charCodeAt(0)>127){ // Non-ASCII characters (like Cyrillic) are wider lineWidth+=charWidth*1.2; }elseif(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 varwrappedLines=Math.ceil(lineWidth/(editorWidth-widthPadding)); totalWrappedLines+=(wrappedLines-1);// Subtract 1 because we already count the original line } } // Add extra height for wrapped lines varwrappedHeight=totalWrappedLines*lineHeight; varfinalHeight=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 varupdateListener=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(); } functionsetupMutationObserver(){ var$cmContent=$(cmWe.view.dom).find('.cm-content'); if($cmContent.length){ varobserver=newMutationObserver(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); }); } } functionloadWikificatorAndSetup($textarea){ // Load wikificator directly from ru.wikipedia.org loadWikificatorFallback($textarea); } functionloadWikificatorFallback($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 }); } functionaddWikificatorToToolbar($textarea){ // Wait for toolbar readiness (like in edit-here-config.js) var$wikiEditorUI=$textarea.closest('.wikiEditor-ui'); vartoolbarReady=$wikiEditorUI.length&&$wikiEditorUI.find('.toolbar').length; if(toolbarReady){ try{ vargadgetsTools={ 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(typeofwindow.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); varcssText=$style.text(); if(cssText.includes('.wikiEditor-ui .group-format')&&cssText.includes('margin-left: 34px !important')){ varnewCssText=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); } } functionaddKeyboardShortcuts($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 varcmWe=$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); } functionhandleShortcut(e){ constisCmdModifierPressed=$.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(); returnfalse; } // Ctrl+Alt+W for wikificator if(isCmdModifierPressed&&!e.shiftKey&&e.altKey&&e.keyCode===87){ e.preventDefault(); e.stopPropagation(); if(typeofwindow.Wikify==='function'){ window.Wikify($textarea[0]); } returnfalse; } // ALT+SHIFT+D for skip if(!isCmdModifierPressed&&e.shiftKey&&e.altKey&&e.keyCode===68){ e.preventDefault(); e.stopPropagation(); $('.tux-editor-skip-button').click(); returnfalse; } // ALT+SHIFT+B for edit summary if(!isCmdModifierPressed&&e.shiftKey&&e.altKey&&e.keyCode===66){ e.preventDefault(); e.stopPropagation(); $('.tux-input-editsummary').focus(); returnfalse; } } } functionrunWikificator(textareaElement){ if(typeofwindow.Wikify==='function'){ window.Wikify(textareaElement); showNotification('Wikificator started','success'); }else{ showNotification('Wikificator unavailable','error'); } } // Add CSS styles for proper WikiEditor and CodeMirror display functionaddStyles(){ varstyles=` /* 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); } functioncleanupPreviousEditors(){ // Clean up previous CodeMirror editors and observers $('textarea.tux-textarea-translation').each(function(){ var$textarea=$(this); // Clean up resize observer varresizeObserver=$textarea.data('wecm-tux-resize-observer'); if(resizeObserver&&typeofresizeObserver.disconnect==='function'){ resizeObserver.disconnect(); $textarea.removeData('wecm-tux-resize-observer'); } // Clean up mutation observer varmutationObserver=$textarea.data('wecm-tux-mutation-observer-instance'); if(mutationObserver&&typeofmutationObserver.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 varcmWe=$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'); }); } functionobserveNewForms(){ // 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); } functionaddMarginToGroupFormat(){ // 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){ varcssText=$existingStyle.text(); if(!cssText.includes('.wikiEditor-ui .group-format')||!cssText.includes('margin-left: 34px !important')){ varnewRule='\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){ constisCmdModifierPressed=$.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'); } returnfalse; } }); })();