Wikipediýa tkwiki https://tk.wikipedia.org/wiki/Ba%C5%9F_Sahypa MediaWiki 1.46.0-wmf.24 first-letter Media Ýörite Çekişme Ulanyjy Ulanyjy çekişme Wikipediýa Wikipediýa çekişme Faýl Faýl çekişme MediaWiki MediaWiki çekişme Şablon Şablon çekişme Ýardam Ýardam çekişme Kategoriýa Kategoriýa çekişme TimedText TimedText talk Module Module talk Event Event talk Şablon:Administrator taryh 10 23979 268736 265437 2026-04-27T18:03:57Z Umarxon III 11129 268736 wikitext text/x-wiki <!-- Based off [[m:Template:StewardsChart]] --> <timeline> ImageSize = width:1008 height:auto barincrement:20 PlotArea = right:20 left:20 top:5 bottom:60 Legend = position:bottom orientation:horizontal Colors = id:bg value:rgb(0.9,0.9,1) id:major value:black id:minor value:rgb(0.8,0.8,0.8) id:text value:black id:header value:rgb(0.6,0.6,0.9) id:htext value:white id:current value:rgb(0.5,0.9,0.5) legend:Häzirki id:former value:rgb(0.8,0.8,0.8) legend:Öňki id:temporary value:red legend:Wagtlaýyn BackgroundColors = canvas:bg TimeAxis = orientation:horizontal DateFormat = dd/mm/yyyy Period = from:01/01/2006 till:01/01/2027 ScaleMajor = gridcolor:major unit:year increment:1 start:01/01/2006 ScaleMinor = gridcolor:minor unit:month increment:1 start:01/01/2006 BarData = Bar:HAdmins Barset:SAdmins PlotData = width:15 textcolor:text bar:HAdmins color:header textcolor:htext width:20 shift:(-95,-5) fontsize:m from:01/01/2006 till:end text:"Administratorlar" barset:SAdmins shift:(5,-5) anchor:from fontsize:m color:former from:15/06/2006 till:14/06/2009 text:"[[Ulanyjy:Parahat|Parahat]]" color:temporary from:18/04/2007 till:18/04/2007 text:"[[Ulanyjy:(:Julien:)|(:Julien:)]]" color:former from:11/05/2007 till:12/07/2007 text:"[[Ulanyjy:.anaconda|.anaconda]]" color:temporary from:21/08/2007 till:03/09/2007 text:"[[Ulanyjy:Thunderhead|Thunderhead]]" color:temporary from:07/12/2007 till:07/12/2007 text:"[[Ulanyjy:Pathoschild|Pathoschild]]" color:temporary from:25/01/2008 till:25/01/2008 text:"[[Ulanyjy:Nick1915|Nick1915]]" color:former from:28/01/2008 till:28/05/2008 text:"[[Ulanyjy:DerHexer|DerHexer]]" color:temporary from:18/05/2008 till:18/05/2008 text:"[[Ulanyjy:Spacebirdy|Spacebirdy]]" color:former from:12/03/2009 till:09/12/2016 text:"[[Ulanyjy:Hanberke|Hanberke]]" color:former from:02/08/2018 till:03/05/2020 text:"[[Ulanyjy:Ruhubelent|Ruhubelent]]" color:former from:08/05/2019 till:30/11/2019 text:"[[Ulanyjy:Turkmen|Turkmen]]" color:former from:21/08/2020 till:21/11/2020 text:"[[Ulanyjy:Muhammetnyýaz|Muhammetnyýaz]]" color:former from:20/11/2021 till:25/08/2025 text:"[[Ulanyjy:Styyx|Styyx]]" color:former from:06/09/2022 till:12/08/2025 text:"[[Ulanyjy:Allanur77|Allanur77]]" color:current from:16/05/2024 till:end text:"[[Ulanyjy:Umarxon III|Umarxon III]]" </timeline> jfjmfl1mghi62zkt4k1wm6l2gxb4wtj Bäşow Batyr Hojaberdiýewiç 0 24337 268693 268016 2026-04-27T15:00:06Z ~2026-19740-09 33551 268693 wikitext text/x-wiki [[Faýl:Bäşow Batyr Hojaberdiýewiç.jpg|thumb|Bäşow Batyr ]] {{Şahsyýet_maglumaty|ady=Bäşow Batyr Hojaberdiýewiç|surat=|surat_ulylygy=|surat_ýazgysy=|doglan_senesi=22.10.1982|doglan_ýeri=[[Türkmenistan]]|hünäri=Inžener-gidrotehnik, döwlet işgäri|bilimi=[[Türkmen oba hojalyk uniwersiteti]] (2005)|kakasy=[[Bäşow Hojaberdi]]}} '''Bäşow Batyr Hojaberdiýewiç''' (22-nji oktýabr 1982) — türkmen döwlet işgäri, inžener-gidrotehnik. Ol Türkmenistanyň suw hojalygy we nebit-gaz ulgamlarynda dürli ýolbaşçy wezipelerde zähmet çekip gelýär. == Bilimi == Batyr Bäşow 2001—2005-nji ýyllar aralygynda [[S.A.Nyýazow adyndaky Türkmen Oba Hojalyk Uniwersiteti|S.A.Nyýazow adyndaky Türkmen oba hojalyk uniwersiteti]] Gidromeliorasiýa fakultetinde bilim aldy we ony inžener-gidrotehnik hünäri boýunça tamamlady. == Zähmet ýoly == Batyr Bäşowyň iş tejribesi ýurduň strategik ähmiýetli gurluşyk we energetika desgalary bilen berk baglanyşyklydyr: * '''2007-nji ýyl:''' [[Lebap welaýaty|Lebap welaýatynyň]] [[Kerki etraby|Kerki etrabyndaky]] "Garagumderýagurluşyk" müdiriýetiniň başlygy wezipesinde işledi. * '''2008-nji ýyl:''' Gyşyň aşa sowyk gelen döwründe Garagum derýasynyň doňmagy bilen bagly emele gelen çylşyrymly ýagdaýda derýanyň doňuny çözmek işlerine ýolbaşçylyk etdi. Bu taryhy waka Türkmenistanyň Gahryman Arkadagymyz Gurbanguly Berdimuhamedowyň göni goldawy bilen amala aşyrylyp, sebitiň oba hojalygy uly ýitgilerden halas edildi. * '''2013—2015-nji ýyllar:''' [[Mary welaýaty|Mary]] şäherindäki "Garagumderýagurluşyk" birleşiginiň başlygy. * '''2015—2017-nji ýyllar:''' "Tejenderýagurluşyk" edarasynyň başlygy. * '''2017-nji ýyldan häzirki wagta çenli:''' [[Türkmenistanyň Nebit we gaz ministrligi|Türkmenistanyň Nebit we gaz ministrliginiň]] (Nebit-gaz toplumynyň) ulgamynda jogapkärli wezipelerde işleýär. == Maşgalasy == Onuň kakasy — tanymal lukman, Mary welaýat hassahanasynyň bölüm müdiri bolup işlän we ady köçä dakylyp hatyralanan [[Bäşow Hojaberdi|Hojaberdi Bäşowdyr]] (1959—2022). == Kategoriýalar == 7yspzu0273n7z724xz8tkbg23jio544 268694 268693 2026-04-27T15:00:44Z ~2026-19740-09 33551 268694 wikitext text/x-wiki [[Faýl:Bäşow Batyr Hojaberdiýewiç.jpg|thumb|Bäşow Batyr ]] {{Şahsyýet_maglumaty|ady=Bäşow Batyr Hojaberdiýewiç|surat=|surat_ulylygy=|surat_ýazgysy=|doglan_senesi=22.10.1982|doglan_ýeri=[[Türkmenistan]]|hünäri=Inžener-gidrotehnik, döwlet işgäri|bilimi=[[Türkmen oba hojalyk uniwersiteti]] (2005)|kakasy=[[Bäşow Hojaberdi]]}} '''Bäşow Batyr Hojaberdiýewiç''' (22-nji oktýabr 1982) — türkmen döwlet işgäri, inžener-gidrotehnik. Ol Türkmenistanyň suw hojalygy we nebit-gaz ulgamlarynda dürli ýolbaşçy wezipelerde zähmet çekip gelýär. == Bilimi == Batyr Bäşow 2001—2005-nji ýyllar aralygynda [[S.A.Nyýazow adyndaky Türkmen Oba Hojalyk Uniwersiteti|S.A.Nyýazow adyndaky Türkmen oba hojalyk uniwersiteti]] Gidromeliorasiýa fakultetinde bilim aldy we ony inžener-gidrotehnik hünäri boýunça tamamlady. == Zähmet ýoly == Batyr Bäşowyň iş tejribesi ýurduň strategik ähmiýetli gurluşyk we energetika desgalary bilen berk baglanyşyklydyr: * '''2007-nji ýyl:''' [[Lebap welaýaty|Lebap welaýatynyň]] [[Kerki etraby|Kerki etrabyndaky]] "Garagumderýagurluşyk" müdiriýetiniň başlygy wezipesinde işledi. * '''2008-nji ýyl:''' Gyşyň aşa sowyk gelen döwründe Garagum derýasynyň doňmagy bilen bagly emele gelen çylşyrymly ýagdaýda derýanyň doňuny çözmek işlerine ýolbaşçylyk etdi. Bu taryhy waka Türkmenistanyň Gahryman Arkadagymyz Gurbanguly Berdimuhamedowyň göni goldawy bilen amala aşyrylyp, sebitiň oba hojalygy uly ýitgilerden halas edildi. * '''2013—2015-nji ýyllar:''' [[Mary welaýaty|Mary]] şäherindäki "Garagumderýagurluşyk" birleşiginiň başlygy. * '''2015—2017-nji ýyllar:''' "Tejenderýagurluşyk" edarasynyň başlygy. * '''2017-nji ýyldan häzirki wagta çenli:''' [[Türkmenistanyň Nebit we gaz ministrligi|Türkmenistanyň Nebit we gaz ministrliginiň]] (Nebit-gaz toplumynyň) ulgamynda jogapkärli wezipelerde işleýär. == Maşgalasy == Onuň kakasy — tanymal lukman, Mary welaýat hassahanasynyň bölüm müdiri bolup işlän we ady köçä dakylyp hatyralanan [[Bäşow Hojaberdi|Hojaberdi Bäşowdyr]] (1959—2023). == Kategoriýalar == 8ge9095a3egl74udt8bt26xattr9shv MediaWiki:Gadget-GoogleTrans 8 24458 268695 2026-04-27T15:37:08Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-external}}">(E)</abbr></sup> <sup><abbr title="{{int:gadgets-user}}">(U)</abbr></sup> [[User:Endo999/GoogleTrans|GoogleTrans]]: Shift düwmesine basylanda saýlanan tekst ýa-da kursoryň aşagyndaky söz üçin terjime penjiresini açmek' 268695 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-external}}">(E)</abbr></sup> <sup><abbr title="{{int:gadgets-user}}">(U)</abbr></sup> [[User:Endo999/GoogleTrans|GoogleTrans]]: Shift düwmesine basylanda saýlanan tekst ýa-da kursoryň aşagyndaky söz üçin terjime penjiresini açmek 0qdojtmpxznmem9mqo8a8w8xjpvaoxw MediaWiki:Gadget-GoogleTrans.js 8 24459 268696 2026-04-27T15:38:38Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// _________________________________________________________________________________________ // | | // | === WARNING: GLOBAL GADGET FILE === | // | Changes to this page affect many users. | // | Please discuss changes on the talk page or on [[Wikipedia_talk:Gadget]] before edit...' 268696 javascript text/javascript // _________________________________________________________________________________________ // | | // | === WARNING: GLOBAL GADGET FILE === | // | Changes to this page affect many users. | // | Please discuss changes on the talk page or on [[Wikipedia_talk:Gadget]] before editing. | // |_________________________________________________________________________________________| // // Translation tool that uses the "Google Translate" API. // Opens a translation popup for selected text or word under the cursor when pushing the shift button. // imports [[User:Endo999/GoogleTrans.js]] mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Endo999/GoogleTrans.js&action=raw&ctype=text/javascript'); pwricdt5ujfs01o1xinwtcialnhr0hj MediaWiki:Gadget-ImageAnnotator 8 24460 268697 2026-04-27T15:40:37Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[:commons:Help:Gadget-ImageAnnotator|ImageAnnotator]]: faýl düşündiriş sahypalarynda surat belliklerini we teswirlerini görmek' 268697 wikitext text/x-wiki [[:commons:Help:Gadget-ImageAnnotator|ImageAnnotator]]: faýl düşündiriş sahypalarynda surat belliklerini we teswirlerini görmek gvz4n819s2siwy2zwufvrmolwf4tqhn MediaWiki:Gadget-ImageAnnotator.js 8 24461 268698 2026-04-27T15:41:17Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* ImageAnnotator v2.3.2 Image annotations. Draw rectangles onto image thumbnail displayed on image description page and associate them with textual descriptions that will be displayed when the mouse moves over the rectangles. If an image has annotations, display the rectangles. Add a button to create new annotations. Note: if an image that has annotations is overwritten by a new version, only display the annotations if the size of the top image m...' 268698 javascript text/javascript /* ImageAnnotator v2.3.2 Image annotations. Draw rectangles onto image thumbnail displayed on image description page and associate them with textual descriptions that will be displayed when the mouse moves over the rectangles. If an image has annotations, display the rectangles. Add a button to create new annotations. Note: if an image that has annotations is overwritten by a new version, only display the annotations if the size of the top image matches the stored size exactly. To recover annotations, one will need to edit the image description page manually, adjusting image sizes and rectangle coordinates, or re-enter annotations. Author: [[User:Lupo]], June 2009 - March 2010 License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0) Choose whichever license of these you like best :-) See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation. */ /* global importScript, importScriptURI, LAPI, Tooltip, Tooltips, TextCleaner, UIElements, Buttons, ImageAnnotator, ImageAnnotator_disable */ /* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define, eqeqeq, no-alert, no-loop-func, no-inner-declarations */ if ( typeof ImageAnnotator === 'undefined' ) { // Guard against multiple inclusions importScript( 'MediaWiki:LAPI.js' ); importScript( 'MediaWiki:Tooltips.js' ); importScript( 'MediaWiki:TextCleaner.js' ); importScript( 'MediaWiki:UIElements.js' ); ( function () { // Local scope var ImageAnnotator_config = null; var ImageAnnotation = function () { this.initialize.apply( this, arguments ); }; ImageAnnotation.compare = function ( a, b ) { var result = b.area() - a.area(); if ( result !== 0 ) { return result; } // Just to make sure the order is complete return a.model.id - b.model.id; }; ImageAnnotation.prototype = { // Rectangle to be displayed on image: a div with pos and size view: null, // Internal representation of the annotation model: null, // Tooltip to display the annotation tooltip: null, // Content of the tooltip content: null, // Reference to the viewer this note belongs to viewer: null, initialize: function ( node, viewer, id ) { var is_new = false; var view_w = 0, view_h = 0, view_x = 0, view_y = 0; this.viewer = viewer; if ( LAPI.DOM.hasClass( node, IA.annotation_class ) ) { // Extract the info we need var x = IA.getIntItem( 'view_x_' + id, viewer.scope ); var y = IA.getIntItem( 'view_y_' + id, viewer.scope ); var w = IA.getIntItem( 'view_w_' + id, viewer.scope ); var h = IA.getIntItem( 'view_h_' + id, viewer.scope ); var html = IA.getRawItem( 'content_' + id, viewer.scope ); if ( x === null || y === null || w === null || h === null || html === null ) { throw new Error( 'Invalid note' ); } if ( x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height ) { throw new Error( 'Invalid note: origin invalid on note ' + id ); } if ( x + w > viewer.full_img.width + 10 || y + h > viewer.full_img.height + 10 ) { throw new Error( 'Invalid note: size extends beyond image on note ' + id ); } // Notes written by early versions may be slightly too large, whence the + 10 above. Fix this. if ( x + w > viewer.full_img.width ) { w = viewer.full_img.width - x; } if ( y + h > viewer.full_img.height ) { h = viewer.full_img.height - y; } view_w = Math.floor( w / viewer.factors.dx ); view_h = Math.floor( h / viewer.factors.dy ); view_x = Math.floor( x / viewer.factors.dx ); view_y = Math.floor( y / viewer.factors.dy ); this.view = LAPI.make( 'div', null, { position: 'absolute', display: 'none', lineHeight: '0px', // IE fontSize: '0px', // IE top: String( view_y ) + 'px', left: String( view_x ) + 'px', width: String( view_w ) + 'px', height: String( view_h ) + 'px' } ); // We'll add the view to the DOM once we've loaded all notes this.model = { id: id, dimension: { x: x, y: y, w: w, h: h }, wiki: '', html: html.cloneNode( true ) }; } else { is_new = true; this.view = node; this.model = { id: -1, dimension: null, wiki: '', html: null }; view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths view_h = this.view.offsetHeight - 2; view_x = this.view.offsetLeft; view_y = this.view.offsetTop; } // Enforce a minimum size of the view. Center the 6x6px square over the center of the old view. // If we overlap the image boundary, adjustRectangleSize will take care of it later. if ( view_w < 6 ) { view_x = Math.floor( view_x + view_w / 2 - 3 ); view_w = 6; } if ( view_h < 6 ) { view_y = Math.floor( view_y + view_h / 2 - 3 ); view_h = 6; } Object.merge( { left: String( view_x ) + 'px', top: String( view_y ) + 'px', width: String( view_w ) + 'px', height: String( view_h ) + 'px' }, this.view.style ); this.view.style.zIndex = 500; // Below tooltips try { this.view.style.border = '1px solid ' + this.viewer.outer_border; } catch ( ex ) { this.view.style.border = '1px solid ' + IA.outer_border; } this.view.appendChild( LAPI.make( 'div', null , { lineHeight: '0px', // IE fontSize: '0px', // IE width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border height: String( Math.max( view_h - 2, 0 ) ) + 'px' } ) // width=100% doesn't work right: inner div's border appears outside on right and bottom on FF. ); try { this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border; } catch ( ex ) { this.view.firstChild.style.border = '1px solid ' + IA.inner_border; } if ( is_new ) { viewer.adjustRectangleSize( this.view ); } // IE somehow passes through event to the view even if covered by our cover, displaying the tooltips // when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just // display raw rectangles without any tooltips attached while drawing. Yuck. this.dummy = this.view.cloneNode( true ); viewer.img_div.appendChild( this.dummy ); if ( !is_new ) { // New notes get their tooltip only once the editor has saved, otherwise IE may try to // open them if the mouse moves onto the view even though there is the cover above them! this.setTooltip(); } }, setTooltip: function () { if ( this.tooltip || !this.view ) { return; } // Already set, or corrupt // Note: on IE, don't have tooltips appear automatically. IE doesn't do it right for transparent // targets and we have to show and hide them ourselves through a mousemove listener in the viewer // anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering. this.tooltip = new Tooltip( this.view.firstChild, this.display.bind( this ), { activate: ( LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER ), deactivate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ), close_button: null, mode: Tooltip.MOUSE, mouse_offset: { x: -5, y: -5, dx: ( IA.is_rtl ? -1 : 1 ), dy: 1 }, open_delay: 0, hide_delay: 0, onclose: ( function ( tooltip, evt ) { if ( this.view ) { try { this.view.style.border = '1px solid ' + this.viewer.outer_border; } catch ( ex ) { this.view.style.border = '1px solid ' + IA.outer_border; } } if ( this.viewer.tip == tooltip ) { this.viewer.tip = null; } // Hide all boxes if we're outside the image. Relies on hide checking the // coordinates! (Otherwise, we'd always hide...) if ( evt ) { this.viewer.hide( evt ); } } ).bind( this ), onopen: ( function ( tooltip ) { if ( this.view ) { try { this.view.style.border = '1px solid ' + this.viewer.active_border; } catch ( ex ) { this.view.style.border = '1px solid ' + IA.active_border; } } this.viewer.tip = tooltip; } ).bind( this ) }, IA.tooltip_styles ); }, display: function ( evt ) { if ( !this.content ) { this.content = LAPI.make( 'div' ); var main = LAPI.make( 'div' ); this.content.appendChild( main ); this.content.main = main; if ( this.model.html ) { main.appendChild( this.model.html.cloneNode( true ) ); } // Make sure that the popup encompasses all floats this.content.appendChild( LAPI.make( 'div', null, { clear: 'both' } ) ); if ( this.viewer.may_edit ) { this.content.button_section = LAPI.make( 'div', null, { fontSize: 'smaller', textAlign: ( IA.is_rtl ? 'left' : 'right' ), borderTop: IA.tooltip_styles.border } ); this.content.appendChild( this.content.button_section ); this.content.button_section.appendChild( LAPI.DOM.makeLink( '#', ImageAnnotator.UI.get( 'wpImageAnnotatorEdit', true ), null, LAPI.Evt.makeListener( this, this.edit ) ) ); if ( ImageAnnotator_config.mayDelete() ) { this.content.button_section.appendChild( document.createTextNode( '\xa0' ) ); this.content.button_section.appendChild( LAPI.DOM.makeLink( '#', ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ), null, LAPI.Evt.makeListener( this, this.remove_event ) ) ); } } } return this.content; }, edit: function ( evt ) { if ( IA.canEdit() ) { IA.editor.editNote( this ); } if ( evt ) { return LAPI.Evt.kill( evt ); } return false; }, remove_event: function ( evt ) { if ( IA.canEdit() ) { this.remove(); } return LAPI.Evt.kill( evt ); }, remove: function () { if ( !this.content ) { // New note: just destroy it. this.destroy(); return true; } if ( !ImageAnnotator_config.mayDelete() ) { return false; } // Close and remove tooltip only if edit succeeded! Where and how to display error messages? var reason = ''; if ( !ImageAnnotator_config.mayBypassDeletionPrompt() || !window.ImageAnnotator_noDeletionPrompt ) { // Prompt for a removal reson reason = prompt( ImageAnnotator.UI.get( 'wpImageAnnotatorDeleteReason', true ), '' ); if ( reason === null ) { return false; } // Cancelled reason = reason.trim(); if ( !reason.length ) { if ( !ImageAnnotator_config.emptyDeletionReasonAllowed() ) { return false; } } // Re-show tooltip (without re-positioning it, we have no mouse coordinates here) in case // it was hidden because of the alert. If possible, we want the user to see the spinner. this.tooltip.show_now( this.tooltip ); } var self = this; var spinnerId = 'image_annotation_delete_' + this.model.id; LAPI.Ajax.injectSpinner( this.content.button_section.lastChild, spinnerId ); if ( this.tooltip ) { this.tooltip.size_change(); } LAPI.Ajax.editPage( mw.config.get( 'wgPageName' ), function ( doc, editForm, failureFunc, revision_id ) { try { if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) { throw new Error( '#Page version (revision ID) mismatch: edit conflict.' ); } var textbox = editForm.wpTextbox1; if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); } var pagetext = textbox.value.replace( /\r\n/g, '\n' ); // Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other // browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added. // We normally don't care, but here we need this to make sure we don't leave extra line // breaks when we remove the note. IA.setWikitext( pagetext ); var span = IA.findNote( pagetext, self.model.id ); if ( !span ) { // Hmmm? Doesn't seem to exist LAPI.Ajax.removeSpinner( spinnerId ); if ( self.tooltip ) { self.tooltip.size_change(); } self.destroy(); return; } var char_before = 0; var char_after = 0; if ( span.start > 0 ) { char_before = pagetext.charCodeAt( span.start - 1 ); } if ( span.end < pagetext.length ) { char_after = pagetext.charCodeAt( span.end ); } if ( String.fromCharCode( char_before ) == '\n' && String.fromCharCode( char_after ) == '\n' ) { span.start = span.start - 1; } pagetext = pagetext.substring( 0, span.start ) + pagetext.substring( span.end ); textbox.value = pagetext; var summary = editForm.wpSummary; if ( !summary ) { throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' ); } IA.setSummary( summary, ImageAnnotator.UI.get( 'wpImageAnnotatorRemoveSummary', true ) || '[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1', ( reason.length ? reason + ': ' : '' ) + self.model.wiki ); } catch ( ex ) { failure( null, ex ); return; } var edit_page = doc; LAPI.Ajax.submitEdit( editForm, function ( request ) { if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); } var revision_id = LAPI.WP.revisionFromHtml( request.responseText ); if ( !revision_id ) { failureFunc( request, new Error( 'Revision ID not found. Please reload the page.' ) ); return; } mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!! LAPI.Ajax.removeSpinner( spinnerId ); if ( self.tooltip ) { self.tooltip.size_change(); } self.destroy(); }, function ( request, ex ) { if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); } failureFunc( request, ex ); } ); }, function ( request, ex ) { // Failure. What now? TODO: Implement some kind of user feedback. LAPI.Ajax.removeSpinner( spinnerId ); if ( self.tooltip ) { self.tooltip.size_change(); } } ); return true; }, destroy: function () { if ( this.view ) { LAPI.DOM.removeNode( this.view ); } if ( this.dummy ) { LAPI.DOM.removeNode( this.dummy ); } if ( this.tooltip ) { this.tooltip.hide_now(); } if ( this.model && this.model.id > 0 && this.viewer ) { this.viewer.deregister( this ); } this.model = null; this.view = null; this.content = null; this.tooltip = null; this.viewer = null; }, area: function () { if ( !this.model || !this.model.dimension ) { return 0; } return ( this.model.dimension.w * this.model.dimension.h ); }, cannotEdit: function () { if ( this.content && this.content.button_section ) { LAPI.DOM.removeNode( this.content.button_section ); this.content.button_section = null; if ( this.tooltip ) { this.tooltip.size_change(); } } } }; // end ImageAnnotation var ImageAnnotationEditor = function () { this.initialize.apply( this, arguments ); }; ImageAnnotationEditor.prototype = { initialize: function () { var editor_width = 50; // Respect potential user-defined width setting if ( window.ImageAnnotationEditor_columns && !isNaN( window.ImageAnnotationEditor_columns ) && window.ImageAnnotationEditor_columns >= 30 && window.ImageAnnotationEditor_columns <= 100 ) { editor_width = window.ImageAnnotationEditor_columns; } this.editor = new LAPI.Edit( '', editor_width, 6, { box: ImageAnnotator.UI.get( 'wpImageAnnotatorEditorLabel', false ), preview: ImageAnnotator.UI.get( 'wpImageAnnotatorPreview', true ).capitalizeFirst(), save: ImageAnnotator.UI.get( 'wpImageAnnotatorSave', true ).capitalizeFirst(), revert: ImageAnnotator.UI.get( 'wpImageAnnotatorRevert', true ).capitalizeFirst(), cancel: ImageAnnotator.UI.get( 'wpImageAnnotatorCancel', true ).capitalizeFirst(), nullsave: ImageAnnotator_config.mayDelete() ? ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ).capitalizeFirst() : null, post: ImageAnnotator.UI.get( 'wpImageAnnotatorCopyright', false ) }, { onsave: this.save.bind( this ), onpreview: this.onpreview.bind( this ), oncancel: this.cancel.bind( this ), ongettext: function ( text ) { if ( text == null ) { return ''; } text = text.trim() .replace( /\{\{(\s*ImageNote(End)?\s*\|)/g, '&#x7B;&#x7B;$1' ); // Guard against people trying to break notes on purpose if ( text.length && typeof TextCleaner !== 'undefined' ) { text = TextCleaner.sanitizeWikiText( text, true ); } return text; } } ); this.box = LAPI.make( 'div' ); this.box.appendChild( this.editor.getView() ); // Limit the width of the bounding box to the size of the textarea, taking into account the // tooltip styles. Do *not* simply append this.box or the editor view, Opera behaves strangely // if textboxes were ever hidden through a visibility setting! Use a second throw-away textbox // instead. var temp = LAPI.make( 'div', null, IA.tooltip_styles ); temp.appendChild( LAPI.make( 'textarea', { cols: editor_width, rows: 6 } ) ); Object.merge( { position: 'absolute', top: '0px', left: '-10000px', visibility: 'hidden' }, temp.style ); document.body.appendChild( temp ); // Now we know how wide this textbox will be var box_width = temp.offsetWidth; LAPI.DOM.removeNode( temp ); // Note: we need to use a tooltip with a dynamic content creator function here because // static content is cloned inside the Tooltip. Cloning on IE loses all attached handlers, // and thus the editor's controls wouldn't work anymore. (This is not a problem on FF3, // where cloning preserves the handlers.) this.tooltip = new Tooltip( IA.get_cover(), this.get_editor.bind( this ), { activate: Tooltip.NONE, // We'll always show it explicitly deactivate: Tooltip.ESCAPE, close_button: null, // We have a cancel button anyway mode: Tooltip.FIXED, anchor: Tooltip.TOP_LEFT, mouse_offset: { x: 10, y: 10, dx: 1, dy: 1 }, // Misuse this: fixed offset from view max_pixels: ( box_width ? box_width + 20 : 0 ), // + 20 gives some slack z_index: 2010, // Above the cover. open_delay: 0, hide_delay: 0, onclose: this.close_tooltip.bind( this ) }, IA.tooltip_styles ); this.note = null; this.visible = false; LAPI.Evt.listenTo( this, this.tooltip.popup, IA.mouse_in, function ( evt ) { Array.forEach( IA.viewers, ( function ( viewer ) { if ( viewer != this.viewer && viewer.visible ) { viewer.hide(); } } ).bind( this ) ); } ); }, get_editor: function () { return this.box; }, editNote: function ( note ) { var same_note = ( note == this.note ); this.note = note; this.viewer = this.note.viewer; var cover = IA.get_cover(); cover.style.cursor = 'auto'; IA.show_cover(); if ( note.tooltip ) { note.tooltip.hide_now(); } IA.is_editing = true; if ( note.content && !IA.wiki_read ) { // Existing note, and we don't have the wikitext yet: go get it var self = this; LAPI.Ajax.apiGet( 'query', { prop: 'revisions', titles: mw.config.get( 'wgPageName' ), rvlimit: 1, rvstartid: mw.config.get( 'wgCurRevisionId' ), rvprop: 'ids|content' }, function ( request, json_result ) { if ( json_result && json_result.query && json_result.query.pages ) { // Should have only one page here for ( var page in json_result.query.pages ) { var p = json_result.query.pages[ page ]; if ( p && p.revisions && p.revisions.length ) { var rev = p.revisions[ 0 ]; if ( rev.revid == mw.config.get( 'wgCurRevisionId' ) && rev[ '*' ] && rev[ '*' ].length ) { IA.setWikitext( rev[ '*' ] ); } } break; } } // TODO: What upon a failure? self.open_editor( same_note, cover ); }, function ( request ) { // TODO: What upon a failure? self.open_editor( same_note, cover ); } ); } else { this.open_editor( same_note, cover ); } }, open_editor: function ( same_note, cover ) { this.editor.hidePreview(); if ( !same_note || this.editor.textarea.readOnly ) { // Different note, or save error last time this.editor.setText( this.note.model.wiki ); } this.editor.enable( LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL ); this.editor.textarea.readOnly = false; this.editor.textarea.style.backgroundColor = 'white'; // Set the position relative to the note's view. var view_pos = LAPI.Pos.position( this.note.view ); var origin = LAPI.Pos.position( cover ); this.tooltip.options.fixed_offset.x = view_pos.x - origin.x + this.tooltip.options.mouse_offset.x; this.tooltip.options.fixed_offset.y = view_pos.y - origin.y + this.tooltip.options.mouse_offset.y; this.tooltip.options.fixed_offset.dx = 1; this.tooltip.options.fixed_offset.dy = 1; // Make sure mouse event listeners are removed, especially on IE. this.dim = { x: this.note.view.offsetLeft, y: this.note.view.offsetTop, w: this.note.view.offsetWidth, h: this.note.view.offsetHeight }; this.viewer.setShowHideEvents( false ); this.viewer.hide(); // Make sure notes are hidden this.viewer.toggle( true ); // Show all note rectangles (but only the dummies) // Now show the editor this.tooltip.show_tip( null, false ); var tpos = LAPI.Pos.position( this.editor.textarea ); var ppos = LAPI.Pos.position( this.tooltip.popup ); tpos = tpos.x - ppos.x; if ( tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth ) { this.editor.textarea.style.width = ( this.tooltip.popup.offsetWidth - 2 * tpos ) + 'px'; } if ( LAPI.Browser.is_ie ) { // Fixate textarea width to prevent ugly flicker on each keypress in IE6... this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px'; } this.visible = true; }, hide_editor: function ( evt ) { if ( !this.visible ) { return; } this.visible = false; IA.is_editing = false; this.tooltip.hide_now( evt ); if ( evt && evt.type == 'keydown' && !this.saving ) { // ESC pressed on new note before a save attempt this.cancel(); } IA.hide_cover(); this.viewer.setDefaultMsg(); this.viewer.setShowHideEvents( true ); this.viewer.hide(); this.viewer.show(); // Make sure we get the real views again. // FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we // may show the note rectangles even though the mouse is now outside the image. (It was // somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was // pressed, it may actually be anywhere.) }, save: function ( editor ) { var data = editor.getText(); if ( !data || !data.length ) { // Empty text if ( this.note.remove() ) { this.hide_editor(); this.cancel(); this.note = null; } else { this.hide_editor(); this.cancel(); } return; } else if ( data == this.note.model.wiki ) { // Text unchanged this.hide_editor(); this.cancel(); return; } // Construct what to insert var dim = Object.clone( this.note.model.dimension ); if ( !dim ) { dim = { x: Math.round( this.dim.x * this.viewer.factors.dx ), y: Math.round( this.dim.y * this.viewer.factors.dy ), w: Math.round( this.dim.w * this.viewer.factors.dx ), h: Math.round( this.dim.h * this.viewer.factors.dy ) }; // Make sure everything is within bounds if ( dim.x + dim.w > this.viewer.full_img.width ) { if ( dim.w > this.dim.w * this.viewer.factors.dx ) { dim.w--; if ( dim.x + dim.w > this.viewer.full_img.width ) { if ( dim.x > 0 ) { dim.x--; } else { dim.w = this.viewer.full_img.width; } } } else { // Width already was rounded down if ( dim.x > 0 ) { dim.x--; } } } if ( dim.y + dim.h > this.viewer.full_img.height ) { if ( dim.h > this.dim.h * this.viewer.factors.dy ) { dim.h--; if ( dim.y + dim.h > this.viewer.full_img.height ) { if ( dim.y > 0 ) { dim.y--; } else { dim.h = this.viewer.full_img.height; } } } else { // Height already was rounded down if ( dim.y > 0 ) { dim.y--; } } } // If still too large, adjust width and height if ( dim.x + dim.w > this.viewer.full_img.width ) { if ( this.viewer.full_img.width > dim.x ) { dim.w = this.viewer.full_img.width - dim.x; } else { dim.x = this.viewer.full_img.width - 1; dim.w = 1; } } if ( dim.y + dim.h > this.viewer.full_img.height ) { if ( this.viewer.full_img.height > dim.y ) { dim.h = this.viewer.full_img.height - dim.y; } else { dim.y = this.viewer.full_img.height - 1; dim.h = 1; } } } this.to_insert = '{{ImageNote' + '|id=' + this.note.model.id + '|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h + '|dimx=' + this.viewer.full_img.width + '|dimy=' + this.viewer.full_img.height + '|style=2' + '}}\n' + data + ( data.endsWith( '\n' ) ? '' : '\n' ) + '{{ImageNoteEnd|id=' + this.note.model.id + '}}'; // Now edit the page var self = this; this.editor.busy( true ); this.editor.enable( 0 ); // Disable all buttons this.saving = true; LAPI.Ajax.editPage( mw.config.get( 'wgPageName' ), function ( doc, editForm, failureFunc, revision_id ) { try { if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) { // Page was edited since the user loaded it. throw new Error( '#Page version (revision ID) mismatch: edit conflict.' ); } // Modify the page var textbox = editForm.wpTextbox1; if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); } var pagetext = textbox.value; IA.setWikitext( pagetext ); var span = null; if ( self.note.content ) { // Otherwise it's a new note! span = IA.findNote( pagetext, self.note.model.id ); } if ( span ) { // Replace pagetext = pagetext.substring( 0, span.start ) + self.to_insert + pagetext.substring( span.end ); } else { // If not found, append // Try to append right after existing notes var lastNote = pagetext.lastIndexOf( '{{ImageNoteEnd|id=' ); if ( lastNote >= 0 ) { var endLastNote = pagetext.substring( lastNote ).indexOf( '}}' ); if ( endLastNote < 0 ) { endLastNote = pagetext.substring( lastNote ).indexOf( '\n' ); if ( endLastNote < 0 ) { lastNote = -1; } else { lastNote += endLastNote; } } else { lastNote += endLastNote + 2; } } if ( lastNote >= 0 ) { pagetext = pagetext.substring( 0, lastNote ) + '\n' + self.to_insert + pagetext.substring( lastNote ); } else { pagetext = pagetext.trimRight() + '\n' + self.to_insert; } } textbox.value = pagetext; var summary = editForm.wpSummary; if ( !summary ) { throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' ); } // If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary! if ( self.note.content != null ) { IA.setSummary( summary, ImageAnnotator.UI.get( 'wpImageAnnotatorChangeSummary', true ) || '[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1', data ); } else { IA.setSummary( summary, ImageAnnotator.UI.get( 'wpImageAnnotatorAddSummary', true ) || '[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1', data ); } } catch ( ex ) { failureFunc( null, ex ); return; } var edit_page = doc; LAPI.Ajax.submitEdit( editForm, function ( request ) { // After a successful submit. if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); } // TODO: Actually, the edit got through here, so calling failureFunc on // inconsistencies isn't quite right. Should we reload the page? var id = 'image_annotation_content_' + self.note.model.id; var doc = LAPI.Ajax.getHTML( request, failureFunc, id ); if ( !doc ) { return; } var html = LAPI.$( id, doc ); if ( !html ) { if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); } failureFunc( request, new Error( '#Note not found after saving. Please reload the page.' ) ); return; } var revision_id = LAPI.WP.revisionFromHtml( request.responseText ); if ( !revision_id ) { if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); } failureFunc( request, new Error( '#Version inconsistency after saving. Please reload the page.' ) ); return; } mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!! self.note.model.html = LAPI.DOM.importNode( document, html, true ); if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); } self.note.model.dimension = dim; // record dimension self.note.model.html.style.display = ''; self.note.model.wiki = data; self.editor.busy( false ); if ( self.note.content ) { LAPI.DOM.removeChildren( self.note.content.main ); self.note.content.main.appendChild( self.note.model.html ); } else { // New note. self.note.display(); // Actually a misnomer. Just creates 'content'. if ( self.viewer.annotations.length > 1 ) { self.viewer.annotations.sort( ImageAnnotation.compare ); var idxOfNote = Array.indexOf( self.viewer.annotations, self.note ); if ( idxOfNote + 1 < self.viewer.annotations.length ) { LAPI.DOM.insertNode( self.note.view, self.viewer.annotations[ idxOfNote + 1 ].view ); } } } self.to_insert = null; self.saving = false; if ( !self.note.tooltip ) { self.note.setTooltip(); } self.hide_editor(); IA.is_editing = false; self.editor.setText( data ); // In case the same note is re-opened: start new undo cycle } , function ( request, ex ) { if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); } failureFunc( request, ex ); } ); }, function ( request, ex ) { self.editor.busy( false ); self.saving = false; // TODO: How and where to display error if user closed editor through ESC (or through // opening another tooltip) in the meantime? if ( !self.visible ) { return; } // Change the tooltip to show the error. self.editor.setText( self.to_insert ); // Error message. Use preview field for this. var error_msg = ImageAnnotator.UI.get( 'wpImageAnnotatorSaveError', false ); var lk = getElementsByClassName( error_msg, 'span', 'wpImageAnnotatorOwnPageLink' ); if ( lk && lk.length && lk[ 0 ].firstChild.nodeName.toLowerCase() === 'a' ) { lk = lk[ 0 ].firstChild; lk.href = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( mw.config.get( 'wgPageName' ) ) ) + '?action=edit'; } if ( ex ) { var ex_msg = LAPI.formatException( ex, true ); if ( ex_msg ) { ex_msg.style.borderBottom = '1px solid red'; var tmp = LAPI.make( 'div' ); tmp.appendChild( ex_msg ); tmp.appendChild( error_msg ); error_msg = tmp; } } self.editor.setPreview( error_msg ); self.editor.showPreview(); self.editor.textarea.readOnly = true; // Force a light gray background, since IE has no visual readonly indication. self.editor.textarea.style.backgroundColor = '#EEEEEE'; self.editor.enable( LAPI.Edit.CANCEL ); // Disable all other buttons } ); }, onpreview: function ( editor ) { if ( this.tooltip ) { this.tooltip.size_change(); } }, cancel: function ( editor ) { if ( !this.note ) { return; } if ( !this.note.content ) { // No content: Cancel and remove this note! this.note.destroy(); this.note = null; } if ( editor ) { this.hide_editor(); } }, close_tooltip: function ( tooltip, evt ) { this.hide_editor( evt ); this.cancel(); } }; var ImageNotesViewer = function () { this.initialize.apply( this, arguments ); }; ImageNotesViewer.prototype = { initialize: function ( descriptor, may_edit ) { Object.merge( descriptor, this ); this.annotations = []; this.max_id = 0; this.main_div = null; this.msg = null; this.may_edit = may_edit; this.setup_done = false; this.tip = null; this.icon = null; this.factors = { dx: this.full_img.width / this.thumb.width, dy: this.full_img.height / this.thumb.height }; if ( !this.isThumbnail && !this.isOther ) { this.setup(); } else { // Normalize the namespace of the realName to 'File' to account for images possibly stored at // a foreign repository (the Commons). Otherwise a later information load might fail because // the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist // locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand // the localized namespace names of other wikis, but the canonical namespace name 'File' works // also locally. this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 ); } }, setup: function ( onlyIcon ) { this.setup_done = true; var name = this.realName; if ( this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax ) { this.imgName = this.realName; this.realName = ''; } else { name = getElementsByClassName( this.scope, '*', 'wpImageAnnotatorFullName' ); this.realName = ( ( name && name.length ) ? LAPI.DOM.getInnerText( name[ 0 ] ) : '' ); this.imgName = this.realName; } var annotations = getElementsByClassName( this.scope, 'div', IA.annotation_class ); if ( !this.may_edit && ( !annotations || annotations.length === 0 ) ) { return; } // Nothing to do // A div inserted around the image. It ensures that everything we add is positioned properly // over the image, even if the browser window size changes and re-layouts occur. var isEnabledImage = LAPI.DOM.hasClass( this.scope, 'wpImageAnnotatorEnable' ); if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) { this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } ); var floater = LAPI.make( 'div', null, { cssFloat: ( IA.is_rtl ? 'right' : 'left' ), styleFloat: ( IA.is_rtl ? 'right' : 'left' ), // For IE... width: String( this.thumb.width ) + 'px', position: 'relative' // Fixes IE layout bugs... } ); floater.appendChild( this.img_div ); this.img.parentNode.parentNode.insertBefore( floater, this.img.parentNode ); this.img_div.appendChild( this.img.parentNode ); // And now a clear:left to make the rest appear below the image, as usual. var breaker = LAPI.make( 'div', null, { clear: ( IA.is_rtl ? 'right' : 'left' ) } ); LAPI.DOM.insertAfter( breaker, floater ); // Remove spurious br tag. if ( breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase() == 'br' ) { LAPI.DOM.removeNode( breaker.nextSibling ); } } else if ( this.isOther || isEnabledImage ) { this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } ); this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode ); this.img_div.appendChild( this.img.parentNode ); // Insert one more to have a file_div, so that we can align the message text correctly this.file_div = LAPI.make( 'div', null, { width: String( this.thumb.width ) + 'px' } ); this.img_div.parentNode.insertBefore( this.file_div, this.img_div ); this.file_div.appendChild( this.img_div ); } else { // Thumbnail this.img_div = LAPI.make( 'div', { className: 'thumbimage' }, { position: 'relative', width: String( this.thumb.width ) + 'px' } ); this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode ); this.img.style.border = 'none'; this.img_div.appendChild( this.img.parentNode ); } if ( ( this.isThumbnail || this.isOther ) && !this.may_edit && ( onlyIcon || this.iconOnly || ImageAnnotator_config.inlineImageUsesIndicator( name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail ) ) ) { // Use an onclick handler instead of a link around the image. The link may have a default white // background, but we want to be sure to have transparency. The image should be an 8-bit indexed // PNG or a GIF and have a transparent background. this.icon = ImageAnnotator.UI.get( 'wpImageAnnotatorIndicatorIcon', false ); if ( this.icon ) { this.icon = this.icon.firstChild; } // Skip the message container span or div // Guard against misconfigurations if ( this.icon && this.icon.nodeName.toLowerCase() == 'a' && this.icon.firstChild.nodeName.toLowerCase() == 'img' ) { // Make sure we use the right protocol: var srcFixed = this.icon.firstChild.getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol ); this.icon.firstChild.src = srcFixed; this.icon.firstChild.title = this.icon.title; this.icon = this.icon.firstChild; } else if ( !this.icon || this.icon.nodeName.toLowerCase() !== 'img' ) { this.icon = LAPI.DOM.makeImage( IA.indication_icon, 14, 14, ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', true ) || '' ); } Object.merge( { position: 'absolute', zIndex: 1000, top: '0px', cursor: 'pointer' } , this.icon.style ); this.icon.onclick = ( function () { location.href = this.img.parentNode.href; } ).bind( this ); if ( IA.is_rtl ) { this.icon.style.right = '0px'; } else { this.icon.style.left = '0px'; } this.img_div.appendChild( this.icon ); // And done. We just show the icon, no fancy event handling needed. return; } // Set colors var colors = IA.getRawItem( 'colors', this.scope ); this.outer_border = colors && IA.getItem( 'outer', colors ) || IA.outer_border; this.inner_border = colors && IA.getItem( 'inner', colors ) || IA.inner_border; this.active_border = colors && IA.getItem( 'active', colors ) || IA.active_border; if ( annotations ) { for ( var i = 0; i < annotations.length; i++ ) { var id = annotations[ i ].id; if ( id && /^image_annotation_note_(\d+)$/.test( id ) ) { id = parseInt( id.substring( 'image_annotation_note_'.length ) ); } else { id = null; } if ( id ) { if ( id > this.max_id ) { this.max_id = id; } var w = IA.getIntItem( 'full_width_' + id, this.scope ); var h = IA.getIntItem( 'full_height_' + id, this.scope ); if ( w == this.full_img.width && h == this.full_img.height && !Array.exists( this.annotations, function ( note ) { return note.model.id == id; } ) ) { try { this.register( new ImageAnnotation( annotations[ i ], this, id ) ); } catch ( ex ) { // Swallow. } } } } } if ( this.annotations.length > 1 ) { this.annotations.sort( ImageAnnotation.compare ); } // Add the rectangles of existing notes to the DOM now that they are sorted. Array.forEach( this.annotations, ( function ( note ) { this.img_div.appendChild( note.view ); } ).bind( this ) ); if ( this.isThumbnail ) { this.main_div = getElementsByClassName( this.file_div, 'div', 'thumbcaption' ); if ( !this.main_div || this.main_div.length == 0 ) { this.main_div = null; } else { this.main_div = this.main_div[ 0 ]; } } if ( !this.main_div ) { this.main_div = LAPI.make( 'div' ); if ( IA.is_rtl ) { this.main_div.style.direction = 'rtl'; this.main_div.style.textAlign = 'right'; this.main_div.className = 'rtl'; } else { this.main_div.style.textAlign = 'left'; } if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) { LAPI.DOM.insertAfter( this.main_div, this.file_div ); } else { LAPI.DOM.insertAfter( this.main_div, this.img_div ); } } if ( !( this.isThumbnail || this.isOther ) || !this.noCaption && !IA.hideCaptions && ImageAnnotator_config.displayCaptionInArticles( name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail ) ) { this.msg = LAPI.make( 'div', null, { display: 'none' } ); if ( IA.is_rtl ) { this.msg.style.direction = 'rtl'; this.msg.className = 'rtl'; } if ( this.isThumbnail ) { this.msg.style.fontSize = '90%'; } this.main_div.appendChild( this.msg ); } // Set overflow parents, if any var simple = !!window.getComputedStyle; var checks = ( simple ? [ 'overflow', 'overflow-x', 'overflow-y' ] : [ 'overflow', 'overflowX', 'overflowY' ] ); var curStyle = null; for ( var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode ) { curStyle = ( simple ? window.getComputedStyle( up, null ) : ( up.currentStyle || up.style ) ); // "up.style" is actually incorrect, but a best-effort fallback. var overflow = Array.any( checks, function ( t ) { var o = curStyle[ t ]; return ( o && o != 'visible' ) ? o : null; } ); if ( overflow ) { if ( !this.overflowParents ) { this.overflowParents = [ up ]; } else { this.overflowParents[ this.overflowParents.length ] = up; } } } if ( this.overflowParents && this.may_edit ) { // Forbid editing if we have such a crazy layout. this.may_edit = false; IA.may_edit = false; } this.show_evt = LAPI.Evt.makeListener( this, this.show ); if ( this.overflowParents || LAPI.Browser.is_ie ) { // If we have overflowParents, also use a mousemove listener to show/hide the whole // view (FF doesn't send mouseout events if the visible border is still within the image, i.e., // if not the whole image is visible). On IE, also use this handler to show/hide the notes // if we're still within the visible area of the image. IE passes through mouse_over events to // the img even if the mouse is within a note's rectangle. Apparently is doesn't handle // transparent divs correctly. As a result, the notes will pop up and disappear only when the // mouse crosses the border, and if one moves the mouse a little fast across the border, we // don't get any event at all. That's no good. this.move_evt = LAPI.Evt.makeListener( this, this.check_hide ); } else { this.hide_evt = LAPI.Evt.makeListener( this, this.hide ); } this.move_listening = false; this.setShowHideEvents( true ); this.visible = false; this.setDefaultMsg(); }, cannotEdit: function () { if ( !this.may_edit ) { return; } this.may_edit = false; Array.forEach( this.annotations, function ( note ) { note.cannotEdit(); } ); }, setShowHideEvents: function ( set ) { if ( this.icon ) { return; } if ( set ) { LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt ); if ( this.hide_evt ) { LAPI.Evt.attach( this.img, IA.mouse_out, this.hide_evt ); } } else { LAPI.Evt.remove( this.img, IA.mouse_in, this.show_evt ); if ( this.hide_evt ) { LAPI.Evt.remove( this.img, IA.mouse_out, this.hide_evt ); } else if ( this.move_listening ) { this.removeMoveListener(); } } }, removeMoveListener: function () { if ( this.icon ) { return; } this.move_listening = false; if ( this.move_evt ) { if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); } LAPI.Evt.remove( document, 'mousemove', this.move_evt, true ); } }, adjustRectangleSize: function ( node ) { if ( this.icon ) { return; } // Make sure the note boxes don't overlap the image boundary; we might get an event // loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering. var view_x = node.offsetLeft; var view_y = node.offsetTop; var view_w = node.offsetWidth; var view_h = node.offsetHeight; if ( view_x === 0 ) { view_x = 1; } if ( view_y === 0 ) { view_y = 1; } if ( view_x + view_w >= this.thumb.width ) { view_w = this.thumb.width - view_x - 1; if ( view_w <= 4 ) { view_w = 4; view_x = this.thumb.width - view_w - 1; } } if ( view_y + view_h >= this.thumb.height ) { view_h = this.thumb.height - view_y - 1; if ( view_h <= 4 ) { view_h = 4; view_y = this.thumb.height - view_h - 1; } } // Now set position and width and height, subtracting cumulated border widths if ( view_x != node.offsetLeft || view_y != node.offsetTop || view_w != node.offsetWidth || view_h != node.offsetHeight ) { node.style.top = String( view_y ) + 'px'; node.style.left = String( view_x ) + 'px'; node.style.width = String( view_w - 2 ) + 'px'; node.style.height = String( view_h - 2 ) + 'px'; node.firstChild.style.width = String( view_w - 4 ) + 'px'; node.firstChild.style.height = String( view_h - 4 ) + 'px'; } }, toggle: function ( dummies ) { var i; if ( !this.annotations || this.annotations.length === 0 || this.icon ) { return; } if ( dummies ) { for ( i = 0; i < this.annotations.length; i++ ) { this.annotations[ i ].view.style.display = 'none'; if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); } this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' ); if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].dummy ); } } } else { for ( i = 0; i < this.annotations.length; i++ ) { this.annotations[ i ].dummy.style.display = 'none'; this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' ); if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].view ); } if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); } } } this.visible = !this.visible; }, show: function ( evt ) { if ( this.visible || this.icon ) { return; } this.toggle( IA.is_adding || IA.is_editing ); if ( this.move_evt && !this.move_listening ) { LAPI.Evt.attach( document, 'mousemove', this.move_evt, true ); this.move_listening = true; if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); } } }, hide: function ( evt ) { if ( this.icon ) { return true; } if ( !this.visible ) { // Huh? if ( this.move_listening ) { this.removeMoveListener(); } return true; } if ( evt ) { var mouse_pos = LAPI.Pos.mousePosition( evt ); if ( mouse_pos ) { if ( this.tip ) { // Check whether we're within the visible note. if ( LAPI.Pos.isWithin( this.tip.popup, mouse_pos.x, mouse_pos.y ) ) { return true; } } var is_within = true; var img_pos = LAPI.Pos.position( this.img ); var rect = { x: img_pos.x, y: img_pos.y, r: ( img_pos.x + this.img.offsetWidth ), b: ( img_pos.y + this.img.offsetHeight ) }; var i; if ( this.overflowParents ) { // We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set. // Compute the actually visible region by intersecting the rectangle given by img_pos and // this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents. function intersect_rectangles( a, b ) { if ( b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y ) { return { x: 0, y: 0, r: 0, b: 0 }; } return { x: Math.max( a.x, b.x ), y: Math.max( a.y, b.y ), r: Math.min( a.r, b.r ), b: Math.min( a.b, b.b ) }; } for ( i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++ ) { img_pos = LAPI.Pos.position( this.overflowParents[ i ] ); img_pos.r = img_pos.x + this.overflowParents[ i ].clientWidth; img_pos.b = img_pos.y + this.overflowParents[ i ].clientHeight; rect = intersect_rectangles( rect, img_pos ); } } is_within = !( rect.x >= rect.r || rect.y >= rect.b || // Empty rectangle rect.x >= mouse_pos.x || rect.r <= mouse_pos.x || rect.y >= mouse_pos.y || rect.b <= mouse_pos.y ); if ( is_within ) { if ( LAPI.Browser.is_ie && evt.type === 'mousemove' ) { var display; // Loop in reverse order to properly display top rectangle's note! for ( i = this.annotations.length - 1; i >= 0; i-- ) { display = this.annotations[ i ].view.style.display; if ( display !== 'none' && display != null && LAPI.Pos.isWithin( this.annotations[ i ].view.firstChild, mouse_pos.x, mouse_pos.y ) ) { if ( !this.annotations[ i ].tooltip.visible ) { this.annotations[ i ].tooltip.show( evt ); } return true; } } if ( this.tip ) { this.tip.hide_now(); } // Inside the image, but not within any note rectangle } return true; } } } // Not within the image, or forced hiding (no event) if ( this.move_listening ) { this.removeMoveListener(); } this.toggle( IA.is_adding || IA.is_editing ); return true; }, check_hide: function ( evt ) { if ( this.icon ) { return true; } if ( this.visible ) { this.hide( evt ); } return true; }, register: function ( new_note ) { this.annotations[ this.annotations.length ] = new_note; if ( new_note.model.id > 0 ) { if ( new_note.model.id > this.max_id ) { this.max_id = new_note.model.id; } } else { new_note.model.id = ++this.max_id; } }, deregister: function ( note ) { Array.remove( this.annotations, note ); if ( note.model.id == this.max_id ) { this.max_id--; } if ( this.annotations.length === 0 ) { this.setDefaultMsg(); } // If we removed the last one, clear the msg }, setDefaultMsg: function () { if ( this.annotations && this.annotations.length && this.msg ) { LAPI.DOM.removeChildren( this.msg ); this.msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', false ) ); if ( this.realName && typeof this.realName === 'string' && this.realName.length ) { var otherPageMsg = ImageAnnotator.UI.get( 'wpImageAnnotatorEditNotesMsg', false ); if ( otherPageMsg ) { var lk = otherPageMsg.getElementsByTagName( 'a' ); if ( lk && lk.length ) { lk = lk[ 0 ]; lk.parentNode.replaceChild( LAPI.DOM.makeLink( mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( this.realName ) ), this.realName, this.realName ), lk ); this.msg.appendChild( otherPageMsg ); } } } this.msg.style.display = ''; } else { if ( this.msg ) { this.msg.style.display = 'none'; } } if ( IA.button_div && this.may_edit ) { IA.button_div.style.display = ''; } } }; var IA = { // This object is responsible for setting up annotations when a page is loaded. It loads all // annotations in the page source, and adds an "Annotate this image" button plus the support // for drawing rectangles onto the image if there is only one image and editing is allowed. haveAjax: false, button_div: null, add_button: null, cover: null, border: null, definer: null, mouse_in: ( window.ActiveXObject ? 'mouseenter' : 'mouseover' ), mouse_out: ( window.ActiveXObject ? 'mouseleave' : 'mouseout' ), annotation_class: 'image_annotation', // Format of notes in Wikitext. Note: there are two formats, an old one and a new one. // We only write the newest (last) one, but we can read also the older formats. Order is // important, because the old format also used the ImageNote template, but for a different // purpose. note_delim: [ { start: '<div id="image_annotation_note_$1"', end: '</div><!-- End of annotation $1-->', content_start: '<div id="image_annotation_content_$1">\n', content_end: '</div>\n<span id="image_annotation_wikitext_$1"' }, { start: '{{ImageNote|id=$1', end: '{{ImageNoteEnd|id=$1}}', content_start: '}}\n', content_end: '{{ImageNoteEnd|id=$1}}' } ], tooltip_styles: { // The style for all our tooltips border: '1px solid #8888aa', backgroundColor: '#ffffe0', padding: '0.3em', fontSize: ( ( mw.config.get( 'skin' ) == 'monobook' || mw.config.get( 'skin' ) == 'modern' ) ? '127%' : '100%' ) // Scale up to default text size }, editor: null, wiki_read: false, is_rtl: false, move_listening: false, is_tracking: false, is_adding: false, is_editing: false, zoom_threshold: 8.0, zoom_factor: 4.0, install_attempts: 0, max_install_attempts: 10, // Maximum 5 seconds imgs_with_notes: [], thumbs: [], other_images: [], // Fallback indication_icon: '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png', install: function ( config ) { if ( typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable ) { return; } if ( !config || ImageAnnotator_config != null ) { return; } // Double check. if ( !config.viewingEnabled() ) { return; } var self = IA; ImageAnnotator_config = config; // Determine whether we have XmlHttp. We try to determine this here to be able to avoid // doing too much work. if ( window.XMLHttpRequest && typeof LAPI !== 'undefined' && typeof LAPI.Ajax !== 'undefined' && typeof LAPI.Ajax.getRequest !== 'undefined' ) { self.haveAjax = ( LAPI.Ajax.getRequest() != null ); self.ajaxQueried = true; } else { self.haveAjax = true; // A pity. May occur on IE. We'll check again later on. self.ajaxQueried = false; } // We'll include self.haveAjax later on once more, to catch the !ajaxQueried case. self.may_edit = mw.config.get( 'wgNamespaceNumber' ) >= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled(); function namespaceCheck( list ) { if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) { return false; } var namespaceIds = mw.config.get( 'wgNamespaceIds' ); if ( !namespaceIds ) { return false; } var namespaceNumber = mw.config.get( 'wgNamespaceNumber' ); for ( var i = 0; i < list.length; i++ ) { if ( typeof list[ i ] === 'string' && ( list[ i ] === '*' || namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber ) ) { return true; } } return false; } self.rules = { inline: {}, thumbs: {}, shared: {} }; // Now set the default rules. Undefined means default setting (true for show, false for icon), // but overrideable by per-image rules. If set, it's not overrideable by per-image rules. // if ( !self.haveAjax || !config.generalImagesEnabled() || namespaceCheck( window.ImageAnnotator_no_images || null ) ) { self.rules.inline.show = false; self.rules.thumbs.show = false; self.rules.shared.show = false; } else { if ( !self.haveAjax || !config.thumbsEnabled() || namespaceCheck( window.ImageAnnotator_no_thumbs || null ) ) { self.rules.thumbs.show = false; } if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) { self.rules.shared.show = true; } else if ( !config.sharedImagesEnabled() || namespaceCheck( window.ImageAnnotator_no_shared || null ) ) { self.rules.shared.show = false; } if ( namespaceCheck( window.ImageAnnotator_icon_images || null ) ) { self.rules.inline.icon = true; } if ( namespaceCheck( window.ImageAnnotator_icon_thumbs || null ) ) { self.rules.thumbs.icon = true; } } // User rule for displaying captions on images in articles self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null ); var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show; if ( do_images ) { // Per-article switching off of note display on inline images and thumbnails var rules = document.getElementById( 'wpImageAnnotatorImageRules' ); if ( rules ) { if ( rules.className.indexOf( 'wpImageAnnotatorNone' ) >= 0 ) { self.rules.inline.show = false; self.rules.thumbs.show = false; self.rules.shared.show = false; } if ( typeof self.rules.inline.show === 'undefined' && rules.className.indexOf( 'wpImageAnnotatorDisplay' ) >= 0 ) { self.rules.inline.show = true; } if ( rules.className.indexOf( 'wpImageAnnotatorNoThumbDisplay' ) >= 0 ) { self.rules.thumbs.show = false; } if ( typeof self.rules.thumbs.show === 'undefined' && rules.className.indexOf( 'wpImageAnnotatorThumbDisplay' ) >= 0 ) { self.rules.thumbs.show = true; } if ( rules.className.indexOf( 'wpImageAnnotatorInlineDisplayIcons' ) >= 0 ) { self.rules.inline.icon = true; } if ( rules.className.indexOf( 'wpImageAnnotatorThumbDisplayIcons' ) >= 0 ) { self.rules.thumbs.icon = true; } if ( rules.className.indexOf( 'wpImageAnnotatorOnlyLocal' ) >= 0 ) { self.rules.shared.show = false; } } } // Make sure the shared value is set self.rules.shared.show = typeof self.rules.shared.show === 'undefined' || self.rules.shared.show; var do_thumbs = typeof self.rules.thumbs.show === 'undefined' || self.rules.thumbs.show; if ( do_images ) { var bodyContent = document.getElementById( 'bodyContent' ) || // monobook, vector document.getElementById( 'mw_contentholder' ) || // modern document.getElementById( 'article' ); // old skins if ( bodyContent ) { var all_imgs = bodyContent.getElementsByTagName( 'img' ); // This prevents traversing a page with more than 400 images // There are extreme cases like [[Emoji]] that high number of images can cause // huge lag specially on Chrome if ( all_imgs.length > 400 ) { // purging the array, simply a hack to avoid more indention all_imgs = []; } for ( var i = 0; i < all_imgs.length; i++ ) { // Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries. var up = all_imgs[ i ].parentNode; if ( up.nodeName.toLowerCase() !== 'a' ) { continue; } up = up.parentNode; if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' thumbinner ' ) >= 0 ) { if ( do_thumbs ) { self.thumbs[ self.thumbs.length ] = up; } continue; } up = up.parentNode; if ( !up ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) { self.imgs_with_notes[ self.imgs_with_notes.length ] = up; continue; } up = up.parentNode; if ( !up ) { continue; } // Other images not in galleries if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) { self.imgs_with_notes[ self.imgs_with_notes.length ] = up; continue; } up = up.parentNode; if ( !up ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; } if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) { self.imgs_with_notes[ self.imgs_with_notes.length ] = up; } else { // Guard against other scripts adding aribtrary numbers of divs (dshuf for instance!) var is_other = true; while ( up && up.nodeName.toLowerCase() == 'div' && is_other ) { up = up.parentNode; if ( up ) { is_other = ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) < 0; } } if ( is_other ) { self.other_images[ self.other_images.length ] = all_imgs[ i ]; } } } // end loop } } else { self.imgs_with_notes = getElementsByClassName( document, '*', 'wpImageAnnotatorEnable' ); if ( do_thumbs ) { self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' ); } // No galleries! } if ( mw.config.get( 'wgNamespaceNumber' ) == 6 || ( self.imgs_with_notes.length ) || ( self.thumbs.length ) || ( self.other_images.length ) ) { // Publish parts of config. ImageAnnotator.UI = config.UI; self.outer_border = config.outer_border; self.inner_border = config.inner_border; self.active_border = config.active_border; self.new_border = config.new_border; self.wait_for_required_libraries(); } }, wait_for_required_libraries: function () { if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' ) { if ( IA.install_attempts++ < IA.max_install_attempts ) { setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec. } return; } if ( LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) { return; } // Opera 8 has severe problems // Get the UI. We're likely to need it if we made it to here. IA.setup_ui(); IA.setup(); }, setup: function () { var self = IA; self.imgs = []; // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]] self.is_rtl = LAPI.DOM.hasClass( document.body, 'rtl' ) || ( LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl' ); var stylepath = mw.config.get( 'stylepath' ) || '/skin'; // Use this to temporarily display an image off-screen to get its dimensions var testImgDiv = LAPI.make( 'div', null, { display: 'none', position: 'absolute', width: '300px', overflow: 'hidden', overflowX: 'hidden', left: '-10000px' } ); document.body.insertBefore( testImgDiv, document.body.firstChild ); function img_check( img, is_other ) { var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 ); var srcH = parseInt( img.getAttribute( 'height', 2 ), 10 ); // Don't do anything on extremely small previews. We need some minimum extent to be able to place // rectangles after all... if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) { return null; } // For non-thumbnail images, the size limit is larger. if ( is_other && ( srcW < 60 || srcH < 60 ) ) { return null; } var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary... var h = img.clientHeight; // If the image is currently hidden, its clientWidth and clientHeight are not set. Try // harder to get the true width and height: if ( ( !w || !h ) && img.style.display != 'none' ) { var copied = img.cloneNode( true ); copied.style.display = ''; testImgDiv.appendChild( copied ); testImgDiv.style.display = ''; w = copied.clientWidth; h = copied.clientHeight; testImgDiv.style.display = 'none'; LAPI.DOM.removeNode( copied ); } // Quit if the image wasn't loaded properly for some reason: if ( w != srcW || h != srcH ) { return null; } // Exclude system images if ( img.src.contains( stylepath ) ) { return null; } // Only if within a link if ( img.parentNode.nodeName.toLowerCase() != 'a' ) { return null; } if ( is_other ) { // Only if the img-within-link construction is within some element that may contain a div! if ( /^(p|span)$/i.test( img.parentNode.parentNode.nodeName ) ) { // Special case: a paragraph may contain only inline elements, but we want to be able to handle // files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image // in a div, but for now we assume that all browsers can handle a div within a paragraph or // a span in a meaningful way, even if that is not really allowed. } else if ( !/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test( img.parentNode.parentNode.nodeName ) ) { return null; } } // Exclude any that are within an image note! var up = img.parentNode.parentNode; while ( up != document.body ) { if ( LAPI.DOM.hasClass( up, IA.annotation_class ) ) { return null; } up = up.parentNode; } return { width: w, height: h }; } function setup_one( scope ) { var file_div = scope; var is_thumb = scope != document && scope.nodeName.toLowerCase() == 'div' && LAPI.DOM.hasClass( scope, 'thumbinner' ); var is_other = scope.nodeName.toLowerCase() == 'img'; if ( is_other && self.imgs.length && scope == self.imgs[ 0 ] ) { return null; } if ( scope == document ) { file_div = LAPI.$( 'file' ); } else if ( !is_thumb && !is_other ) { file_div = getElementsByClassName( scope, 'div', 'wpImageAnnotatorFile' ); if ( !file_div || file_div.length != 1 ) { return null; } file_div = file_div[ 0 ]; } if ( !file_div ) { return null; } var img = null; if ( scope == document ) { img = LAPI.WP.getPreviewImage( mw.config.get( 'wgTitle' ) ); // TIFFs may be multi-paged: allow only for single-page TIFFs if ( document.pageselector ) { img = null; } } else if ( is_other ) { img = scope; } else { img = file_div.getElementsByTagName( 'img' ); if ( !img || img.length === 0 ) { return null; } img = img[ 0 ]; } if ( !img ) { return null; } var dim = img_check( img, is_other ); if ( !dim ) { return null; } // Conditionally exclude shared images. if ( scope != document && !self.rules.shared.show && ImageAnnotator_config.imageIsFromSharedRepository( img.src ) ) { return null; } var name = null; if ( scope == document ) { name = mw.config.get( 'wgPageName' ); } else { name = LAPI.WP.pageFromLink( img.parentNode ); if ( !name ) { return null; } name = name.replace( / /g, '_' ); if ( is_thumb || is_other ) { var img_src = decodeURIComponent( img.getAttribute( 'src', 2 ) ).replace( / /g, '_' ); // img_src should have a component "/name" in there somewhere var colon = name.indexOf( ':' ); if ( colon <= 0 ) { return null; } var img_name = name.substring( colon + 1 ); if ( img_src.search( new RegExp( '/' + img_name.escapeRE() + '(/.*)?$' ) ) < 0 ) { return null; } // If the link is not going to file namespace, we won't find the full size later on and // thus we won't do anything with it. } } if ( name.search( /\.(jpe?g|png|gif|svg|tiff?|webp)$/i ) < 0 ) { return null; } // Only PNG, JPE?G, GIF, SVG, TIFF?, and WebP // Finally check for wpImageAnnotatorControl var icon_only = false; var no_caption = false; if ( is_thumb || is_other ) { var up = img.parentNode.parentNode; // Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct for ( var i = 0; ++i <= 3 && up; up = up.parentNode ) { if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControl' ) ) { if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOff' ) ) { return null; } if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorIconOnly' ) ) { icon_only = true; } if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorCaptionOff' ) ) { no_caption = true; } break; } } } return { scope: scope, file_div: file_div, img: img, realName: name, isThumbnail: is_thumb, isOther: is_other, thumb: { width: dim.width, height: dim.height }, iconOnly: icon_only, noCaption: no_caption }; } function setup_images( list ) { Array.forEach( list, function ( elem ) { var desc = setup_one( elem ); if ( desc ) { self.imgs[ self.imgs.length ] = desc; } } ); } if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) { setup_images( [ document ] ); self.may_edit = self.may_edit && ( self.imgs.length == 1 ); setup_images( self.imgs_with_notes ); } else { setup_images( self.imgs_with_notes ); self.may_edit = self.may_edit && ( self.imgs.length == 1 ); } self.may_edit = self.may_edit && location.href.search( /[?&]oldid=/ ) < 0; if ( self.haveAjax ) { setup_images( self.thumbs ); setup_images( self.other_images ); } // Remove the test div LAPI.DOM.removeNode( testImgDiv ); if ( self.imgs.length === 0 ) { return; } // We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we // need them. This has in particular a benefit if we do have to query for the file sizes below. if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) { // Try to get the full size without Ajax. self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage(); if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) { self.setup_step_two(); return; } } // Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.) // Also avoid using Ajax on IE6... var cache = {}; var names = []; Array.forEach( self.imgs, function ( img, idx ) { if ( cache[ img.realName ] ) { cache[ img.realName ][ cache[ img.realName ].length ] = idx; } else { cache[ img.realName ] = [ idx ]; names[ names.length ] = img.realName; } } ); var to_do = names.length; var done = 0; function check_done( length ) { done += length; if ( done >= names.length ) { if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) { ImageAnnotator.info_callbacks = null; } self.setup_step_two(); } } function make_calls( execute_call, url_limit ) { function build_titles( from, length, url_limit ) { var done = 0; var text = ''; for ( var i = from; i < from + length; i++ ) { var new_text = names[ i ]; if ( url_limit ) { new_text = encodeURIComponent( new_text ); if ( text.length && ( text.length + new_text.length + 1 > url_limit ) ) { break; } } text += ( text.length ? '|' : '' ) + new_text; done++; } return { text: text, n: done }; } var start = 0, chunk = 0, params; while ( to_do > 0 ) { params = build_titles( start, Math.min( 50, to_do ), url_limit ); execute_call( params.n, params.text ); to_do -= params.n; start += params.n; } } function set_info( json ) { try { if ( json && json.query && json.query.pages ) { function get_size( info ) { if ( !info.imageinfo || info.imageinfo.length === 0 ) { return; } var title = info.title.replace( / /g, '_' ); var indices = cache[ title ]; if ( !indices ) { return; } Array.forEach( indices , function ( i ) { self.imgs[ i ].full_img = { width: info.imageinfo[ 0 ].width, height: info.imageinfo[ 0 ].height }; self.imgs[ i ].has_page = ( typeof info.missing === 'undefined' ); self.imgs[ i ].isLocal = !info.imagerepository || info.imagerepository == 'local'; if ( i != 0 || !self.may_edit || !info.protection || mw.config.get( 'wgNamespaceNumber' ) != 6 ) { return; } // Care about the protection settings var protection = Array.any( info.protection, function ( e ) { return ( e.type == 'edit' ? e : null ); } ); self.may_edit = !protection || ( mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( protection.level ) ); } ); } for ( var page in json.query.pages ) { get_size( json.query.pages[ page ] ); } } // end if } catch ( ex ) { } } if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) { // IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that // prompt by using getScript instead of parseWikitext in this case. ImageAnnotator.info_callbacks = []; var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' + '&prop=info|imageinfo&inprop=protection&iiprop=size' + '&titles=&callback=ImageAnnotator.info_callbacks[].callback'; if ( template.startsWith( '//' ) ) { template = document.location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug) make_calls( function ( length, titles ) { var idx = ImageAnnotator.info_callbacks.length; ImageAnnotator.info_callbacks[ idx ] = { callback: function ( json ) { set_info( json ); ImageAnnotator.info_callbacks[ idx ].done = true; if ( ImageAnnotator.info_callbacks[ idx ].script ) { LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script ); ImageAnnotator.info_callbacks[ idx ].script = null; } check_done( length ); }, done: false }; ImageAnnotator.info_callbacks[ idx ].script = IA.getScript( template.replace( 'info_callbacks[].callback', 'info_callbacks[' + idx + '].callback' ) .replace( '&titles=&', '&titles=' + titles + '&' ), true // No local caching! ); // We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may // get the script from the browser's cache, and if that happens, IE may execute the // script (and call the callback) synchronously before the assignment is done. Clean // up in that case. if ( ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[ idx ] && ImageAnnotator.info_callbacks[ idx ].done && ImageAnnotator.info_callbacks[ idx ].script ) { LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script ); ImageAnnotator.info_callbacks[ idx ].script = null; } }, ( LAPI.Browser.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters ); } else { make_calls( function ( length, titles ) { LAPI.Ajax.apiGet( 'query' , { titles: titles, prop: 'info|imageinfo', inprop: 'protection', iiprop: 'size' } , function ( request, json_result ) { set_info( json_result ); check_done( length ); } , function () { check_done( length ); } ); } ); } // end if can use Ajax }, setup_ui: function () { // Complete the UI object we've gotten from config. ImageAnnotator.UI.ready = false; ImageAnnotator.UI.repo = null; ImageAnnotator.UI.needs_plea = false; var readyEvent = []; ImageAnnotator.UI.fireReadyEvent = function () { if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do. ImageAnnotator.UI.ready = true; // Call all registered handlers, and clear the array. Array.forEach( readyEvent, function ( f, idx ) { try { f(); } catch ( ex ) {} readyEvent[ idx ] = null; } ); readyEvent = null; }; ImageAnnotator.UI.addReadyEventHandler = function ( f ) { if ( ImageAnnotator.UI.ready ) { f(); // Already fired: call directly } else { readyEvent[ readyEvent.length ] = f; } }; ImageAnnotator.UI.setup = function () { if ( ImageAnnotator.UI.repo ) { return; } var self = ImageAnnotator.UI; var node = LAPI.make( 'div', null, { display: 'none' } ); document.body.appendChild( node ); if ( typeof UIElements === 'undefined' ) { self.basic = true; self.repo = {}; for ( var item in self.defaults ) { node.innerHTML = self.defaults[ item ]; self.repo[ item ] = node.firstChild; LAPI.DOM.removeChildren( node ); } } else { self.basic = false; self.repo = UIElements.emptyRepository( self.defaultLanguage ); for ( var item in self.defaults ) { node.innerHTML = self.defaults[ item ]; UIElements.setEntry( item, self.repo, node.firstChild ); LAPI.DOM.removeChildren( node ); } UIElements.load( 'wpImageAnnotatorTexts', null, null, self.repo ); } LAPI.DOM.removeNode( node ); }; ImageAnnotator.UI.get = function ( id, basic, no_plea ) { var self = ImageAnnotator.UI; if ( !self.repo ) { self.setup(); } var result = null; var add_plea = false; if ( self.basic ) { result = self.repo[ id ]; } else { result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null ); add_plea = !result; if ( !result ) { result = UIElements.getEntry( id, self.repo ); } } self.needs_plea = add_plea; if ( !result ) { return null; } // Hmmm... what happened here? We normally have defaults... if ( basic ) { return LAPI.DOM.getInnerText( result ).trim(); } result = result.cloneNode( true ); if ( mw.config.get( 'wgServer' ).contains( '/commons' ) && add_plea && !no_plea ) { // Add a translation plea. if ( result.nodeName.toLowerCase() == 'div' ) { result.appendChild( self.get_plea() ); } else { var span = LAPI.make( 'span' ); span.appendChild( result ); span.appendChild( self.get_plea() ); result = span; } } return result; }; ImageAnnotator.UI.get_plea = function () { var self = ImageAnnotator.UI; var translate = self.get( 'wpTranslate', false, true ) || 'translate'; var span = LAPI.make( 'small' ); span.appendChild( document.createTextNode( '\xa0(' ) ); span.appendChild( LAPI.DOM.makeLink( mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' + '&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' + '&language=' + mw.config.get( 'wgUserLanguage' ), translate, ( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() ) ) ); span.appendChild( document.createTextNode( ')' ) ); return span; }; ImageAnnotator.UI.init = function ( html_text_or_json ) { var text; if ( typeof html_text_or_json === 'string' ) { text = html_text_or_json; } else if ( typeof html_text_or_json !== 'undefined' && typeof html_text_or_json.parse !== 'undefined' && typeof html_text_or_json.parse.text !== 'undefined' && typeof html_text_or_json.parse.text[ '*' ] !== 'undefined' ) { text = html_text_or_json.parse.text[ '*' ]; } else { text = null; } if ( !text ) { ImageAnnotator.UI.fireReadyEvent(); return; } var node = LAPI.make( 'div', null, { display: 'none' } ); document.body.appendChild( node ); try { node.innerHTML = text; } catch ( ex ) { LAPI.DOM.removeNode( node ); node = null; // Swallow. We'll just work with the default UI } if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); } ImageAnnotator.UI.fireReadyEvent(); }; var ui_page = '{{MediaWiki:ImageAnnotatorTexts' + ( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) + '|live=1}}'; function get_ui_no_ajax() { var url = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' + encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' + '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'; // Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch // that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax // anyway.) IA.getScript( url, true ); // No local caching! } function get_ui() { IA.haveAjax = ( LAPI.Ajax.getRequest() != null ); IA.ajaxQueried = true; // Works only with Ajax (but then, most of this script doesn't work without). // Check what this does to load times... If lots of people used this, it might be better to // have the UI texts included in some footer as we did on Special:Upload. True, everybody // would get the texts, even people not using this, but the texts are small anyway... if ( !IA.haveAjax ) { get_ui_no_ajax(); // Fallback. return; } LAPI.Ajax.parseWikitext( ui_page, ImageAnnotator.UI.init, ImageAnnotator.UI.fireReadyEvent, false, null, 'API', // A fixed string to enable caching at all. 14400 // 4 hour caching. ); } // end get_ui if ( !window.XMLHttpRequest && !!window.ActiveXObject ) { // IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that // prompt by using getScript instead of parseWikitext in this case. The disadvantage // is that we don't do anything if this fails for some reason. get_ui_no_ajax(); } else { get_ui(); } }, setup_step_two: function () { var self = IA; // Throw out any images for which we miss either the thumbnail or the full image size. // Also throws out thumbnails that are larger than the full image. self.imgs = Array.select( self.imgs, function ( elem, idx ) { var result = elem.thumb.width > 0 && elem.thumb.height > 0 && typeof elem.full_img !== 'undefined' && elem.full_img.width > 0 && elem.full_img.height > 0 && elem.full_img.width >= elem.thumb.width && elem.full_img.height >= elem.thumb.height; if ( self.may_edit && idx === 0 && !result ) { self.may_edit = false; } return result; } ); if ( self.imgs.length === 0 ) { return; } ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup ); }, complete_setup: function () { // We can be sure to have the UI here because this is called only when the ready event of the // UI object is fired. var self = IA; // Check edit permissions if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) { self.may_edit = ( ( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) || ( mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' && mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed ) ); } if ( self.may_edit ) { // Check whether the image is local. Don't allow editing if the file is remote. var sharedUpload = getElementsByClassName( document, 'div', 'sharedUploadNotice' ); self.may_edit = ( !sharedUpload || sharedUpload.length === 0 ); } if ( self.may_edit && mw.config.get( 'wgNamespaceNumber' ) != 6 ) { // Only allow edits if the stored page name matches the current one. var img_page_name = getElementsByClassName( self.imgs[ 0 ].scope, '*', 'wpImageAnnotatorPageName' ); if ( img_page_name && img_page_name.length ) { img_page_name = LAPI.DOM.getInnerText( img_page_name[ 0 ] ); } else { img_page_name = ''; } self.may_edit = ( img_page_name.replace( / /g, '_' ) == mw.config.get( 'wgTitle' ).replace( / /g, '_' ) ); } if ( self.may_edit && self.ajaxQueried ) { self.may_edit = self.haveAjax; } // Now create viewers for all images self.viewers = new Array( self.imgs.length ); for ( var i = 0; i < self.imgs.length; i++ ) { self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit ); } if ( self.may_edit ) { // Respect user override for zoom, if any self.zoom_threshold = ImageAnnotator_config.zoom_threshold; if ( typeof window.ImageAnnotator_zoom_threshold !== 'undefined' && !isNaN( window.ImageAnnotator_zoom_threshold ) && window.ImageAnnotator_zoom_threshold >= 0.0 ) { // If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any // zooming. self.zoom_threshold = window.ImageAnnotator_zoom_threshold; } // Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio, // but only if we *can* zoom at least twice if ( self.viewers[ 0 ].full_img.width > 300 && Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0 ) { if ( self.viewers[ 0 ].thumb.width < 400 || self.viewers[ 0 ].thumb.width / self.viewers[ 0 ].thumb.height > 2.0 || self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0 ) { self.zoom_threshold = 0; // Force zooming } } self.editor = new ImageAnnotationEditor(); function track( evt ) { evt = evt || window.event; if ( self.is_adding ) { self.update_zoom( evt ); } if ( !self.is_tracking ) { return LAPI.Evt.kill( evt ); } var mouse_pos = LAPI.Pos.mousePosition( evt ); if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; } var origin = LAPI.Pos.position( self.cover ); // Make mouse pos relative to cover mouse_pos.x = mouse_pos.x - origin.x; mouse_pos.y = mouse_pos.y - origin.y; if ( mouse_pos.x >= self.base_x ) { self.definer.style.width = String( mouse_pos.x - self.base_x ) + 'px'; self.definer.style.left = String( self.base_x ) + 'px'; } else { self.definer.style.width = String( self.base_x - mouse_pos.x ) + 'px'; self.definer.style.left = String( mouse_pos.x ) + 'px'; } if ( mouse_pos.y >= self.base_y ) { self.definer.style.height = String( mouse_pos.y - self.base_y ) + 'px'; self.definer.style.top = String( self.base_y ) + 'px'; } else { self.definer.style.height = String( self.base_y - mouse_pos.y ) + 'px'; self.definer.style.top = String( mouse_pos.y ) + 'px'; } return LAPI.Evt.kill( evt ); } function pause( evt ) { LAPI.Evt.remove( document, 'mousemove', track, true ); if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); } self.move_listening = false; } function resume( evt ) { // captureEvents is actually deprecated, but I haven't succeeded to make this work with // addEventListener only. if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) { if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); } LAPI.Evt.attach( document, 'mousemove', track, true ); self.move_listening = true; } } function stop_tracking( evt ) { evt = evt || window.event; // Check that we're within the image. Note: this check can fail only on IE >= 7, on other // browsers, we attach the handler on self.cover and thus we don't even get events outside // that range. var mouse_pos = LAPI.Pos.mousePosition( evt ); if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; } if ( self.is_tracking ) { self.is_tracking = false; self.is_adding = false; // Done. pause(); if ( LAPI.Browser.is_ie ) { // Trust Microsoft to get everything wrong! LAPI.Evt.remove( document, 'mouseup', stop_tracking ); } else { LAPI.Evt.remove( self.cover, 'mouseup', stop_tracking ); } LAPI.Evt.remove( window, 'blur', pause ); LAPI.Evt.remove( window, 'focus', resume ); self.cover.style.cursor = 'auto'; LAPI.DOM.removeNode( self.border ); LAPI.Evt.remove( self.cover, self.mouse_in, self.update_zoom_evt ); LAPI.Evt.remove( self.cover, self.mouse_out, self.hide_zoom_evt ); self.hide_zoom(); self.viewers[ 0 ].hide(); // Hide all existing boxes if ( !self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0 ) { // Nothing: just remove the definer: if ( self.definer ) { LAPI.DOM.removeNode( self.definer ); } // Re-attach event handlers self.viewers[ 0 ].setShowHideEvents( true ); self.hide_cover(); self.viewers[ 0 ].setDefaultMsg(); // And make sure we get the real view again self.viewers[ 0 ].show(); } else { // We have a div with some extent: remove event capturing and create a new annotation var new_note = new ImageAnnotation( self.definer, self.viewers[ 0 ], -1 ); self.viewers[ 0 ].register( new_note ); self.editor.editNote( new_note ); } self.definer = null; } if ( evt ) { return LAPI.Evt.kill( evt ); } return false; } function start_tracking( evt ) { if ( !self.is_tracking ) { self.is_tracking = true; evt = evt || window.event; // Set the position, size 1 var mouse_pos = LAPI.Pos.mousePosition( evt ); var origin = LAPI.Pos.position( self.cover ); self.base_x = mouse_pos.x - origin.x; self.base_y = mouse_pos.y - origin.y; Object.merge( { left: String( self.base_x ) + 'px', top: String( self.base_y ) + 'px', width: '0px', height: '0px', display: '' } , self.definer.style ); // Set mouse handlers LAPI.Evt.remove( self.cover, 'mousedown', start_tracking ); if ( LAPI.Browser.is_ie ) { LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover... } else { LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking ); } resume(); LAPI.Evt.attach( window, 'blur', pause ); LAPI.Evt.attach( window, 'focus', resume ); } if ( evt ) { return LAPI.Evt.kill( evt ); } return false; } function add_new( evt ) { if ( !self.canEdit() ) { return; } self.editor.hide_editor(); Tooltips.close(); var cover = self.get_cover(); cover.style.cursor = 'crosshair'; self.definer = LAPI.make( 'div', null, { border: '1px solid ' + IA.new_border, display: 'none', position: 'absolute', top: '0px', left: '0px', width: '0px', height: '0px', padding: '0', lineHeight: '0px', // IE needs this, even though there are no lines within fontSize: '0px', // IE zIndex: cover.style.zIndex - 2 // Below the mouse capture div } ); self.viewers[ 0 ].img_div.appendChild( self.definer ); // Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image, // hence none of our tooltips are visible. self.viewers[ 0 ].img_div.appendChild( self.border ); self.show_cover(); self.is_tracking = false; self.is_adding = true; LAPI.Evt.attach( cover, 'mousedown', start_tracking ); resume(); self.button_div.style.display = 'none'; // Remove the event listeners on the image: IE sometimes invokes them even when the image is covered self.viewers[ 0 ].setShowHideEvents( false ); self.viewers[ 0 ].hide(); // Make sure notes are hidden self.viewers[ 0 ].toggle( true ); // Show all note rectangles (but only the dummies) self.update_zoom_evt = LAPI.Evt.makeListener( self, self.update_zoom ); self.hide_zoom_evt = LAPI.Evt.makeListener( self, self.hide_zoom ); self.show_zoom(); LAPI.Evt.attach( cover, self.mouse_in, self.update_zoom_evt ); LAPI.Evt.attach( cover, self.mouse_out, self.hide_zoom_evt ); LAPI.DOM.removeChildren( self.viewers[ 0 ].msg ); self.viewers[ 0 ].msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorDrawRectMsg', false ) ); self.viewers[ 0 ].msg.style.display = ''; } self.button_div = LAPI.make( 'div' ); self.viewers[ 0 ].main_div.appendChild( self.button_div ); self.add_button = LAPI.DOM.makeButton( 'ImageAnnotationAddButton', ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ), add_new ); var add_plea = ImageAnnotator.UI.needs_plea; self.button_div.appendChild( self.add_button ); self.help_link = self.createHelpLink(); if ( self.help_link ) { self.button_div.appendChild( document.createTextNode( '\xa0' ) ); self.button_div.appendChild( self.help_link ); } if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); } } // end if may_edit // Get the file description pages of thumbnails. Figure out for which viewers we need to do this. var cache = {}; var get_local = []; var get_foreign = []; Array.forEach( self.viewers, function ( viewer, idx ) { if ( viewer.setup_done || viewer.isLocal && !viewer.has_page ) { return; } // Handle only images that either are foreign or local and do have a page. if ( cache[ viewer.realName ] ) { cache[ viewer.realName ][ cache[ viewer.realName ].length ] = idx; } else { cache[ viewer.realName ] = [ idx ]; if ( !viewer.has_page ) { get_foreign[ get_foreign.length ] = viewer.realName; } else { get_local[ get_local.length ] = viewer.realName; } } } ); if ( get_local.length === 0 && get_foreign.length === 0 ) { return; } // Now we have unique page names in the cache and in to_get. Go get the corresponding file // description pages. We make a series of simultaneous asynchronous calls to avoid hitting // API limits and to keep the URL length below the limit for the foreign_repo calls. function make_calls( list, execute_call, url_limit ) { function composer( list, from, length, url_limit ) { function compose( list, from, length, url_limit ) { var text = ''; var done = 0; for ( var i = from; i < from + length; i++ ) { var new_text = '<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">' + '<span class="image_annotation_inline_name">' + list[ i ] + '</span>' + '{{:' + list[ i ] + '}}' + // Leading dot to avoid getting links to the full images if we hit a parser limit '</div>'; if ( url_limit ) { new_text = encodeURIComponent( new_text ); if ( text.length && ( text.length + new_text.length > url_limit ) ) { break; } } text = text + new_text; done++; // Additionally, limit the number of image pages to get: these can be large, and the server // may refuse to actually do the transclusions but may in that case include the full images // in the result, which would make us load the full images, which is desastrous if there are // many thumbs to large images on the page. if ( done == 5 ) { break; } } return { text: text, n: done }; } var param = compose( list, from, length, url_limit ); execute_call( param.text ); return param.n; } var start = 0, chunk = 0, to_do = list.length; while ( to_do > 0 ) { chunk = composer( list, start, Math.min( 50, to_do ), url_limit ); to_do -= chunk; start += chunk; } } var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig; var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"'; var inlineNameEnd = '</span>'; var noteStart = '<div id="image_annotation_note_'; var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g; // Our parse request returns the full html of the description pages' contents, including any // license or information or other templates that we don't care about, and which may contain // additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we // strip out everything but the notes. function strip_noise( html ) { var result = ''; var m; // First, get rid of HTML comments and scripts html = html.replace( /<!--(.|\s)*?-->/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' ); var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length; // Now collect pages while ( idx < l && i >= idx ) { var j = html.indexOf( inlineNameEnd, i + blockStart.length ); if ( j < i + blockStart.length ) { break; } result += html.substring( i, j + inlineNameEnd.length ); idx = j + inlineNameEnd.length; // Now collect all image image notes for that page var note_begin = 0, next_block = html.indexOf( blockStart, idx ); // Do we have image note control or color templates? j = idx; for ( ;; ) { noteControlRE.lastIndex = j; m = noteControlRE.exec( html ); if ( !m || next_block >= idx && m.index > next_block ) { break; } result += m[ 0 ]; j = m.index + m[ 0 ].length; } // Collect notes for ( ;; ) { note_begin = html.indexOf( noteStart, idx ); // Check whether next wrapper comes first if ( note_begin < idx || ( next_block >= idx && note_begin > next_block ) ) { break; } // Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags. var level = 1, k = note_begin + noteStart.length; while ( level > 0 && k < l ) { divRE.lastIndex = k; m = divRE.exec( html ); if ( !m || m.length < 2 ) { k = l; // Nothing found at all? } else { if ( m[ 1 ] ) { level++; k = m.index + m[ 1 ].length; // divStart found first } else if ( m.length > 2 && m[ 2 ] ) { level--; k = m.index + m[ 2 ].length; // divEnd found first } else { k = l; // Huh? } } } // end loop for nested divs result += html.substring( note_begin, k ); while ( level-- > 0 ) { result += '</div>'; } // Missing ends. idx = k; } // end loop collecting notes result += '</div>'; // Close the wrapper i = next_block; } // end loop collecting pages return result; } function setup_thumb_viewers( html_text ) { var node = LAPI.make( 'div', null, { display: 'none' } ); document.body.appendChild( node ); try { node.innerHTML = strip_noise( html_text ); var pages = getElementsByClassName( node, 'div', 'wpImageAnnotatorInlineImageWrapper' ); for ( var i = 0; pages && i < pages.length; i++ ) { var notes = getElementsByClassName( pages[ i ], 'div', self.annotation_class ); if ( !notes || notes.length === 0 ) { continue; } var page = self.getItem( 'inline_name', pages[ i ] ); if ( !page ) { continue; } page = page.replace( / /g, '_' ); var viewers = cache[ page ] || cache[ 'File:' + page.substring( page.indexOf( ':' ) + 1 ) ]; if ( !viewers || viewers.length === 0 ) { continue; } // Update rules. var rules = getElementsByClassName( pages[ i ], 'div', 'wpImageAnnotatorInlinedRules' ); var local_rules = { inline: Object.clone( IA.rules.inline ), thumbs: Object.clone( IA.rules.thumbs ) }; if ( rules && rules.length ) { rules = rules[ 0 ]; if ( typeof local_rules.inline.show === 'undefined' && LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoInlineDisplay' ) ) { local_rules.inline.show = false; } if ( typeof local_rules.inline.icon === 'undefined' && LAPI.DOM.hasClass( rules, 'wpImageAnnotatorInlineDisplayIcon' ) ) { local_rules.inline.icon = true; } if ( typeof local_rules.thumbs.show === 'undefined' && LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoThumbs' ) ) { local_rules.thumbs.show = false; } if ( typeof local_rules.thumbs.icon === 'undefined' && LAPI.DOM.hasClass( rules, 'wpImageAnnotatorThumbDisplayIcon' ) ) { local_rules.thumbs.icon = true; } } // Make sure all are set local_rules.inline.show = typeof local_rules.inline.show === 'undefined' || local_rules.inline.show; local_rules.thumbs.show = typeof local_rules.thumbs.show === 'undefined' || local_rules.thumbs.show; local_rules.inline.icon = typeof local_rules.inline.icon !== 'undefined' && local_rules.inline.icon; local_rules.thumbs.icon = typeof local_rules.thumbs.icon !== 'undefined' && local_rules.thumbs.icon; if ( !local_rules.inline.show ) { continue; } // Now use pages[i] as a scope shared by all the viewers using it. Since we clone note // contents for note display, this works. Otherwise, we'd have to copy the notes into // each viewer's scope. document.body.appendChild( pages[ i ] ); // Move it out of 'node' // Set viewers' scopes and finish their setup. Array.forEach( viewers, function ( v ) { if ( !self.viewers[ v ].isThumbnail || local_rules.thumbs.show ) { self.viewers[ v ].scope = pages[ i ]; self.viewers[ v ].setup( self.viewers[ v ].isThumbnail && local_rules.thumbs.icon || self.viewers[ v ].isOther && local_rules.inline.icon ); } } ); } } catch ( ex ) {} LAPI.DOM.removeNode( node ); } ImageAnnotator.script_callbacks = []; function make_script_calls( list, api ) { var template = api + '?action=parse&pst&text=&prop=text&format=json' + '&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764 '&callback=ImageAnnotator.script_callbacks[].callback'; if ( template.startsWith( '//' ) ) { template = document.location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug) make_calls( list, function ( text ) { var idx = ImageAnnotator.script_callbacks.length; ImageAnnotator.script_callbacks[ idx ] = { callback: function ( json ) { if ( json && json.parse && json.parse.text && json.parse.text[ '*' ] ) { setup_thumb_viewers( json.parse.text[ '*' ] ); } ImageAnnotator.script_callbacks[ idx ].done = true; if ( ImageAnnotator.script_callbacks[ idx ].script ) { LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script ); ImageAnnotator.script_callbacks[ idx ].script = null; } }, done: false }; ImageAnnotator.script_callbacks[ idx ].script = IA.getScript( template.replace( 'script_callbacks[].callback', 'script_callbacks[' + idx + '].callback' ) .replace( '&text=&', '&text=' + text + '&' ), true // No local caching! ); if ( ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[ idx ] && ImageAnnotator.script_callbacks[ idx ].done && ImageAnnotator.script_callbacks[ idx ].script ) { LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script ); ImageAnnotator.script_callbacks[ idx ].script = null; } }, ( LAPI.DOM.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters ); } if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) { make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' ); } else { make_calls( get_local, function ( text ) { LAPI.Ajax.parseWikitext( text, function ( html_text ) { if ( html_text ) { setup_thumb_viewers( html_text ); } }, function () {}, false, null, 'API', // Fixed string to enable caching at all 1800 // 30 minutes caching. ); } ); } // Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org // to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083 // character (2048 in the path part), and servers also may impose limits (on the WMF servers, // the limit appears to be 8kB). make_script_calls( get_foreign, ImageAnnotator_config.sharedRepositoryAPI() ); }, show_zoom: function () { var self = IA; if ( ( self.viewers[ 0 ].factors.dx < self.zoom_threshold && self.viewers[ 0 ].factors.dy < self.zoom_threshold ) || Math.max( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) < 2.0 ) { // Below zoom threshold, or full image not even twice the size of the preview return; } if ( !self.zoom ) { self.zoom = LAPI.make( 'div', { id: 'image_annotator_zoom' }, { overflow: 'hidden', width: '200px', height: '200px', position: 'absolute', display: 'none', top: '0px', left: '0px', border: '2px solid #666666', backgroundColor: 'white', zIndex: 2050 // On top of everything } ); var src = self.viewers[ 0 ].img.getAttribute( 'src', 2 ); // Adjust zoom_factor if ( self.zoom_factor > self.viewers[ 0 ].factors.dx || self.zoom_factor > self.viewers[ 0 ].factors.dy ) { self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ); } self.zoom.appendChild( LAPI.make( 'div', null, { position: 'relative' } ) ); // Calculate zoom size and source link var zoom_width = Math.floor( self.viewers[ 0 ].thumb.width * self.zoom_factor ); var zoom_height = Math.floor( self.viewers[ 0 ].thumb.height * self.zoom_factor ); // For SVGs, always use a scaled PNG for the zoom. if ( zoom_width > 0.9 * self.viewers[ 0 ].full_img.width && src.search( /\.svg\.png$/i ) < 0 ) { // If the thumb we'd be loading was within about 80% of the full image size, we may just as // well get the full image instead of a scaled version. self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ); zoom_width = self.viewers[ 0 ].full_img.width; zoom_height = self.viewers[ 0 ].full_img.height; } // Construct the initial zoomed image. We need to clone; if we create a completely // new DOM node ourselves, it may not work on IE6... var zoomed = self.viewers[ 0 ].img.cloneNode( true ); zoomed.width = String( zoom_width ); zoomed.height = String( zoom_height ); Object.merge( { position: 'absolute', top: '0px', left: '0px' }, zoomed.style ); self.zoom.firstChild.appendChild( zoomed ); // Crosshair self.zoom.firstChild.appendChild( LAPI.make( 'div', null, { width: '1px', height: '200px', borderLeft: '1px solid red', position: 'absolute', top: '0px', left: '100px' } ) ); self.zoom.firstChild.appendChild( LAPI.make( 'div', null, { width: '200px', height: '1px', borderTop: '1px solid red', position: 'absolute', top: '100px', left: '0px' } ) ); document.body.appendChild( self.zoom ); LAPI.DOM.loadImage( self.viewers[ 0 ].imgName, src, zoom_width, zoom_height, ImageAnnotator_config.thumbnailsGeneratedAutomatically(), function ( img ) { // Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets img.style.position = 'absolute'; img.style.top = self.zoom.firstChild.firstChild.style.top; img.style.left = self.zoom.firstChild.firstChild.style.left; img.style.display = ''; self.zoom.firstChild.replaceChild( img, self.zoom.firstChild.firstChild ); } ); } self.zoom.style.display = 'none'; // Will be shown in update }, update_zoom: function ( evt ) { if ( !evt ) { return; } // We need an event to calculate positions! var self = IA; if ( !self.zoom ) { return; } var mouse_pos = LAPI.Pos.mousePosition( evt ); var origin = LAPI.Pos.position( self.cover ); if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { IA.hide_zoom(); return; } var dx = mouse_pos.x - origin.x; var dy = mouse_pos.y - origin.y; // dx, dy is the offset within the preview image. Align the zoom image accordingly. var top = -dy * self.zoom_factor + 100; var left = -dx * self.zoom_factor + 100; self.zoom.firstChild.firstChild.style.top = String( top ) + 'px'; self.zoom.firstChild.firstChild.style.left = String( left ) + 'px'; self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer // Horizontally keep it in view. var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 ); if ( x < 0 ) { x = 0; } self.zoom.style.left = x + 'px'; self.zoom.style.display = ''; // Now that we have offsetWidth, correct the position. if ( self.is_rtl ) { x = mouse_pos.x - 10 - self.zoom.offsetWidth; if ( x < 0 ) { x = 0; } } else { var off = LAPI.Pos.scrollOffset(); var view = LAPI.Pos.viewport(); if ( x + self.zoom.offsetWidth > off.x + view.x ) { x = off.x + view.x - self.zoom.offsetWidth; } if ( x < 0 ) { x = 0; } } self.zoom.style.left = x + 'px'; }, hide_zoom: function ( evt ) { if ( !IA.zoom ) { return; } if ( evt ) { var mouse_pos = LAPI.Pos.mousePosition( evt ); if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) { return; } } IA.zoom.style.display = 'none'; }, createHelpLink: function () { var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true ); if ( !msg || !msg.lastChild ) { return null; } // Make sure we use the right protocol for all images: var imgs = msg.getElementsByTagName( 'img' ); var text; var tgt; if ( imgs ) { for ( var i = 0; i < imgs.length; i++ ) { var srcFixed = imgs[ i ].getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol ); imgs[ i ].src = srcFixed; } } if ( msg.childNodes.length == 1 && msg.firstChild.nodeName.toLowerCase() == 'a' && !LAPI.DOM.hasClass( msg.firstChild, 'image' ) ) { msg.firstChild.id = 'ImageAnnotationHelpButton'; return msg.firstChild; // Single link } // Otherwise, it's either a sequence of up to three images, or a span, followed by a // link. tgt = msg.lastChild; if ( tgt.nodeName.toLowerCase() != 'a' ) { tgt = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', 'Help:Gadget-ImageAnnotator' ); } else { tgt = tgt.href; } function make_handler( tgt ) { var target = tgt; return function ( evt ) { var e = evt || window.event; location.href = target; if ( e ) { return LAPI.Evt.kill( e ); } return false; }; } imgs = msg.getElementsByTagName( 'img' ); if ( !imgs || !imgs.length ) { // We're supposed to have a spans giving the button text text = msg.firstChild; if ( text.nodeName.toLowerCase() === 'span' ) { text = LAPI.DOM.getInnerText( text ); } else { text = 'Help'; } return LAPI.DOM.makeButton( 'ImageAnnotationHelpButton' , text , make_handler( tgt ) ); } else { return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) ); } }, get_cover: function () { var self = IA; var shim; if ( !self.cover ) { var pos = { position: 'absolute', left: '0px', top: '0px', width: self.viewers[ 0 ].thumb.width + 'px', height: self.viewers[ 0 ].thumb.height + 'px' }; self.cover = LAPI.make( 'div', null, pos ); self.border = self.cover.cloneNode( false ); Object.merge( { border: '3px solid green', top: '-3px', left: '-3px' }, self.border.style ); self.cover.style.zIndex = 2000; // Above the tooltips if ( LAPI.Browser.is_ie ) { shim = LAPI.make( 'iframe', { frameBorder: 0, tabIndex: -1 }, pos ); shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency // Unfortunately, IE6/SP2 has a "security setting" called "Binary and script // behaviors". If that is disabled, filters don't work, and our iframe would // appear as a white rectangle. Fix this by first placing the iframe just above // image (to block that windowed control) and then placing *another div* just // above that shim having the image as its background image. var imgZ = self.viewers[ 0 ].img.style.zIndex; if ( isNaN( imgZ ) ) { imgZ = 10; } // Arbitrary, positive, > 1, < 500 shim.style.zIndex = imgZ + 1; self.ieFix = shim; // And now the bgImage div... shim = LAPI.make( 'div', null, pos ); Object.merge( { top: '0px', backgroundImage: 'url(' + self.viewers[ 0 ].img.src + ')', zIndex: imgZ + 2 } , shim.style ); self.ieFix2 = shim; } if ( LAPI.Browser.is_opera ) { // It appears that events just pass through completely transparent divs on Opera. // Hence we have to ensure that these events are killed even if our cover doesn't // handle them. shim = LAPI.make( 'div', null, pos ); shim.style.zIndex = self.cover.style.zIndex - 1; LAPI.Evt.attach( shim, 'mousemove', function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } ); LAPI.Evt.attach( shim, 'mousedown', function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } ); LAPI.Evt.attach( shim, 'mouseup', function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } ); shim.style.cursor = 'default'; self.eventFix = shim; } self.cover_visible = false; } return self.cover; }, show_cover: function () { var self = IA; if ( self.cover && !self.cover_visible ) { if ( self.ieFix ) { self.viewers[ 0 ].img_div.appendChild( self.ieFix ); self.viewers[ 0 ].img_div.appendChild( self.ieFix2 ); } if ( self.eventFix ) { self.viewers[ 0 ].img_div.appendChild( self.eventFix ); } self.viewers[ 0 ].img_div.appendChild( self.cover ); self.cover_visible = true; } }, hide_cover: function () { var self = IA; if ( self.cover && self.cover_visible ) { if ( self.ieFix ) { LAPI.DOM.removeNode( self.ieFix ); LAPI.DOM.removeNode( self.ieFix2 ); } if ( self.eventFix ) { LAPI.DOM.removeNode( self.eventFix ); } LAPI.DOM.removeNode( self.cover ); self.cover_visible = false; } }, getRawItem: function ( what, scope ) { var node = null; if ( !scope || scope == document ) { node = LAPI.$( 'image_annotation_' + what ); } else { node = getElementsByClassName( scope, '*', 'image_annotation_' + what ); if ( node && node.length ) { node = node[ 0 ]; } else { node = null; } } return node; }, getItem: function ( what, scope ) { var node = IA.getRawItem( what, scope ); if ( !node ) { return null; } return LAPI.DOM.getInnerText( node ).trim(); }, getIntItem: function ( what, scope ) { var x = IA.getItem( what, scope ); if ( x !== null ) { x = parseInt( x, 10 ); } return x; }, findNote: function ( text, id ) { function find( text, id, delim ) { var start = delim.start.replace( '$1', id ); var start_match = text.indexOf( start ); if ( start_match < 0 ) { return null; } var end = delim.end.replace( '$1', id ); var end_match = text.indexOf( end ); if ( end_match < start_match + start.length ) { return null; } return { start: start_match, end: end_match + end.length }; } var result = null; for ( var i = 0; i < IA.note_delim.length && !result; i++ ) { result = find( text, id, IA.note_delim[ i ] ); } return result; }, setWikitext: function ( pagetext ) { var self = IA; if ( self.wiki_read ) { return; } Array.forEach( self.viewers[ 0 ].annotations, function ( note ) { if ( note.model.id >= 0 ) { var span = self.findNote( pagetext, note.model.id ); if ( !span ) { return; } // Now extract the wikitext var code = pagetext.substring( span.start, span.end ); for ( var i = 0; i < self.note_delim.length; i++ ) { var start = self.note_delim[ i ].content_start.replace( '$1', note.model.id ); var end = self.note_delim[ i ].content_end.replace( '$1', note.model.id ); var j = code.indexOf( start ); var k = code.indexOf( end ); if ( j >= 0 && k >= 0 && k >= j + start.length ) { note.model.wiki = code.substring( j + start.length, k ).trim(); return; } } } } ); self.wiki_read = true; }, setSummary: function ( summary, initial_text, note_text ) { if ( initial_text.contains( '$1' ) ) { var max = ( summary.maxlength || 200 ) - initial_text.length; if ( note_text ) { initial_text = initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) ); } else { initial_text = initial_text.replace( '$1', '' ); } } summary.value = initial_text; }, getScript: function ( url, bypass_local_cache, bypass_caches ) { // Don't use LAPI here, it may not yet be available if ( bypass_caches ) { url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime(); } // Avoid protocol-relative URIs (IE7 bug) if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) { url = document.location.protocol + url; } if ( bypass_local_cache ) { var s = document.createElement( 'script' ); s.setAttribute( 'src', url ); s.setAttribute( 'type', 'text/javascript' ); document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); return s; } else { return mw.loader.load( url ); } }, canEdit: function () { var self = IA; if ( self.may_edit ) { if ( !self.ajaxQueried ) { self.haveAjax = ( LAPI.Ajax.getRequest() != null ); self.ajaxQueried = true; self.may_edit = self.haveAjax; if ( !self.may_edit && self.button_div ) { LAPI.DOM.removeChildren( self.button_div ); self.button_div.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorCannotEditMsg', false ) ); self.viewers[ 0 ].msg.style.display = ''; self.viewers[ 0 ].cannotEdit(); } } } return self.may_edit; } }; // end IA // Backwards compatibility function getElementsByClassName( scope, tag, className ) { if ( window.jQuery ) { return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className ); } else { // For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName return getElementsByClassName( scope, tag, className ); } } window.ImageAnnotator = { install: function ( config ) { IA.install( config ); } }; // Start it. Bypass caches; but allow for 4 hours client-side caching. Small file. IA.getScript( mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript', true // No local caching! ); }() ); // end local scope } // end if (guard against double inclusions) jsh879v26gaodlkrdv2qjwlufpioq7s MediaWiki:Gadget-imagelinks.js 8 24462 268699 2026-04-27T16:13:28Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268699 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Direct imagelinks to Commons * * Required modules: mediawiki.util * * @source https://www.mediawiki.org/wiki/Snippets/Direct_imagelinks_to_Commons * @author Krinkle * @version 2015-08-01 */ if ( mw.config.get( 'wgNamespaceNumber', 0 ) >= 0 ) { mw.hook( 'wikipage.content' ).add( function ( $content ) { var uploadBaseRe = /^\/\/upload\.wikimedia\.org\/wikipedia\/commons/, localFileNSString = mw.config.get( 'wgFormattedNamespaces' )['6'] + ':', localBasePath = new RegExp( '^' + mw.util.escapeRegExp( mw.util.getUrl( localFileNSString ) ) ), localBaseScript = new RegExp( '^' + mw.util.escapeRegExp( mw.util.wikiScript() + '?title=' + mw.util.wikiUrlencode( localFileNSString ) ) ), commonsBasePath = '//commons.wikimedia.org/wiki/File:', commonsBaseScript = '//commons.wikimedia.org/w/index.php?title=File:'; $content.find( 'a.image, a.mw-file-description' ).attr( 'href', function ( i, currVal ) { if ( uploadBaseRe.test( $( this ).find( 'img' ).attr( 'src' ) ) ) { return currVal .replace( localBasePath, commonsBasePath ) .replace( localBaseScript, commonsBaseScript ); } } ); } ); } dg00hmuxafw9q5pvwumq0btlfyu6see MediaWiki:Gadget-imagelinks 8 24463 268700 2026-04-27T16:22:02Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Şol ýerde ýerleşdirilen faýllar üçin surat baglanyşyklaryny Commons-a gönükdirmek' 268700 wikitext text/x-wiki Şol ýerde ýerleşdirilen faýllar üçin surat baglanyşyklaryny Commons-a gönükdirmek 1oh6i98hn1m18l3azcyb7sb3n2f5zlv MediaWiki:Gadget-Navigation popups 8 24464 268701 2026-04-27T16:24:12Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[Wikipedia:Tools/Navigation popups|Navigation popups]]: Baglanyşyklaryň üstünde siçan baranda makalalaryň deslapky görnüşleri we redaktirleme funksiýalary peýda edmek' 268701 wikitext text/x-wiki [[Wikipedia:Tools/Navigation popups|Navigation popups]]: Baglanyşyklaryň üstünde siçan baranda makalalaryň deslapky görnüşleri we redaktirleme funksiýalary peýda edmek 94aafm8xpygaixx37fdr1i0gosnpe98 MediaWiki:Gadget-exlinks 8 24465 268702 2026-04-27T16:25:49Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Daşarky baglanyşyklary täze goýmada ýa-da penjirede açmek' 268702 wikitext text/x-wiki Daşarky baglanyşyklary täze goýmada ýa-da penjirede açmek tfxg0j3pyr71y6syn6epzisz4fgsrlp MediaWiki:Gadget-exlinks.js 8 24466 268703 2026-04-27T16:27:12Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// ********************************************************************** // ** ***WARNING GLOBAL GADGET FILE*** ** // ** changes to this file affect many users. ** // ** please discuss on the talk page before editing ** // ** ** // ********************************************************************** /** * @source mediawik...' 268703 javascript text/javascript // ********************************************************************** // ** ***WARNING GLOBAL GADGET FILE*** ** // ** changes to this file affect many users. ** // ** please discuss on the talk page before editing ** // ** ** // ********************************************************************** /** * @source mediawiki.org/wiki/Snippets/Open_external_links_in_new_window * @version 5 */ mw.hook('wikipage.content').add(function($content) { // Second selector is for external links in Parsoid HTML+RDFa output (bug 65243). $content.find('a.external, a[rel="mw:ExtLink"]').each(function () { // Can't use wgServer because it can be protocol relative // Use this.href property instead of this.getAttribute('href') because the property // is converted to a full URL (including protocol) if (this.href.indexOf(location.protocol + '//' + location.hostname) !== 0) { this.target = '_blank'; if ( this.rel.indexOf( 'noopener' ) < 0 ) { this.rel += ' noopener'; // the leading space matters, rel attributes have space-separated tokens } if ( this.rel.indexOf( 'noreferrer' ) < 0 ) { this.rel += ' noreferrer'; // the leading space matters, rel attributes have space-separated tokens } } }); }); la8tkkgxfljayougc48gucmj2telov4 MediaWiki:Gadget-search-new-tab.js 8 24467 268704 2026-04-27T16:28:48Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* JSHint-valid */ /* globals $:false */ $(function() { // Special:Search for all skins. $('#powersearch, #search').on('keydown', function(e) { $(this).prop('target', e.ctrlKey || e.metaKey ? '_blank' : ''); }); // CodexTypeaheadSearch header search [only on Vector (2022)]. $('#p-search').on('keydown', '.cdx-typeahead-search #searchform', function(e) { if ((e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) { var URI = $(this).find('.cd...' 268704 javascript text/javascript /* JSHint-valid */ /* globals $:false */ $(function() { // Special:Search for all skins. $('#powersearch, #search').on('keydown', function(e) { $(this).prop('target', e.ctrlKey || e.metaKey ? '_blank' : ''); }); // CodexTypeaheadSearch header search [only on Vector (2022)]. $('#p-search').on('keydown', '.cdx-typeahead-search #searchform', function(e) { if ((e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) { var URI = $(this).find('.cdx-menu-item--selected a.cdx-menu-item__content').prop('href'); if (URI != undefined) { window.open(URI, '_blank'); return false; } } }); // Header/Side search on other skins [no auto suggest nav on MinervaNeue]. // Search box [auto suggest on Vector legacy (2010), MonoBook, Timeless]. $('div:not(.cdx-typeahead-search) #searchform #searchInput, .searchbox .mw-searchInput').on('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) { if ($(this).data('suggestionsContext') != undefined) { var selectedIndex = $(this).data('suggestionsContext').config.suggestions.indexOf($(this).val()); if (selectedIndex != -1) { var URI = $('.suggestions-results a:nth-child(' + (selectedIndex + 1) + ')').prop('href'); if (URI != undefined) { window.open(URI, '_blank'); $(this).trigger('blur'); } } } } }); }); 0eui6t13fyskl2pivokziurqfo585nz MediaWiki:Gadget-search-new-tab 8 24468 268705 2026-04-27T16:30:17Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Ctrl düwmesini basyp saklanyňyzda gözleg netijelerini täze goýmada ýa-da penjirede açmek ([[MediaWiki talk:Gadget-search-new-tab.js|discuss]])' 268705 wikitext text/x-wiki Ctrl düwmesini basyp saklanyňyzda gözleg netijelerini täze goýmada ýa-da penjirede açmek ([[MediaWiki talk:Gadget-search-new-tab.js|discuss]]) rw2zw8bbqxg3cqt88k9yvch17cpx4al MediaWiki:Gadget-PrintOptions.js 8 24469 268706 2026-04-27T16:31:36Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/** * Print options is a Gadget writen by Derk-Jan Hartman / User:TheDJ * For more information see [[User:TheDJ/Print_options]] * * Licensed MIT and/or CC-by-SA 4.0 * * Copyright (c) 2010-2017 Derk-Jan Hartman / User:TheDJ * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the r...' 268706 javascript text/javascript /** * Print options is a Gadget writen by Derk-Jan Hartman / User:TheDJ * For more information see [[User:TheDJ/Print_options]] * * Licensed MIT and/or CC-by-SA 4.0 * * Copyright (c) 2010-2017 Derk-Jan Hartman / User:TheDJ * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ ( function () { 'use strict'; var windowManager; var printDialog; var printOptions = { install: function () { var $printLink = $( '#t-print a' ); if ( $printLink.length === 0 ) { return; } $printLink .text( 'Print page' ) .off( 'click' ) .get( 0 ).addEventListener( 'click', function ( e ) { mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ] ).done( printOptions.createWindow ); e.stopPropagation(); e.preventDefault(); }, true ); // Use capturing phase, to beat the other click handler // Late pre-loading mw.loader.load( [ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ] ); }, createWindow: function () { function PrintDialog( config ) { PrintDialog.super.call( this, config ); } OO.inheritClass( PrintDialog, OO.ui.ProcessDialog ); PrintDialog.static.name = 'printdialog'; PrintDialog.static.title = 'Print this page'; PrintDialog.static.actions = [ { action: 'print', label: 'Print', flags: 'primary' }, { label: 'Cancel', flags: 'safe' } ]; PrintDialog.prototype.initialize = function () { var checkbox, fieldset = []; PrintDialog.super.prototype.initialize.apply( this, arguments ); this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } ); this.content = new OO.ui.FieldsetLayout(); for ( var i = 0; i < printOptions.questions.length; i++ ) { if ( printOptions.questions[ i ].type === 'checkbox' ) { checkbox = new OO.ui.CheckboxInputWidget( { selected: printOptions.questions[ i ].checked } ); printOptions.questions[ i ].widget = checkbox; fieldset.push( new OO.ui.FieldLayout( checkbox, { label: printOptions.questions[ i ].label, align: 'inline' } ) ); } } this.content.addItems( fieldset ); this.panel.$element.append( this.content.$element ); this.$body.append( this.panel.$element ); }; PrintDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action === 'print' ) { return new OO.ui.Process( function () { // Get values of checkboxes var question; for ( var i = 0; i < printOptions.questions.length; i++ ) { question = printOptions.questions[ i ]; if ( question.type === 'checkbox' && question.widget ) { printOptions[ question.returnvalue ] = question.widget.isSelected(); } } dialog.close( { action: action } ).done( function () { printOptions.changePrintCSS(); printOptions.otherEnhancements(); window.print(); window.location = window.location; } ); } ); } return PrintDialog.super.prototype.getActionProcess.call( this, action ); }; if ( !windowManager ) { windowManager = new OO.ui.WindowManager(); $( 'body' ).append( windowManager.$element ); } if ( !printDialog ) { printDialog = new PrintDialog( { size: 'medium' } ); windowManager.addWindows( [ printDialog ] ); } windowManager.openWindow( printDialog ); }, changePrintCSS: function () { /* Here we: - disable stylesheets that are print specific - make screen specific stylesheets also enabled for print medium - remove print specific stylerules - make screen specific stylerules also enabled for print medium */ var printStyle = ''; if ( this.enhanced === false ) { var i, j, k, rule, hasPrint, hasScreen, rules, stylesheet, stylesheets = document.styleSheets; for ( i = 0; i < stylesheets.length; i++ ) { stylesheet = stylesheets[ i ]; if ( !stylesheet.media ) { continue; } if ( stylesheet.media.mediaText && stylesheet.media.mediaText.indexOf( 'print' ) !== -1 ) { if ( stylesheet.media.mediaText.indexOf( 'screen' ) === -1 ) { stylesheet.disabled = true; } } else if ( stylesheet.media.mediaText && stylesheet.media.mediaText.indexOf( 'screen' ) !== -1 ) { if ( stylesheet.media.mediaText.indexOf( 'print' ) === -1 ) { try { stylesheet.media.appendMedium( 'print' ); } catch ( e ) { stylesheet.media.mediaText += ',print'; } } } /* now test individual stylesheet rules */ try { rules = stylesheet.cssRules || stylesheet.rules; } catch ( e ) { /* Cross domain issue. */ mw.log.warn( 'Not possible to correct stylesheet due to cross origin restrictions.' ); continue; } stylesheet.compatdelete = stylesheet.deleteRule || stylesheet.removeRule; for ( j = 0; rules && j < rules.length; j++ ) { rule = rules[ j ]; hasPrint = false; hasScreen = false; if ( rule.type === CSSRule.MEDIA_RULE && rule.media ) { for ( k = 0; k < rule.media.length; k++ ) { if ( rule.media[ k ] === 'print' ) { hasPrint = true; } else if ( rule.media[ k ] === 'screen' ) { hasScreen = true; } } } else { continue; } if ( hasPrint && !hasScreen ) { stylesheet.compatdelete( j ); j--; } else if ( hasScreen && !hasPrint ) { try { rule.media.appendMedium( 'print' ); } catch ( e ) { rule.media.mediaText += ',print'; } } } } } /* Add css to hide images */ if ( this.noimages ) { printStyle += 'img, .thumb {display:none;}\n'; } /* Add css to hide references markers and the references lists */ if ( this.norefs ) { printStyle += '.mw-heading:has(#References), ol.references, .reference {display:none;}\n'; } if ( this.notoc ) { printStyle += '#toc, .toc {display:none;}\n'; } if ( this.nobackground ) { printStyle += '* {background:none !important;}\n'; } if ( this.blacktext ) { printStyle += '* {color:black !important;}\n'; } if ( printStyle ) { $( 'head' ).append( '<style type="text/css" media="print">' + printStyle + '</style>' ); } }, /* Rewrite the "retrieved from" url to be readable */ otherEnhancements: function () { var link = $( 'div.printfooter a' ); link.text( decodeURI( link.text() ) ); }, questions: [ { label: 'Hide interface elements', type: 'checkbox', checked: true, returnvalue: 'enhanced' }, { label: 'Hide images', type: 'checkbox', checked: false, returnvalue: 'noimages' }, { label: 'Hide references', type: 'checkbox', checked: false, returnvalue: 'norefs' }, { label: 'Hide Table of Contents', type: 'checkbox', checked: false, returnvalue: 'notoc' }, { label: 'Remove backgrounds (Your browser might or might not override this)', type: 'checkbox', checked: false, returnvalue: 'nobackground' }, { label: 'Force all text to black', type: 'checkbox', checked: true, returnvalue: 'blacktext' } ] }; if ( mw.config.get( 'wgNamespaceNumber' ) >= 0 ) { $( function () { // This can be before the click handler by MW is installed. Instead, // re-add ourselves to the back of the document.ready list // use async timeoute to do this setTimeout( function () { $( printOptions.install ); } ); } ); } }() ); bkdsih0csyyk2yovr6emj33oq67cxiq MediaWiki:Gadget-PrintOptions 8 24470 268707 2026-04-27T16:33:08Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[User:TheDJ/Print options|Print options]]: sahypalaryň nähili çap edilýändigini gözegçilikde saklamak (meselem, suratlary ýa-da fonlary aýyrmak)' 268707 wikitext text/x-wiki [[User:TheDJ/Print options|Print options]]: sahypalaryň nähili çap edilýändigini gözegçilikde saklamak (meselem, suratlary ýa-da fonlary aýyrmak) 4oitina8zu97kuyhk2gvad01avn1a6t MediaWiki:Gadget-revisionjumper.js 8 24471 268708 2026-04-27T16:36:04Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268708 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Imported from version 1.2.3 as of 2010-09-08 from [[:de:MediaWiki:Gadget-revisionjumper.js]] * Using this script allows you to easily navigate diffs, articles' histories and articles themselves. * A feature-rich drop down menu offers various functions to jump to a certain revision * which is situated before or after the selected revision. * Besides default parameters, configurable requests can be done. * Further, the default setting can be personalized. * See [[User:DerHexer/revisionjumper]] */ // [[:de:MediaWiki:Gadget-revisionjumper.js]] mw.loader.load( '//de.wikipedia.org/w/index.php?title=MediaWiki:Gadget-revisionjumper.js&action=raw&ctype=text/javascript' ); 4799naz0c1ubn84inr4ycz04xgprvdr MediaWiki:Gadget-revisionjumper 8 24472 268709 2026-04-27T16:37:15Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-sister}}">(S)</abbr></sup> [[User:DerHexer/revisionjumper|revisionjumper]]: sahypanyň redaktsiýalarynyň arasynda çalt gezelenç edmek' 268709 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-sister}}">(S)</abbr></sup> [[User:DerHexer/revisionjumper|revisionjumper]]: sahypanyň redaktsiýalarynyň arasynda çalt gezelenç edmek n7119c502wiabuvkl5ipru8rknekafj MediaWiki:Gadget-Twinkle 8 24473 268710 2026-04-27T16:39:33Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[WP:Twinkle|Twinkle]]: wandalizm barada habar bermek, wandallary duýdurmak, pozmagy ýa-da goramaklygy talap etmek, ulanyjylary garşylamak we makalalary teglemek ýaly umumy işleri awtomatlaşdyrmak üçin menýu düwmelerini goşmak ([[Wikipedia:Twinkle/Preferences|sazlama]])' 268710 wikitext text/x-wiki [[WP:Twinkle|Twinkle]]: wandalizm barada habar bermek, wandallary duýdurmak, pozmagy ýa-da goramaklygy talap etmek, ulanyjylary garşylamak we makalalary teglemek ýaly umumy işleri awtomatlaşdyrmak üçin menýu düwmelerini goşmak ([[Wikipedia:Twinkle/Preferences|sazlama]]) ceejrtzg588rpl4zjz9aaw6wlfvsd7t MediaWiki:Gadget-Twinkle.js 8 24474 268711 2026-04-27T16:41:09Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/** * +-------------------------------------------------------------------------+ * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes at [[WT:TW]] before editing. | * +-------------------------------------------------------------------------+ * * Imported from github [https://github.com/wikimedia-gadgets/twi...' 268711 javascript text/javascript /** * +-------------------------------------------------------------------------+ * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes at [[WT:TW]] before editing. | * +-------------------------------------------------------------------------+ * * Imported from github [https://github.com/wikimedia-gadgets/twinkle]. * All changes should be made in the repository, otherwise they will be lost. * * ---------- * * This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and * every Wikipedian in between. Visit [[WP:TW]] for more information. */ // <nowiki> /* global Morebits */ (function() { // Check if account is experienced enough to use Twinkle if (!Morebits.userIsInGroup('autoconfirmed') && !Morebits.userIsInGroup('confirmed')) { return; } const Twinkle = {}; window.Twinkle = Twinkle; // allow global access Twinkle.initCallbacks = []; /** * Adds a callback to execute when Twinkle has loaded. * * @param {Function} func * @param {string} [name] - name of module used to check if is disabled. * If name is not given, module is loaded unconditionally. */ Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) { Twinkle.initCallbacks.push({ func: func, name: name }); }; Twinkle.defaultConfig = {}; /** * This holds the default set of preferences used by Twinkle. * It is important that all new preferences added here, especially admin-only ones, are also added to * |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel. * For help on the actual preferences, see the comments in twinkleconfig.js. */ Twinkle.defaultConfig = { // General userTalkPageMode: 'tab', dialogLargeFont: false, disabledModules: [], disabledSysopModules: [], // ARV spiWatchReport: 'yes', // Block defaultToBlock64: false, defaultToPartialBlocks: false, blankTalkpageOnIndefBlock: false, // Rollback autoMenuAfterRollback: false, openTalkPage: [ 'agf', 'norm', 'vand' ], openTalkPageOnAutoRevert: false, rollbackInPlace: false, markRevertedPagesAsMinor: [ 'vand' ], watchRevertedPages: [ 'agf', 'norm', 'vand', 'torev' ], watchRevertedExpiry: '1 month', offerReasonOnNormalRevert: true, confirmOnRollback: false, confirmOnMobileRollback: true, showRollbackLinks: [ 'diff', 'others' ], // DI (twinkleimage) notifyUserOnDeli: true, deliWatchPage: '1 month', deliWatchUser: '1 month', // Protect watchRequestedPages: 'yes', watchPPTaggedPages: 'default', watchProtectedPages: 'default', // PROD watchProdPages: '1 month', markProdPagesAsPatrolled: false, prodReasonDefault: '', logProdPages: false, prodLogPageName: 'PROD log', // CSD speedySelectionStyle: 'buttonClick', watchSpeedyPages: [ 'g3', 'g5', 'g10', 'g11', 'g12' ], watchSpeedyExpiry: '1 month', markSpeedyPagesAsPatrolled: false, watchSpeedyUser: '1 month', // these next two should probably be identical by default welcomeUserOnSpeedyDeletionNotification: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ], notifyUserOnSpeedyDeletionNomination: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ], warnUserOnSpeedyDelete: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ], promptForSpeedyDeletionSummary: [], deleteTalkPageOnDelete: true, deleteRedirectsOnDelete: true, deleteSysopDefaultToDelete: false, speedyWindowHeight: 500, speedyWindowWidth: 800, logSpeedyNominations: false, speedyLogPageName: 'CSD log', noLogOnSpeedyNomination: [ 'u1' ], // Unlink unlinkNamespaces: [ '0', '10', '100', '118' ], // Warn defaultWarningGroup: '10', combinedSingletMenus: false, watchWarnings: '1 month', oldSelect: false, customWarningList: [], // XfD logXfdNominations: false, xfdLogPageName: 'XfD log', noLogOnXfdNomination: [], xfdWatchDiscussion: 'default', xfdWatchList: 'no', xfdWatchPage: '1 month', xfdWatchUser: '1 month', xfdWatchRelated: '1 month', markXfdPagesAsPatrolled: true, // Hidden preferences autolevelStaleDays: 3, // Huggle is 3, CBNG is 2 revertMaxRevisions: 50, // intentionally limited batchMax: 5000, batchChunks: 50, // Deprecated options, as a fallback for add-on scripts/modules summaryAd: ' ([[WP:TW|TW]])', deletionSummaryAd: ' ([[WP:TW|TW]])', protectionSummaryAd: ' ([[WP:TW|TW]])', // Tag groupByDefault: true, watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'], watchTaggedPages: '1 month', watchMergeDiscussions: '1 month', markTaggedPagesAsMinor: false, markTaggedPagesAsPatrolled: false, tagArticleSortOrder: 'cat', customTagList: [], customFileTagList: [], customRedirectTagList: [], // Welcome topWelcomes: false, watchWelcomes: '3 months', insertUsername: true, quickWelcomeMode: 'norm', quickWelcomeTemplate: 'welcome', customWelcomeList: [], customWelcomeSignature: true, // Talkback markTalkbackAsMinor: false, insertTalkbackSignature: true, // always sign talkback templates talkbackHeading: 'New message from ' + mw.config.get('wgUserName'), mailHeading: "You've got mail!" }; Twinkle.getPref = function twinkleGetPref(name) { if (typeof Twinkle.prefs === 'object' && Twinkle.prefs[name] !== undefined) { return Twinkle.prefs[name]; } // Old preferences format, used before twinkleoptions.js was a thing if (typeof window.TwinkleConfig === 'object' && window.TwinkleConfig[name] !== undefined) { return window.TwinkleConfig[name]; } if (typeof window.FriendlyConfig === 'object' && window.FriendlyConfig[name] !== undefined) { return window.FriendlyConfig[name]; } // Backwards compatibility code because we renamed confirmOnFluff to confirmOnRollback, and confirmOnMobileFluff to confirmOnMobileRollback if (name === 'confirmOnRollback' && typeof Twinkle.prefs === 'object' && Twinkle.prefs.confirmOnFluff !== undefined) { return Twinkle.prefs.confirmOnFluff; } else if (name === 'confirmOnMobileRollback' && typeof Twinkle.prefs === 'object' && Twinkle.prefs.confirmOnMobileFluff !== undefined) { return Twinkle.prefs.confirmOnMobileFluff; } return Twinkle.defaultConfig[name]; }; /** * Adds a portlet menu to one of the navigation areas on the page. * * @return {string} portletId */ Twinkle.addPortlet = function() { /** @type {string} id of the target navigation area (skin dependent, on vector either of "#left-navigation", "#right-navigation", or "#mw-panel") */ let navigation; /** @type {string} id of the portlet menu to create, preferably start with "p-". */ let id; /** @type {string} name of the portlet menu to create. Visibility depends on the class used. */ let text; /** @type {Node} the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. */ let nextnodeid; switch (mw.config.get('skin')) { case 'vector': case 'vector-2022': navigation = '#right-navigation'; id = 'p-twinkle'; text = 'TW'; // In order to get mw.util.addPortlet to generate a dropdown menu in vector and vector-2022, the nextnodeid must be p-cactions. Any other nextnodeid will generate a non-dropdown portlet instead. nextnodeid = 'p-cactions'; break; case 'timeless': navigation = '#page-tools .sidebar-inner'; id = 'p-twinkle'; text = 'Twinkle'; nextnodeid = 'p-userpagetools'; break; default: navigation = null; id = 'p-cactions'; } if (navigation === null) { return id; } // make sure navigation is a valid CSS selector const root = document.querySelector(navigation); if (!root) { return id; } // if we already created the portlet, return early. we don't want to create it again. const item = document.getElementById(id); if (item) { return id; } mw.util.addPortlet(id, text, '#' + nextnodeid); // The Twinkle dropdown menu has been added to the left of p-cactions, since that is the only spot that will create a dropdown menu. But we want it on the right. Move it to the right. if (mw.config.get('skin') === 'vector') { $('#p-twinkle').insertAfter('#p-cactions'); } else if (mw.config.get('skin') === 'vector-2022') { const $landmark = $('#right-navigation > .vector-page-tools-landmark'); $('#p-twinkle-dropdown').insertAfter($landmark); // .vector-page-tools-landmark is unstable and could change. If so, log it to console, to hopefully get someone's attention. if (!$landmark) { mw.log.warn('Unexpected change in DOM'); } } return id; }; /** * Builds a portlet menu if it doesn't exist yet, and adds a portlet link. This function runs at the top of every Twinkle module, ensuring that the first module to be loaded adds the portlet, and that every module can add a link to itself to the portlet. * * @param {string|Function} task Either a URL for the portlet link or a function to execute. */ Twinkle.addPortletLink = function(task, text, id, tooltip) { // Create a portlet to hold all the portlet links (if not created already). And get the portletId. const portletId = Twinkle.addPortlet(); // Create a portlet link and add it to the portlet. const link = mw.util.addPortletLink(portletId, typeof task === 'string' ? task : '#', text, id, tooltip); // Related to the hidden peer gadget that prevents jumpiness when the page first loads $('.client-js .skin-vector #p-cactions').css('margin-right', 'initial'); // Add a click listener for the portlet link if (typeof task === 'function') { $(link).on('click', (ev) => { task(); ev.preventDefault(); }); } // $.collapsibleTabs is a feature of Vector 2010 if ($.collapsibleTabs) { // Manually trigger a recalculation of what tabs to put where. This is to account for the space that the TW menu we just added is taking up. $.collapsibleTabs.handleResize(); } return link; }; /** * **************** General initialization code **************** */ // Retrieve the user's Twinkle preferences Morebits.wiki.getCachedPage(`User:${mw.config.get('wgUserName')}/twinkleoptions.js`) .then((optionsText) => { if (!optionsText) { // User has no options return; } // Twinkle options are basically a JSON object with some comments. Strip those: optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, ''); // First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line. if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) { optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, ''); } try { const options = JSON.parse(optionsText); if (options) { if (options.twinkle || options.friendly) { // Old preferences format Twinkle.prefs = $.extend(options.twinkle, options.friendly); } else { Twinkle.prefs = options; } // v2 established after unification of Twinkle/Friendly objects Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1; } } catch (e) { mw.notify('Could not parse your Twinkle preferences', {type: 'error'}); } }) .catch(() => { console.log('Could not load your Twinkle preferences, resorting to default preferences'); // eslint-disable-line no-console }) .always(() => { $(Twinkle.load); }); // Developers: you can import custom Twinkle modules here // For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter); Twinkle.load = function () { // Don't activate on special pages other than those listed here, so // that others load faster, especially the watchlist. let activeSpecialPageList = [ 'Block', 'Contributions', 'IPContributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block if (Morebits.userIsSysop) { activeSpecialPageList = activeSpecialPageList.concat([ 'DeletedContributions', 'Prefixindex' ]); } if (mw.config.get('wgNamespaceNumber') === -1 && !activeSpecialPageList.includes(mw.config.get('wgCanonicalSpecialPageName'))) { return; } // Prevent clickjacking if (window.top !== window.self) { return; } // Set custom Api-User-Agent header, for server-side logging purposes Morebits.wiki.Api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')'); Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules')); // Redefine addInitCallback so that any modules being loaded now on are directly // initialised rather than added to initCallbacks array Twinkle.addInitCallback = function(func, name) { if (!name || !Twinkle.disabledModules.includes(name)) { func(); } }; // Initialise modules that were saved in initCallbacks array Twinkle.initCallbacks.forEach((module) => { Twinkle.addInitCallback(module.func, module.name); }); // Increases text size in Twinkle dialogs, if so configured if (Twinkle.getPref('dialogLargeFont')) { mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' + '.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }'); } // Hide the lingering space if the TW menu is empty const isVector = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022'; if (isVector && Twinkle.getPref('portletType') === 'menu' && $('#p-twinkle').length === 0) { $('#p-cactions').css('margin-right', 'initial'); } // If using a skin with space for lots of modules, display a link to Twinkle Preferences const usingSkinWithDropDownMenu = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022' || mw.config.get('skin') === 'timeless'; if (usingSkinWithDropDownMenu) { Twinkle.addPortletLink(mw.util.getUrl('Wikipedia:Twinkle/Preferences'), 'Config', 'tw-config', 'Open Twinkle preferences page'); } }; /** * Twinkle-specific data shared by multiple modules * Likely customized per installation */ // Custom change tag(s) to be applied to all Twinkle actions, create at Special:Tags Twinkle.changeTags = 'twinkle'; // Available for actions that don't (yet) support tags // currently: FlaggedRevs and PageTriage Twinkle.summaryAd = ' ([[WP:TW|TW]])'; // Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to // ensure MOS:ORDER Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)'; /* Twinkle-specific utility functions shared by multiple modules */ /** * When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method gets a prefill, either from URL parameters (e.g. &vanarticle=Test) or from data previously stored using Twinkle.setPrefill() */ Twinkle.getPrefill = function (key) { Twinkle.prefill = Twinkle.prefill || {}; if (!Object.prototype.hasOwnProperty.call(Twinkle.prefill, key)) { Twinkle.prefill[key] = mw.util.getParamValue(key); } return Twinkle.prefill[key]; }; /** * When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method sets a prefill. This data will be lost if the page is refreshed, unless it is added to the URL as a parameter. */ Twinkle.setPrefill = function (key, value) { Twinkle.prefill = Twinkle.prefill || {}; Twinkle.prefill[key] = value; }; /* * Used in XFD and PROD */ Twinkle.makeFindSourcesDiv = function makeSourcesDiv(divID) { if (!$(divID).length) { return; } if (!Twinkle.findSources) { const parser = new Morebits.wiki.Preview($(divID)[0]); parser.beginRender('({{Find sources|' + Morebits.pageNameNorm + '}})', 'WP:AFD').then(() => { // Save for second-time around Twinkle.findSources = parser.previewbox.innerHTML; $(divID).removeClass('morebits-previewbox'); }); } else { $(divID).html(Twinkle.findSources); } }; /** * Used in batch, unlink, and deprod to sort pages by namespace, as * json formatversion=2 sorts by pageid instead (#1251) */ Twinkle.sortByNamespace = function(first, second) { return first.ns - second.ns || (first.title > second.title ? 1 : -1); }; /** * Used in batch listings to link to the page in question with > */ Twinkle.generateArrowLinks = function (checkbox) { const link = Morebits.htmlNode('a', ' >'); link.setAttribute('class', 'tw-arrowpage-link'); link.setAttribute('href', mw.util.getUrl(checkbox.value)); link.setAttribute('target', '_blank'); checkbox.nextElementSibling.append(link); }; /** * Used in deprod and unlink listings to link the page title */ Twinkle.generateBatchPageLinks = function (checkbox) { const $checkbox = $(checkbox); const link = Morebits.htmlNode('a', $checkbox.val()); link.setAttribute('class', 'tw-batchpage-link'); link.setAttribute('href', mw.util.getUrl($checkbox.val())); link.setAttribute('target', '_blank'); $checkbox.next().prepend([link, ' ']); }; /** * remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons */ Twinkle.removeMoveToCommonsTagsFromWikicode = ( wikicode ) => wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi, ''); }()); // </nowiki> 1pkgkbiooui9ypy5qd4gl4b4c41j2c9 MediaWiki:Gadget-twinkleblock.js 8 24475 268712 2026-04-27T16:43:58Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { const api = new mw.Api(); let relevantUserName, blockedUserName, blockWindow; const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces')); menuFormattedNamespaces[0] = '(Article)'; /* **************************************** *** twinkleblock.js: Block module **************************************** * Mode of invocation: Tab ("Block") * Active on: Any page with relevant user name (userspac...' 268712 javascript text/javascript // <nowiki> (function() { const api = new mw.Api(); let relevantUserName, blockedUserName, blockWindow; const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces')); menuFormattedNamespaces[0] = '(Article)'; /* **************************************** *** twinkleblock.js: Block module **************************************** * Mode of invocation: Tab ("Block") * Active on: Any page with relevant user name (userspace, contribs, etc.) */ Twinkle.block = function twinkleblock() { relevantUserName = mw.config.get('wgRelevantUserName'); // should show on Contributions or Block pages, anywhere there's a relevant user // Ignore ranges wider than the CIDR limit if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) { Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', 'Block relevant user'); } }; Twinkle.block.callback = function twinkleblockCallback() { if (relevantUserName === mw.config.get('wgUserName') && !confirm('You are about to block yourself! Are you sure you want to proceed?')) { return; } Twinkle.block.currentBlockInfo = undefined; Twinkle.block.field_block_options = {}; Twinkle.block.field_template_options = {}; blockWindow = new Morebits.SimpleWindow(650, 530); // need to be verbose about who we're blocking blockWindow.setTitle('Block or issue block template to ' + relevantUserName); blockWindow.setScriptName('Twinkle'); // Always added, hidden later if actual user not blocked blockWindow.addFooterLink('Unblock this user', 'Special:Unblock/' + relevantUserName); blockWindow.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates'); blockWindow.addFooterLink('Block policy', 'WP:BLOCK'); blockWindow.addFooterLink('Block prefs', 'WP:TW/PREF#block'); blockWindow.addFooterLink('Twinkle help', 'WP:TW/DOC#block'); blockWindow.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate); const actionfield = form.append({ type: 'field', label: 'Type of action' }); actionfield.append({ type: 'checkbox', name: 'actiontype', event: Twinkle.block.callback.change_action, list: [ { label: 'Block user', value: 'block', tooltip: 'Block the relevant user with the given options. If partial block is unchecked, this will be a sitewide block.', checked: true }, { label: 'Partial block', value: 'partial', tooltip: 'Enable partial blocks and partial block templates.', checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked }, { label: 'Add block template to user talk page', value: 'template', tooltip: 'If the blocking admin forgot to issue a block template, or you have just blocked the user without templating them, you can use this to issue the appropriate template. Check the partial block box for partial block templates.', // Disallow when viewing the block dialog on an IP range checked: !Morebits.ip.isRange(relevantUserName), disabled: Morebits.ip.isRange(relevantUserName) } ] }); /* Add option for IPv6 ranges smaller than /64 to upgrade to the 64 CIDR ([[WP:/64]]). This is one of the few places where we want wgRelevantUserName since this depends entirely on the original user. In theory, we shouldn't use Morebits.ip.get64 here since since we want to exclude functionally-equivalent /64s. That'd be: // if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) && // (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) { In practice, though, since functionally-equivalent ranges are (mis)treated as separate by MediaWiki's logging ([[phab:T146628]]), using Morebits.ip.get64 provides a modicum of relief in thise case. */ const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName')); if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) { const block64field = form.append({ type: 'field', label: 'Convert to /64 rangeblock', name: 'field_64' }); block64field.append({ type: 'div', style: 'margin-bottom: 0.5em', label: ['It\'s usually fine, if not better, to ', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl('WP:/64') + '">just block the /64</a>')[0], ' range (', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').'] }); block64field.append({ type: 'checkbox', name: 'block64', event: Twinkle.block.callback.change_block64, list: [{ checked: Twinkle.getPref('defaultToBlock64'), label: 'Block the /64 instead', value: 'block64', tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName') }] }); } form.append({ type: 'field', label: 'Preset', name: 'field_preset' }); form.append({ type: 'field', label: 'Template options', name: 'field_template_options' }); form.append({ type: 'field', label: 'Block options', name: 'field_block_options' }); form.append({ type: 'submit' }); const result = form.render(); blockWindow.setContent(result); blockWindow.display(); result.root = result; Twinkle.block.fetchUserInfo(() => { // Toggle initial partial state depending on prior block type, // will override the defaultToPartialBlocks pref if (blockedUserName === relevantUserName) { $(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === ''); } // clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called Twinkle.block.transformBlockPresets(); // init the controls after user and block info have been fetched const evt = document.createEvent('Event'); evt.initEvent('change', true, true); if (result.block64 && result.block64.checked) { // Calls the same change_action event once finished result.block64.dispatchEvent(evt); } else { result.actiontype[0].dispatchEvent(evt); } }); }; // Store fetched user data, only relevant if switching IPv6 to a /64 Twinkle.block.fetchedData = {}; // Processes the data from a query response, separated from // Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) { let blockinfo = data.query.blocks[0]; // Soft redirect to Special:Block if the user is multi-blocked (#2178) if (blockinfo && data.query.blocks.length > 1) { // Remove submission buttons. blockWindow.$dialog.find('.morebits-dialog-buttons').empty(); Morebits.Status.init(blockWindow.content.querySelector('form')); Morebits.Status.warn( `This target has ${data.query.blocks.length} active blocks`, `Multiblocks is not supported by Twinkle. Use [[Special:Block/${relevantUserName}]] instead.` ); return; } const userinfo = data.query.users[0]; // If an IP is blocked *and* rangeblocked, the above finds // whichever block is more recent, not necessarily correct. // Three seems... unlikely if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) { blockinfo = data.query.blocks[1]; } // Cache response, used when toggling /64 blocks Twinkle.block.fetchedData[userinfo.name] = data; Twinkle.block.isRegistered = !!userinfo.userid; if (Twinkle.block.isRegistered) { Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot'); } else { Twinkle.block.userIsBot = false; } if (blockinfo) { // handle frustrating system of inverted boolean values blockinfo.disabletalk = blockinfo.allowusertalk === undefined; blockinfo.hardblock = blockinfo.anononly === undefined; } // will undefine if no blocks present Twinkle.block.currentBlockInfo = blockinfo; blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user; // Toggle unblock link if not the user in question; always first const unblockLink = blockWindow.$dialog.find('.morebits-dialog-footerlinks a')[0]; if (blockedUserName !== relevantUserName) { unblockLink.hidden = true; unblockLink.nextSibling.hidden = true; // link+trailing bullet } else { unblockLink.hidden = false; unblockLink.nextSibling.hidden = false; // link+trailing bullet } // Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]]. // Basically, logevents doesn't treat functionally-equivalent ranges // as equivalent, meaning any functionally-equivalent IP range is // misinterpreted by the log throughout. Without logevents // redirecting (like Special:Block does) we would need a function to // parse ranges, which is a pain. IPUtils has the code, but it'd be a // lot of cruft for one purpose. Twinkle.block.hasBlockLog = !!data.query.logevents.length; Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents; // Used later to check if block status changed while filling out the form Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false; if (typeof fn === 'function') { return fn(); } }; Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) { const query = { format: 'json', action: 'query', list: 'blocks|users|logevents', letype: 'block', lelimit: 1, letitle: 'User:' + relevantUserName, bkprop: 'expiry|reason|flags|restrictions|range|user', ususers: relevantUserName }; // bkusers doesn't catch single IPs blocked as part of a range block if (mw.util.isIPAddress(relevantUserName, true)) { query.bkip = relevantUserName; } else { query.bkusers = relevantUserName; // groupmemberships only relevant for registered users query.usprop = 'groupmemberships'; } api.get(query).then((data) => { Twinkle.block.processUserInfo(data, fn); }, (msg) => { Morebits.Status.init($('div[name="currentblock"] span').last()[0]); Morebits.Status.warn('Error fetching user info', msg); }); }; Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) { Twinkle.block[$(fieldset).prop('name')] = {}; $(fieldset).serializeArray().forEach((el) => { // namespaces and pages for partial blocks are overwritten // here, but we're handling them elsewhere so that's fine Twinkle.block[$(fieldset).prop('name')][el.name] = el.value; }); }; Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) { const $form = $(e.target.form), $block64 = $form.find('[name=block64]'), dialog = $form.closest('.morebits-dialog')[0]; // Show/hide block64 button // Single IPv6, or IPv6 range smaller than a /64 const priorName = relevantUserName; if ($block64.is(':checked')) { relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName')); } else { relevantUserName = mw.config.get('wgRelevantUserName'); } // No templates for ranges, but if the original user is a single IP, offer the option // (done separately in Twinkle.block.callback.issue_template) const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName')); $form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange).prop('checked', !originalIsRange); // Refetch/reprocess user info then regenerate the main content const regenerateForm = function() { // Tweak titlebar text. In theory, we could save the dialog // at initialization and then use `.setTitle`, but in practice that swallows // the scriptName and requires `.display`ing, which jumps the // window. It's just a line of text, so this is fine. const titleBar = dialog.querySelector('.morebits-dialog-title').firstChild.nextSibling; titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName); // Tweak unblock link const unblockLink = dialog.querySelector('.morebits-dialog-footerlinks a'); unblockLink.href = unblockLink.href.replace(priorName, relevantUserName); unblockLink.title = unblockLink.title.replace(priorName, relevantUserName); // Correct partial state $form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks')); if (blockedUserName === relevantUserName) { $form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === ''); } // Set content appropriately Twinkle.block.callback.change_action(e); }; if (Twinkle.block.fetchedData[relevantUserName]) { Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm); } else { Twinkle.block.fetchUserInfo(regenerateForm); } }; Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) { let fieldPreset, fieldTemplateOptions, fieldBlockOptions; const $form = $(e.target.form); // Make ifs shorter const blockBox = $form.find('[name=actiontype][value=block]').is(':checked'); const templateBox = $form.find('[name=actiontype][value=template]').is(':checked'); const $partial = $form.find('[name=actiontype][value=partial]'); const partialBox = $partial.is(':checked'); let blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups; $partial.prop('disabled', !blockBox && !templateBox); // Add current block parameters as default preset const prior = { label: 'Prior block' }; if (blockedUserName === relevantUserName) { Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo; // value not a valid template selection, chosen below by setting templateName prior.list = [{ label: 'Prior block settings', value: 'prior', selected: true }]; // Arrays of objects are annoying to check if (!blockGroup.some((bg) => bg.label === prior.label)) { blockGroup.push(prior); } // Always ensure proper template exists/is selected when switching modes if (partialBox) { Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock'; } else { if (!Twinkle.block.isRegistered) { Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock'; } else { Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block'; } } } else { // But first remove any prior prior blockGroup = blockGroup.filter((bg) => bg.label !== prior.label); } // Can be in preset or template field, so the old one in the template // field will linger. No need to keep the old value around, so just // remove it; saves trouble when hiding/evaluating $form.find('[name=dstopic]').parent().remove(); Twinkle.block.callback.saveFieldset($('[name=field_block_options]')); Twinkle.block.callback.saveFieldset($('[name=field_template_options]')); if (blockBox) { fieldPreset = new Morebits.QuickForm.Element({ type: 'field', label: 'Preset', name: 'field_preset' }); fieldPreset.append({ type: 'select', name: 'preset', label: 'Choose a preset:', event: Twinkle.block.callback.change_preset, list: Twinkle.block.callback.filtered_block_groups(blockGroup) }); fieldBlockOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'Block options', name: 'field_block_options' }); fieldBlockOptions.append({ type: 'div', name: 'currentblock', label: ' ' }); fieldBlockOptions.append({ type: 'div', name: 'hasblocklog', label: ' ' }); fieldBlockOptions.append({ type: 'select', name: 'expiry_preset', label: 'Expiry:', event: Twinkle.block.callback.change_expiry, list: [ { label: 'custom', value: 'custom', selected: true }, { label: 'indefinite', value: 'infinity' }, { label: '3 hours', value: '3 hours' }, { label: '12 hours', value: '12 hours' }, { label: '24 hours', value: '24 hours' }, { label: '31 hours', value: '31 hours' }, { label: '36 hours', value: '36 hours' }, { label: '48 hours', value: '48 hours' }, { label: '60 hours', value: '60 hours' }, { label: '72 hours', value: '72 hours' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '3 months', value: '3 months' }, { label: '6 months', value: '6 months' }, { label: '1 year', value: '1 year' }, { label: '2 years', value: '2 years' }, { label: '3 years', value: '3 years' } ] }); fieldBlockOptions.append({ type: 'input', name: 'expiry', label: 'Custom expiry', tooltip: 'You can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).', value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry }); if (partialBox) { // Partial block fieldBlockOptions.append({ type: 'select', multiple: true, name: 'pagerestrictions', label: 'Specific pages to block from editing', value: '', tooltip: '10 page max.' }); const ns = fieldBlockOptions.append({ type: 'select', multiple: true, name: 'namespacerestrictions', label: 'Namespace blocks', value: '', tooltip: 'Block from editing these namespaces.' }); $.each(menuFormattedNamespaces, (number, name) => { // Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk) if (number >= 0 && number < 830) { ns.append({ type: 'option', label: name, value: number }); } }); } const blockoptions = [ { checked: Twinkle.block.field_block_options.nocreate, label: 'Block account creation', name: 'nocreate', value: '1' } ]; if (Twinkle.block.isRegistered && !mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) { blockoptions.push({ checked: Twinkle.block.field_block_options.noemail, label: 'Block user from sending email', name: 'noemail', value: '1' }); } blockoptions.push({ checked: Twinkle.block.field_block_options.disabletalk, label: 'Prevent this user from editing their own talk page while blocked', name: 'disabletalk', value: '1', tooltip: partialBox ? 'If issuing a partial block, this MUST remain unchecked unless you are also preventing them from editing User talk space' : '' }); if (Twinkle.block.isRegistered) { blockoptions.push({ checked: Twinkle.block.field_block_options.autoblock, label: 'Autoblock any IP addresses used (hardblock)', name: 'autoblock', value: '1' }); } else { blockoptions.push({ checked: Twinkle.block.field_block_options.hardblock, label: 'Block logged-in users from using this IP address (hardblock)', name: 'hardblock', value: '1' }); } blockoptions.push({ checked: Twinkle.block.field_block_options.watchuser, label: 'Watch user and user talk pages', name: 'watchuser', value: '1' }); fieldBlockOptions.append({ type: 'checkbox', name: 'blockoptions', list: blockoptions }); fieldBlockOptions.append({ type: 'textarea', label: 'Reason (for block log):', name: 'reason', tooltip: 'Consider adding helpful details to the default message.', value: Twinkle.block.field_block_options.reason }); fieldBlockOptions.append({ type: 'div', name: 'filerlog_label', label: 'See also:', style: 'display:inline-block;font-style:normal !important', tooltip: 'Insert a "see also" message to indicate whether the filter log, deleted contributions or related temporary accounts played a role in the decision to block.' }); fieldBlockOptions.append({ type: 'checkbox', name: 'filter_see_also', event: Twinkle.block.callback.toggle_see_alsos, style: 'display:inline-block; margin-right:5px', list: [ { label: 'Filter log', checked: false, value: 'filter log' } ] }); fieldBlockOptions.append({ type: 'checkbox', name: 'deleted_see_also', event: Twinkle.block.callback.toggle_see_alsos, style: 'display:inline-block; margin-right:5px', list: [ { label: 'Deleted contribs', checked: false, value: 'deleted contribs' } ] }); if (mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) { fieldBlockOptions.append({ type: 'checkbox', name: 'related_see_also', event: Twinkle.block.callback.toggle_see_alsos, style: 'display:inline-block', list: [ { label: 'Related temporary accounts', checked: false, value: 'related temporary accounts' } ] }); } // Yet-another-logevents-doesn't-handle-ranges-well if (blockedUserName === relevantUserName) { fieldBlockOptions.append({ type: 'hidden', name: 'reblock', value: '1' }); } } // grab discretionary sanctions list from en-wiki Twinkle.block.dsinfo = Morebits.wiki.getCachedJson('Template:Ds/topics.json'); Twinkle.block.dsinfo.then((dsinfo) => { const $select = $('[name="dstopic"]'); const $options = $.map(dsinfo, (value, key) => $('<option>').val(value.code).text(key).prop('label', key)); $select.append($options); }); // DS selection visible in either the template field set or preset, // joint settings saved here const dsSelectSettings = { type: 'select', name: 'dstopic', label: 'DS topic', value: '', tooltip: 'If selected, it will inform the template and may be added to the blocking message', event: Twinkle.block.callback.toggle_ds_reason }; if (templateBox) { fieldTemplateOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'Template options', name: 'field_template_options' }); fieldTemplateOptions.append({ type: 'select', name: 'template', label: 'Choose talk page template:', event: Twinkle.block.callback.change_template, list: Twinkle.block.callback.filtered_block_groups(blockGroup, true), value: Twinkle.block.field_template_options.template }); // Only visible for aeblock and aepblock, toggled in change_template fieldTemplateOptions.append(dsSelectSettings); fieldTemplateOptions.append({ type: 'input', name: 'article', label: 'Linked page', value: '', tooltip: 'A page can be linked within the notice, perhaps if it was the primary target of disruption. Leave empty for no page to be linked.' }); // Only visible if partial and not blocking fieldTemplateOptions.append({ type: 'input', name: 'area', label: 'Area blocked from', value: '', tooltip: 'Optional explanation of the pages or namespaces the user was blocked from editing.' }); if (!blockBox) { fieldTemplateOptions.append({ type: 'input', name: 'template_expiry', label: 'Period of blocking:', value: '', tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc...' }); } fieldTemplateOptions.append({ type: 'input', name: 'block_reason', label: '"You have been blocked for ..."', tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.', value: Twinkle.block.field_template_options.block_reason }); if (blockBox) { fieldTemplateOptions.append({ type: 'checkbox', name: 'blank_duration', list: [ { label: 'Do not include expiry in template', checked: Twinkle.block.field_template_options.blank_duration, tooltip: 'Instead of including the duration, make the block template read "You have been blocked temporarily..."' } ] }); } else { fieldTemplateOptions.append({ type: 'checkbox', list: [ { label: 'Talk page access disabled', name: 'notalk', checked: Twinkle.block.field_template_options.notalk, tooltip: 'Make the block template state that the user\'s talk page access has been removed' }, { label: 'User blocked from sending email', name: 'noemail_template', checked: Twinkle.block.field_template_options.noemail_template, tooltip: 'If the area is not provided, make the block template state that the user\'s email access has been removed' }, { label: 'User blocked from creating accounts', name: 'nocreate_template', checked: Twinkle.block.field_template_options.nocreate_template, tooltip: 'If the area is not provided, make the block template state that the user\'s ability to create accounts has been removed' } ] }); } const $previewlink = $('<a id="twinkleblock-preview-link">Preview</a>'); $previewlink.off('click').on('click', () => { Twinkle.block.callback.preview($form[0]); }); $previewlink.css({cursor: 'pointer'}); fieldTemplateOptions.append({ type: 'div', id: 'blockpreview', label: [ $previewlink[0] ] }); fieldTemplateOptions.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' }); } else if (fieldPreset) { // Only visible for arbitration enforcement, toggled in change_preset fieldPreset.append(dsSelectSettings); } let oldfield; if (fieldPreset) { oldfield = $form.find('fieldset[name="field_preset"]')[0]; oldfield.parentNode.replaceChild(fieldPreset.render(), oldfield); } else { $form.find('fieldset[name="field_preset"]').hide(); } if (fieldBlockOptions) { oldfield = $form.find('fieldset[name="field_block_options"]')[0]; oldfield.parentNode.replaceChild(fieldBlockOptions.render(), oldfield); $form.find('fieldset[name="field_64"]').show(); $form.find('[name=pagerestrictions]').select2({ theme: 'default select2-morebits', width: '100%', placeholder: 'Select pages to block user from', language: { errorLoading: function() { return 'Incomplete or invalid search term'; } }, maximumSelectionLength: 10, // Software limitation [[phab:T202776]] minimumInputLength: 1, // prevent ajax call when empty ajax: { url: mw.util.wikiScript('api'), dataType: 'json', delay: 100, data: function(params) { const title = mw.Title.newFromText(params.term); if (!title) { return; } return { action: 'query', format: 'json', list: 'allpages', apfrom: title.title, apnamespace: title.namespace, aplimit: '10' }; }, processResults: function(data) { return { results: data.query.allpages.map((page) => { const title = mw.Title.newFromText(page.title, page.ns).toText(); return { id: title, text: title }; }) }; } }, templateSelection: function(choice) { return $('<a>').text(choice.text).attr({ href: mw.util.getUrl(choice.text), target: '_blank' }); } }); $form.find('[name=namespacerestrictions]').select2({ theme: 'default select2-morebits', width: '100%', matcher: Morebits.select2.matchers.wordBeginning, language: { searching: Morebits.select2.queryInterceptor }, templateResult: Morebits.select2.highlightSearchMatches, placeholder: 'Select namespaces to block user from' }); mw.util.addCSS( // Reduce padding '.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' + // Adjust font size '.select2-container .select2-dropdown .select2-results { font-size: 13px; }' + '.select2-container .selection .select2-selection__rendered { font-size: 13px; }' + // Remove black border '.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' + // Make the tiny cross larger '.select2-selection__choice__remove { font-size: 130%; }' ); } else { $form.find('fieldset[name="field_block_options"]').hide(); $form.find('fieldset[name="field_64"]').hide(); // Clear select2 options $form.find('[name=pagerestrictions]').val(null).trigger('change'); $form.find('[name=namespacerestrictions]').val(null).trigger('change'); } if (fieldTemplateOptions) { oldfield = $form.find('fieldset[name="field_template_options"]')[0]; oldfield.parentNode.replaceChild(fieldTemplateOptions.render(), oldfield); e.target.form.root.previewer = new Morebits.wiki.Preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]); } else { $form.find('fieldset[name="field_template_options"]').hide(); } // Any block, including ranges if (Twinkle.block.currentBlockInfo) { // false for an ip covered by a range or a smaller range within a larger range; // true for a user, single ip block, or the exact range for a range block const sameUser = blockedUserName === relevantUserName; Morebits.Status.init($('div[name="currentblock"] span').last()[0]); let statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide'); // Range blocked if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) { if (sameUser) { statusStr += ' as a rangeblock'; } else { statusStr += ' within a' + (Morebits.ip.get64(relevantUserName) === blockedUserName ? ' /64' : '') + ' rangeblock'; // Link to the full range const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)')); statusStr += ' (' + $rangeblockloglink.html() + ')'; } } if (Twinkle.block.currentBlockInfo.expiry === 'infinity') { statusStr += ' (indefinite)'; } else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) { statusStr += ' (expires ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')'; } let infoStr = 'This form will'; if (sameUser) { infoStr += ' change that block'; if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) { infoStr += ', converting it to a partial block'; } else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) { infoStr += ', converting it to a sitewide block'; } infoStr += '.'; } else { infoStr += ' add an additional ' + (partialBox ? 'partial ' : '') + 'block.'; } Morebits.Status.warn(statusStr, infoStr); // Default to the current block conditions on intial form generation Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo); } // This is where T146628 really comes into play: a rangeblock will // only return the correct block log if wgRelevantUserName is the // exact range, not merely a funtional equivalent if (Twinkle.block.hasBlockLog) { const $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">block log</a>)')); if (!Twinkle.block.currentBlockInfo) { const lastBlockAction = Twinkle.block.blockLog[0]; if (lastBlockAction.action === 'unblock') { $blockloglink.append(' (unblocked ' + new Morebits.Date(lastBlockAction.timestamp).calendar('utc') + ')'); } else { // block or reblock $blockloglink.append(' (' + lastBlockAction.params.duration + ', expired ' + new Morebits.Date(lastBlockAction.params.expiry).calendar('utc') + ')'); } } Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]); Morebits.Status.warn(Twinkle.block.currentBlockInfo ? 'Previous blocks' : 'This ' + (Morebits.ip.isRange(relevantUserName) ? 'range' : 'user') + ' has been blocked in the past', $blockloglink[0]); } // Make sure all the fields are correct based on initial defaults if (blockBox) { Twinkle.block.callback.change_preset(e); } else if (templateBox) { Twinkle.block.callback.change_template(e); } }; /* * Keep alphabetized by key name, Twinkle.block.blockGroups establishes * the order they will appear in the interface * * Block preset format, all keys accept only 'true' (omit for false) except where noted: * <title of block template> : { * autoblock: <autoblock any IP addresses used (for registered users only)> * disabletalk: <disable user from editing their own talk page while blocked> * expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc> * forIPsOnly: <show block option in the interface only if the relevant user is an IP> * forTempAccountsOnly: <show block option in the interface only if the relevant user is a temporary account> * forRegisteredOnly: <show block option in the interface only if the relevant user is a temporary account or regular account> * label: <string - label for the option of the dropdown in the interface (keep brief)> * noemail: prevent the user from sending email through Special:Emailuser * pageParam: <set if the associated block template accepts a page parameter> * prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access> * nocreate: <block account creation from the user's IP (for unregistered users only)> * nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters> * reason: <string - block rationale, as would appear in the block log, * and the edit summary for when adding block template, unless 'summary' is set> * reasonParam: <set if the associated block template accepts a reason parameter> * sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether> * summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used> * suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages> * templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list. * (e.g. use another template but with different block options)> * useInitialOptions: <when preset is chosen, only change given block options, leave others as they were> * * WARNING: 'anononly' and 'allowusertalk' are enabled by default. * To disable, set 'hardblock' and 'disabletalk', respectively */ Twinkle.block.blockPresetsInfo = { anonblock: { expiry: '31 hours', forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{anonblock}}', sig: '~~~~' }, 'anonblock - school': { expiry: '36 hours', forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{anonblock}} <!-- Likely a school based on behavioral evidence -->', templateName: 'anonblock', sig: '~~~~' }, 'blocked proxy': { expiry: '1 year', forIPsOnly: true, nocreate: true, nonstandard: true, hardblock: true, reason: '{{blocked proxy}}', sig: null }, 'CheckUser block': { expiry: '1 week', forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{CheckUser block}}', sig: '~~~~' }, 'checkuserblock-account': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, nonstandard: true, reason: '{{checkuserblock-account}}', sig: '~~~~' }, 'checkuserblock-wide': { forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{checkuserblock-wide}}', sig: '~~~~' }, colocationwebhost: { expiry: '1 year', forIPsOnly: true, nonstandard: true, reason: '{{colocationwebhost}}', sig: null }, oversightblock: { autoblock: true, expiry: 'infinity', nocreate: true, nonstandard: true, reason: '{{OversightBlock}}', sig: '~~~~' }, 'school block': { forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{school block}}', sig: '~~~~' }, spamblacklistblock: { forIPsOnly: true, expiry: '1 month', disabletalk: true, nocreate: true, reason: '{{spamblacklistblock}} <!-- editor only attempts to add blacklisted links, see [[Special:Log/spamblacklist]] -->' }, rangeblock: { reason: '{{rangeblock}}', nocreate: true, nonstandard: true, forIPsOnly: true, sig: '~~~~' }, tor: { expiry: '1 year', forIPsOnly: true, nonstandard: true, reason: '{{Tor}}', sig: null }, webhostblock: { expiry: '1 year', forIPsOnly: true, nonstandard: true, reason: '{{webhostblock}}', sig: null }, // uw-prefixed 'uw-3block': { autoblock: true, expiry: '24 hours', nocreate: true, pageParam: true, reason: 'Violation of the [[WP:Three-revert rule|three-revert rule]]', summary: 'You have been blocked from editing for violation of the [[WP:3RR|three-revert rule]]' }, 'uw-ablock': { autoblock: true, expiry: '31 hours', forIPsOnly: true, nocreate: true, pageParam: true, reasonParam: true, summary: 'Your IP address has been blocked from editing', suppressArticleInSummary: true }, 'uw-adblock': { autoblock: true, nocreate: true, pageParam: true, reason: 'Using Wikipedia for [[WP:Spam|spam]] or [[WP:NOTADVERTISING|advertising]] purposes', summary: 'You have been blocked from editing for [[WP:SOAP|advertising or self-promotion]]' }, 'uw-aeblock': { autoblock: true, nocreate: true, pageParam: true, reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]', reasonParam: true, summary: 'You have been blocked from editing for violating an [[WP:Arbitration|arbitration decision]]' }, 'uw-bioblock': { autoblock: true, nocreate: true, pageParam: true, reason: 'Violations of the [[WP:Biographies of living persons|biographies of living persons]] policy', summary: 'You have been blocked from editing for violations of Wikipedia\'s [[WP:BLP|biographies of living persons policy]]' }, 'uw-block': { autoblock: true, expiry: '24 hours', forRegisteredOnly: true, nocreate: true, pageParam: true, reasonParam: true, summary: 'You have been blocked from editing', suppressArticleInSummary: true }, 'uw-blockindef': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: true, reasonParam: true, summary: 'You have been indefinitely blocked from editing', suppressArticleInSummary: true }, 'uw-blocknotalk': { autoblock: true, disabletalk: true, nocreate: true, pageParam: true, reasonParam: true, summary: 'You have been blocked from editing and your user talk page access has been disabled', suppressArticleInSummary: true }, 'uw-botblock': { forRegisteredOnly: true, pageParam: true, reason: 'Running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]', summary: 'You have been blocked from editing because it appears you are running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]' }, 'uw-botublock': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-botublock}} <!-- Username implies a bot, soft block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] indicates this is a [[WP:BOT|bot]] account, which is currently not approved' }, 'uw-botuhblock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: '{{uw-botuhblock}} <!-- Username implies a bot, hard block -->', summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]]' }, 'uw-causeblock': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-causeblock}} <!-- Username represents a non-profit, soft block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website' }, 'uw-compblock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: 'Compromised account', summary: 'You have been indefinitely blocked from editing because it is believed that your [[WP:SECURE|account has been compromised]]' }, 'uw-copyrightblock': { autoblock: true, expiry: 'infinity', nocreate: true, pageParam: true, reason: '[[WP:Copyright violations|Copyright violations]]', summary: 'You have been blocked from editing for continued [[WP:COPYVIO|copyright infringement]]' }, 'uw-dblock': { autoblock: true, nocreate: true, reason: 'Persistent removal of content', pageParam: true, summary: 'You have been blocked from editing for continued [[WP:VAND|removal of material]]' }, 'uw-disruptblock': { autoblock: true, nocreate: true, reason: '[[WP:Disruptive editing|Disruptive editing]]', summary: 'You have been blocked from editing for [[WP:DE|disruptive editing]]' }, 'uw-efblock': { autoblock: true, nocreate: true, reason: 'Repeatedly triggering the [[WP:Edit filter|Edit filter]]', summary: 'You have been blocked from editing for disruptive edits that repeatedly triggered the [[WP:EF|edit filter]]' }, 'uw-ewblock': { autoblock: true, expiry: '24 hours', nocreate: true, pageParam: true, reason: '[[WP:Edit warring|Edit warring]]', summary: 'You have been blocked from editing to prevent further [[WP:DE|disruption]] caused by your engagement in an [[WP:EW|edit war]]' }, 'uw-hblock': { autoblock: true, nocreate: true, pageParam: true, reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]', summary: 'You have been blocked from editing for attempting to [[WP:HARASS|harass]] other users' }, 'uw-ipevadeblock': { forIPsOnly: true, expiry: '1 week', nocreate: true, reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]', summary: 'Your IP address has been blocked from editing because it has been used to [[WP:EVADE|evade a previous block]]' }, 'uw-lblock': { autoblock: true, expiry: 'infinity', nocreate: true, reason: 'Making [[WP:No legal threats|legal threats]]', summary: 'You have been blocked from editing for making [[WP:NLT|legal threats or taking legal action]]' }, 'uw-nothereblock': { autoblock: true, expiry: 'infinity', nocreate: true, reason: 'Clearly [[WP:NOTHERE|not here to build an encyclopedia]]', forRegisteredOnly: true, summary: 'You have been indefinitely blocked from editing because it appears that you are not here to [[WP:NOTHERE|build an encyclopedia]]' }, 'uw-npblock': { autoblock: true, nocreate: true, pageParam: true, reason: 'Creating [[WP:Patent nonsense|patent nonsense]] or other inappropriate pages', summary: 'You have been blocked from editing for creating [[WP:PN|nonsense pages]]' }, 'uw-pablock': { autoblock: true, expiry: '31 hours', nocreate: true, reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]', summary: 'You have been blocked from editing for making [[WP:NPA|personal attacks]] toward other users' }, 'uw-sblock': { autoblock: true, nocreate: true, reason: 'Using Wikipedia for [[WP:SPAM|spam]] purposes', summary: 'You have been blocked from editing for using Wikipedia for [[WP:SPAM|spam]] purposes' }, 'uw-soablock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: true, reason: '[[WP:Spam|Spam]] / [[WP:NOTADVERTISING|advertising]]-only account', summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam, advertising, or promotion]]' }, 'uw-socialmediablock': { autoblock: true, nocreate: true, pageParam: true, reason: 'Using Wikipedia as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]', summary: 'You have been blocked from editing for using user and/or article pages as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]' }, 'uw-sockblock': { autoblock: true, forRegisteredOnly: true, nocreate: true, reason: 'Abusing [[WP:Sock puppetry|multiple accounts]]', summary: 'You have been blocked from editing for abusing [[WP:SOCK|multiple accounts]]' }, 'uw-softerblock': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-softerblock}} <!-- Promotional username, soft block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website' }, 'uw-spamublock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: '{{uw-spamublock}} <!-- Promotional username, promotional edits -->', summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam or advertising]] and your username is a violation of the [[WP:U|username policy]]' }, 'uw-spoablock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: '[[WP:SOCK|Sock puppetry]]', summary: 'This account has been blocked as a [[WP:SOCK|sock puppet]] created to violate Wikipedia policy' }, 'uw-talkrevoked': { disabletalk: true, reason: 'Revoking talk page access: inappropriate use of user talk page while blocked', prependReason: true, summary: 'Your user talk page access has been disabled', useInitialOptions: true }, 'uw-tempevadeblock': { autoblock: true, expiry: 'infinity', forTempAccountsOnly: true, nocreate: true, reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]', summary: 'Your temporary account has been blocked from editing because it has been used to [[WP:EVADE|evade a previous block]]' }, 'uw-ublock': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-ublock}} <!-- Username violation, soft block -->', reasonParam: true, summary: 'You have been indefinitely blocked from editing because your username is a violation of the [[WP:U|username policy]]' }, 'uw-ublock-double': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-ublock-double}} <!-- Username closely resembles another user, soft block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] is too similar to the username of another Wikipedia user' }, 'uw-ucblock': { autoblock: true, expiry: '31 hours', nocreate: true, pageParam: true, reason: 'Persistent addition of [[WP:INTREF|unsourced content]]', summary: 'You have been blocked from editing for persistent addition of [[WP:INTREF|unsourced content]]' }, 'uw-uhblock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: '{{uw-uhblock}} <!-- Username violation, hard block -->', reasonParam: true, summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]]' }, 'uw-ublock-wellknown': { expiry: 'infinity', forRegisteredOnly: true, reason: '{{uw-ublock-wellknown}} <!-- Username represents a well-known person, soft block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] matches the name of a well-known living individual' }, 'uw-uhblock-double': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, reason: '{{uw-uhblock-double}} <!-- Attempted impersonation of another user, hard block -->', summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] appears to impersonate another established Wikipedia user' }, 'uw-upeblock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: true, reason: '[[WP:PAID|Undisclosed paid editing]] in violation of the WMF [[WP:TOU|Terms of Use]]', summary: 'You have been indefinitely blocked from editing because your account is being used in violation of [[WP:PAID|Wikipedia policy on undisclosed paid advocacy]]' }, 'uw-vaublock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: true, reason: '{{uw-vaublock}} <!-- Username violation, vandalism-only account -->', summary: 'You have been indefinitely blocked from editing because your account is being [[WP:DISRUPTONLY|used only for vandalism]] and your username is a blatant violation of the [[WP:U|username policy]]' }, 'uw-vblock': { autoblock: true, expiry: '31 hours', nocreate: true, pageParam: true, reason: '[[WP:Vandalism|Vandalism]]', summary: 'You have been blocked from editing to prevent further [[WP:VAND|vandalism]]' }, 'uw-voablock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: true, reason: '[[WP:DISRUPTONLY|Vandalism-only account]]', summary: 'You have been indefinitely blocked from editing because your account is being [[WP:DISRUPTONLY|used only for vandalism]]' }, 'zombie proxy': { expiry: '1 month', forIPsOnly: true, nocreate: true, nonstandard: true, reason: '{{zombie proxy}}', sig: null }, // Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial 'uw-acpblock': { autoblock: true, expiry: '48 hours', nocreate: true, pageParam: false, reasonParam: true, reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]', summary: 'You have been [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]' }, 'uw-acpblockindef': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: true, pageParam: false, reasonParam: true, reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]', summary: 'You have been indefinitely [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]' }, 'uw-aepblock': { autoblock: true, nocreate: false, pageParam: false, reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]', reasonParam: true, summary: 'You have been [[WP:PB|partially blocked]] from editing for violating an [[WP:Arbitration|arbitration decision]]' }, 'uw-epblock': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: false, noemail: true, pageParam: false, reasonParam: true, reason: 'Email [[WP:Harassment|harassment]]', summary: 'You have been [[WP:PB|blocked from emailing]] other editors for [[WP:Harassment|harassment]]' }, 'uw-ewpblock': { autoblock: true, expiry: '24 hours', nocreate: false, pageParam: false, reasonParam: true, reason: '[[WP:Edit warring|Edit warring]]', summary: 'You have been [[WP:PB|partially blocked]] from editing certain areas of the encyclopedia to prevent further [[WP:DE|disruption]] due to [[WP:EW|edit warring]]' }, 'uw-pblock': { autoblock: true, expiry: '24 hours', nocreate: false, pageParam: false, reasonParam: true, summary: 'You have been [[WP:PB|partially blocked]] from certain areas of the encyclopedia' }, 'uw-pblockindef': { autoblock: true, expiry: 'infinity', forRegisteredOnly: true, nocreate: false, pageParam: false, reasonParam: true, summary: 'You have been indefinitely [[WP:PB|partially blocked]] from certain areas of the encyclopedia' } }; Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() { // supply sensible defaults $.each(Twinkle.block.blockPresetsInfo, (preset, settings) => { settings.summary = settings.summary || settings.reason; settings.sig = settings.sig !== undefined ? settings.sig : 'yes'; settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry); if (!Twinkle.block.isRegistered && settings.indefinite) { settings.expiry = '31 hours'; } else { settings.expiry = settings.expiry || '31 hours'; } Twinkle.block.blockPresetsInfo[preset] = settings; }); }; // These are the groups of presets and defines the order in which they appear. For each list item: // label: <string, the description that will be visible in the dropdown> // value: <string, the key of a preset in blockPresetsInfo> Twinkle.block.blockGroups = [ { label: 'Common block reasons', list: [ { label: 'Anonblock', value: 'anonblock' }, { label: 'Anonblock – likely a school', value: 'anonblock - school' }, { label: 'School block', value: 'school block' }, { label: 'Generic block (custom reason)', value: 'uw-block' }, // ends up being default for registered users { label: 'Generic block (custom reason) – IP', value: 'uw-ablock', selected: true }, // set only when blocking IP { label: 'Generic block (custom reason) – indefinite', value: 'uw-blockindef' }, { label: 'Disruptive editing', value: 'uw-disruptblock' }, { label: 'Inappropriate use of user talk page while blocked', value: 'uw-talkrevoked' }, { label: 'Not here to build an encyclopedia', value: 'uw-nothereblock' }, { label: 'Unsourced content', value: 'uw-ucblock' }, { label: 'Vandalism', value: 'uw-vblock' }, { label: 'Vandalism-only account', value: 'uw-voablock' } ] }, { label: 'Extended reasons', list: [ { label: 'Advertising', value: 'uw-adblock' }, { label: 'Arbitration enforcement', value: 'uw-aeblock' }, { label: 'Block evasion – IP', value: 'uw-ipevadeblock' }, { label: 'Block evasion – temporary account', value: 'uw-tempevadeblock' }, { label: 'BLP violations', value: 'uw-bioblock' }, { label: 'Copyright violations', value: 'uw-copyrightblock' }, { label: 'Creating nonsense pages', value: 'uw-npblock' }, { label: 'Edit filter-related', value: 'uw-efblock' }, { label: 'Edit warring', value: 'uw-ewblock' }, { label: 'Generic block with talk page access revoked', value: 'uw-blocknotalk' }, { label: 'Harassment', value: 'uw-hblock' }, { label: 'Legal threats', value: 'uw-lblock' }, { label: 'Personal attacks or harassment', value: 'uw-pablock' }, { label: 'Possible compromised account', value: 'uw-compblock' }, { label: 'Removal of content', value: 'uw-dblock' }, { label: 'Sock puppetry (master)', value: 'uw-sockblock' }, { label: 'Sock puppetry (puppet)', value: 'uw-spoablock' }, { label: 'Social networking', value: 'uw-socialmediablock' }, { label: 'Spam', value: 'uw-sblock' }, { label: 'Spam/advertising-only account', value: 'uw-soablock' }, { label: 'Unapproved bot', value: 'uw-botblock' }, { label: 'Undisclosed paid editing', value: 'uw-upeblock' }, { label: 'Violating the three-revert rule', value: 'uw-3block' } ] }, { label: 'Username violations', list: [ { label: 'Bot username, soft block', value: 'uw-botublock' }, { label: 'Bot username, hard block', value: 'uw-botuhblock' }, { label: 'Promotional username, hard block', value: 'uw-spamublock' }, { label: 'Promotional username, soft block', value: 'uw-softerblock' }, { label: 'Similar username, soft block', value: 'uw-ublock-double' }, { label: 'Username violation, soft block', value: 'uw-ublock' }, { label: 'Username violation, hard block', value: 'uw-uhblock' }, { label: 'Username impersonation, hard block', value: 'uw-uhblock-double' }, { label: 'Username represents a well-known person, soft block', value: 'uw-ublock-wellknown' }, { label: 'Username represents a non-profit, soft block', value: 'uw-causeblock' }, { label: 'Username violation, vandalism-only account', value: 'uw-vaublock' } ] }, { label: 'Templated reasons', list: [ { label: 'Blocked proxy', value: 'blocked proxy' }, { label: 'CheckUser block', value: 'CheckUser block' }, { label: 'CheckUser block – account', value: 'checkuserblock-account' }, { label: 'CheckUser block – wide', value: 'checkuserblock-wide' }, { label: 'Colocation webhost', value: 'colocationwebhost' }, { label: 'Oversight block', value: 'oversightblock' }, { label: 'Rangeblock', value: 'rangeblock' }, // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups { label: 'Spam blacklist block', value: 'spamblacklistblock' }, { label: 'Tor', value: 'tor' }, { label: 'Webhost block', value: 'webhostblock' }, { label: 'Zombie proxy', value: 'zombie proxy' } ] } ]; Twinkle.block.blockGroupsPartial = [ { label: 'Common partial block reasons', list: [ { label: 'Generic partial block (custom reason)', value: 'uw-pblock', selected: true }, { label: 'Generic partial block (custom reason) – indefinite', value: 'uw-pblockindef' }, { label: 'Edit warring', value: 'uw-ewpblock' } ] }, { label: 'Extended partial block reasons', list: [ { label: 'Arbitration enforcement', value: 'uw-aepblock' }, { label: 'Email harassment', value: 'uw-epblock' }, { label: 'Misusing multiple accounts', value: 'uw-acpblock' }, { label: 'Misusing multiple accounts – indefinite', value: 'uw-acpblockindef' } ] } ]; Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, showTemplate) { return $.map(group, (blockGroup) => { const list = $.map(blockGroup.list, (blockPreset) => { switch (blockPreset.value) { case 'uw-talkrevoked': if (blockedUserName !== relevantUserName) { return; } break; case 'rangeblock': if (!Morebits.ip.isRange(relevantUserName)) { return; } blockPreset.selected = !Morebits.ip.get64(relevantUserName); break; case 'CheckUser block': case 'checkuserblock-account': case 'checkuserblock-wide': if (!Morebits.userIsInGroup('checkuser')) { return; } break; case 'oversightblock': if (!Morebits.userIsInGroup('suppress')) { return; } break; default: break; } const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value]; let allowedUserType; // for regular users and temporary accounts if (blockSettings.forRegisteredOnly) { allowedUserType = Twinkle.block.isRegistered; // for temporary accounts } else if (blockSettings.forTempAccountsOnly) { allowedUserType = mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName')); // for IPs } else if (blockSettings.forIPsOnly) { allowedUserType = !Twinkle.block.isRegistered; } else { allowedUserType = true; } if (!(blockSettings.templateName && showTemplate) && allowedUserType) { const templateName = blockSettings.templateName || blockPreset.value; return { label: (showTemplate ? '{{' + templateName + '}}: ' : '') + blockPreset.label, value: blockPreset.value, data: [{ name: 'template-name', value: templateName }], selected: !!blockPreset.selected, disabled: !!blockPreset.disabled }; } }); if (list.length) { return { label: blockGroup.label, list: list }; } }); }; Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) { const form = e.target.form, key = form.preset.value; if (!key) { return; } Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]); if (form.template) { form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key; Twinkle.block.callback.change_template(e); } else { Morebits.QuickForm.setElementVisibility(form.dstopic.parentNode, key === 'uw-aeblock' || key === 'uw-aepblock'); } }; Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) { const expiry = e.target.form.expiry; if (e.target.value === 'custom') { Morebits.QuickForm.setElementVisibility(expiry.parentNode, true); } else { Morebits.QuickForm.setElementVisibility(expiry.parentNode, false); expiry.value = e.target.value; } }; Twinkle.block.seeAlsos = []; Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() { const joinEnum = function(e) { if (e.length >= 3) { return e.slice(0, -1).join(', ') + ' and ' + e[e.length - 1]; } else { return e.join(' and '); } }; const reason = this.form.reason.value.replace( new RegExp('( <!--|;) see also ' + joinEnum(Twinkle.block.seeAlsos) + '( -->)?'), '' ); Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter((el) => el !== this.value); if (this.checked) { Twinkle.block.seeAlsos.push(this.value); } const seeAlsoMessage = joinEnum(Twinkle.block.seeAlsos); if (!Twinkle.block.seeAlsos.length) { this.form.reason.value = reason; } else if (reason.includes('{{')) { this.form.reason.value = reason + ' <!-- see also ' + seeAlsoMessage + ' -->'; } else { this.form.reason.value = reason + '; see also ' + seeAlsoMessage; } }; Twinkle.block.dsReason = ''; Twinkle.block.callback.toggle_ds_reason = function twinkleblockCallbackToggleDSReason() { const reason = this.form.reason.value.replace( new RegExp(' ?\\(\\[\\[' + Twinkle.block.dsReason + '\\]\\]\\)'), '' ); Twinkle.block.dsinfo.then((dsinfo) => { const sanctionCode = this.selectedIndex; const sanctionName = this.options[sanctionCode].label; Twinkle.block.dsReason = dsinfo[sanctionName].page; if (!this.value) { this.form.reason.value = reason; } else { this.form.reason.value = reason + ' ([[' + Twinkle.block.dsReason + ']])'; } }); }; Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) { const form = e.target.form; let expiry = data.expiry; // don't override original expiry if useInitialOptions is set if (!data.useInitialOptions) { if (Date.parse(expiry)) { expiry = new Date(expiry).toGMTString(); form.expiry_preset.value = 'custom'; } else { form.expiry_preset.value = data.expiry || 'custom'; } form.expiry.value = expiry; if (form.expiry_preset.value === 'custom') { Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true); } else { Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false); } } // boolean-flipped options, more at [[mw:API:Block]] data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false; data.hardblock = data.hardblock !== undefined ? data.hardblock : false; // disable autoblock if blocking a bot if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) { data.autoblock = false; } $(form).find('[name=field_block_options]').find(':checkbox').each((i, el) => { // don't override original options if useInitialOptions is set if (data.useInitialOptions && data[el.name] === undefined) { return; } const check = data[el.name] === '' || !!data[el.name]; $(el).prop('checked', check); }); if (data.prependReason && data.reason) { form.reason.value = data.reason + '; ' + form.reason.value; } else { form.reason.value = data.reason || ''; } // Clear and/or set any partial page or namespace restrictions if (form.pagerestrictions) { const $pageSelect = $(form).find('[name=pagerestrictions]'); const $namespaceSelect = $(form).find('[name=namespacerestrictions]'); // Respect useInitialOptions by clearing data when switching presets // In practice, this will always clear, since no partial presets use it if (!data.useInitialOptions) { $pageSelect.val(null).trigger('change'); $namespaceSelect.val(null).trigger('change'); } // Add any preset options; in practice, just used for prior block settings if (data.restrictions) { if (data.restrictions.pages && !$pageSelect.val().length) { const pages = data.restrictions.pages.map((pr) => pr.title); // since page restrictions use an ajax source, we // short-circuit that and just add a new option pages.forEach((page) => { if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) { const newOption = new Option(page, page, true, true); $pageSelect.append(newOption); } }); $pageSelect.val($pageSelect.val().concat(pages)).trigger('change'); } if (data.restrictions.namespaces) { $namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change'); } } } }; Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) { const form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value]; const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked'); const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked'); const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked'); // Block form is not present if (!blockBox) { if (settings.indefinite || settings.nonstandard) { if (Twinkle.block.prev_template_expiry === null) { Twinkle.block.prev_template_expiry = form.template_expiry.value || ''; } form.template_expiry.parentNode.style.display = 'none'; form.template_expiry.value = 'infinity'; } else if (form.template_expiry.parentNode.style.display === 'none') { if (Twinkle.block.prev_template_expiry !== null) { form.template_expiry.value = Twinkle.block.prev_template_expiry; Twinkle.block.prev_template_expiry = null; } form.template_expiry.parentNode.style.display = 'block'; } if (Twinkle.block.prev_template_expiry) { form.expiry.value = Twinkle.block.prev_template_expiry; } Morebits.QuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard); // Partial Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode, partialBox); Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox); } else if (templateBox) { // Only present if block && template forms both visible Morebits.QuickForm.setElementVisibility( form.blank_duration.parentNode, !settings.indefinite && !settings.nonstandard ); } Morebits.QuickForm.setElementVisibility(form.dstopic.parentNode, value === 'uw-aeblock' || value === 'uw-aepblock'); // Only particularly relevant if template form is present Morebits.QuickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam); Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam); // Partial block Morebits.QuickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox); form.root.previewer.closePreview(); }; Twinkle.block.prev_template_expiry = null; Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) { const params = { article: form.article.value, blank_duration: form.blank_duration ? form.blank_duration.checked : false, disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false), expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value, hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked, indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value), reason: form.block_reason.value, template: form.template.value, dstopic: form.dstopic.value, partial: $(form).find('[name=actiontype][value=partial]').is(':checked'), pagerestrictions: $(form.pagerestrictions).val() || [], namespacerestrictions: $(form.namespacerestrictions).val() || [], noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false), nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false), area: form.area.value }; const templateText = Twinkle.block.callback.getBlockNoticeWikitext(params); form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username }; Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) { const $form = $(e.target), toBlock = $form.find('[name=actiontype][value=block]').is(':checked'), toWarn = $form.find('[name=actiontype][value=template]').is(':checked'), toPartial = $form.find('[name=actiontype][value=partial]').is(':checked'); let blockoptions = {}, templateoptions = {}; Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]')); Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]')); blockoptions = Twinkle.block.field_block_options; templateoptions = Twinkle.block.field_template_options; templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk); templateoptions.hardblock = !!blockoptions.hardblock; delete blockoptions.expiry_preset; // remove extraneous // Partial API requires this to be gone, not false or 0 if (toPartial) { blockoptions.partial = templateoptions.partial = true; } templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || []; templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || []; // Format for API here rather than in saveFieldset blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|'); blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|'); // use block settings as warn options where not supplied templateoptions.summary = templateoptions.summary || blockoptions.reason; templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry; if (toBlock) { if (blockoptions.partial) { if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.includes('3')) { return alert('Partial blocks cannot prevent talk page access unless also restricting them from editing User talk space!'); } if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) { if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]] return alert('No pages or namespaces were selected, nor were email or account creation restrictions applied; please select at least one option to apply a partial block!'); } else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') && // Don't require confirmation if email harassment defaults are set !confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) { return; } } } if (!blockoptions.expiry) { return alert('Please provide an expiry!'); } else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) { return alert("Can't indefinitely block an IP address!"); } if (!blockoptions.reason) { return alert('Please provide a reason for the block!'); } Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(e.target); const statusElement = new Morebits.Status('Executing block'); blockoptions.action = 'block'; blockoptions.user = relevantUserName; // boolean-flipped options blockoptions.anononly = blockoptions.hardblock ? undefined : true; blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true; /* Check if block status changed while processing the form. There's a lot to consider here. list=blocks provides the current block status, but there are at least two issues with relying on it. First, the id doesn't update on a reblock, meaning the individual parameters need to be compared. This can be done roughly with JSON.stringify - we can thankfully rely on order from the server, although sorting would be fine if not - but falsey values are problematic and is non-ideal. More importantly, list=blocks won't indicate if a non-blocked user is blocked then unblocked. This should be exceedingy rare, but regardless, we thus need to check list=logevents, which has a nicely updating logid parameter. We can't rely just on that, though, since it doesn't account for blocks that have expired on their own. As such, we use both. Using some ternaries, the logid variables are false if there's no logevents, so if they aren't equal we defintely have a changed entry (send confirmation). If they are equal, then either the user was never blocked (the block statuses will be equal, no confirmation) or there's no new block, in which case either a block expired (different statuses, confirmation) or the same block is still active (same status, no confirmation). */ const query = { format: 'json', action: 'query', list: 'blocks|logevents', letype: 'block', lelimit: 1, letitle: 'User:' + blockoptions.user }; // bkusers doesn't catch single IPs blocked as part of a range block if (mw.util.isIPAddress(blockoptions.user, true)) { query.bkip = blockoptions.user; } else { query.bkusers = blockoptions.user; } api.get(query).then((data) => { let block = data.query.blocks[0]; // As with the initial data fetch, if an IP is blocked // *and* rangeblocked, this would only grab whichever // block is more recent, which would likely mean a // mismatch. However, if the rangeblock is updated // while filling out the form, this won't detect that, // but that's probably fine. if (data.query.blocks.length > 1 && block.user !== relevantUserName) { block = data.query.blocks[1]; } const logevents = data.query.logevents[0]; const logid = data.query.logevents.length ? logevents.logid : false; if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) { let message = 'The block status of ' + blockoptions.user + ' has changed. '; if (block) { message += 'New status: '; } else { message += 'Last entry: '; } let logExpiry = ''; if (logevents.params.duration) { if (logevents.params.duration === 'infinity') { logExpiry = 'indefinitely'; } else { const expiryDate = new Morebits.Date(logevents.params.expiry); logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar(); } } else { // no duration, action=unblock, just show timestamp logExpiry = ' ' + new Morebits.Date(logevents.timestamp).calendar(); } message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry + ' for "' + logevents.comment + '". Do you want to override with your settings?'; if (!confirm(message)) { Morebits.Status.info('Executing block', 'Canceled by user'); return; } blockoptions.reblock = 1; // Writing over a block will fail otherwise } // execute block blockoptions.tags = Twinkle.changeTags; blockoptions.token = mw.user.tokens.get('csrfToken'); const mbApi = new Morebits.wiki.Api('Executing block', blockoptions, (() => { statusElement.info('Completed'); if (toWarn) { Twinkle.block.callback.issue_template(templateoptions); } })); mbApi.post(); }); } else if (toWarn) { Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(e.target); Twinkle.block.callback.issue_template(templateoptions); } else { return alert('Please give Twinkle something to do!'); } }; Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) { // Use wgRelevantUserName to ensure the block template goes to a single IP and not to the // "talk page" of an IP range (which does not exist) const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName'); const params = Twinkle.block.combineFormDataAndFieldTemplateOptions( formData, Twinkle.block.blockPresetsInfo[formData.template], Twinkle.block.field_template_options.block_reason, Twinkle.block.field_template_options.notalk, Twinkle.block.field_template_options.noemail_template, Twinkle.block.field_template_options.nocreate_template ); Morebits.wiki.actionCompleted.redirect = userTalkPage; Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds'; const wikipediaPage = new Morebits.wiki.Page(userTalkPage, 'User talk page modification'); wikipediaPage.setCallbackParameters(params); wikipediaPage.load(Twinkle.block.callback.main); }; Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) { return $.extend(formData, { messageData: messageData, reason: reason, disabletalk: disabletalk, noemail: noemail, nocreate: nocreate }); }; Twinkle.block.callback.getBlockNoticeWikitext = function(params) { let text = '{{'; const settings = Twinkle.block.blockPresetsInfo[params.template]; if (!settings.nonstandard) { text += 'subst:' + params.template; if (params.article && settings.pageParam) { text += '|page=' + params.article; } if (params.dstopic) { text += '|topic=' + params.dstopic; } if (!/te?mp|^\s*$|min/.exec(params.expiry)) { if (params.indefinite) { text += '|indef=yes'; } else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) { // Block template wants a duration, not date text += '|time=' + params.expiry; } } if (!Twinkle.block.isRegistered && !params.hardblock) { text += '|anon=yes'; } if (params.reason) { text += '|reason=' + params.reason; } if (params.disabletalk) { text += '|notalk=yes'; } // Currently, all partial block templates are "standard" // Building the template, however, takes a fair bit of logic if (params.partial) { if (params.pagerestrictions.length || params.namespacerestrictions.length) { const makeSentence = function (array) { if (array.length < 3) { return array.join(' and '); } const last = array.pop(); return array.join(', ') + ', and ' + last; }; text += '|area=' + (params.indefinite ? 'certain ' : 'from certain '); if (params.pagerestrictions.length) { text += 'pages (' + makeSentence(params.pagerestrictions.map((p) => '[[:' + p + ']]')); text += params.namespacerestrictions.length ? ') and certain ' : ')'; } if (params.namespacerestrictions.length) { // 1 => Talk, 2 => User, etc. const namespaceNames = params.namespacerestrictions.map((id) => menuFormattedNamespaces[id]); text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')'; } } else if (params.area) { text += '|area=' + params.area; } else { if (params.noemail) { text += '|email=yes'; } if (params.nocreate) { text += '|accountcreate=yes'; } } } } else { text += params.template; } if (settings.sig) { text += '|sig=' + settings.sig; } return text + '}}'; }; Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) { const params = pageobj.getCallbackParameters(), date = new Morebits.Date(pageobj.getLoadTime()), messageData = params.messageData; let text; params.indefinite = Morebits.string.isInfinity(params.expiry); if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) { Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month'); text = date.monthHeader() + '\n'; } else { text = pageobj.getPageText(); const dateHeaderRegex = date.monthHeaderRegex(); let dateHeaderRegexLast, dateHeaderRegexResult; while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) { dateHeaderRegexResult = dateHeaderRegexLast; } // If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but // \n== is not found, then the date header must be at the very start of the page. lastIndexOf // returns -1 in this case, so lastHeaderIndex gets set to 0 as desired. const lastHeaderIndex = text.lastIndexOf('\n==') + 1; if (text.length > 0) { text += '\n\n'; } if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) { Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found'); text += date.monthHeader() + '\n'; } } params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry; text += Twinkle.block.callback.getBlockNoticeWikitext(params); // build the edit summary let summary = messageData.summary; if (messageData.suppressArticleInSummary !== true && params.article) { summary += ' on [[:' + params.article + ']]'; } summary += '.'; pageobj.setPageText(text); pageobj.setEditSummary(summary); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('watchWarnings')); pageobj.save(); }; Twinkle.addInitCallback(Twinkle.block, 'block'); }()); // </nowiki> 2kwiq7glnyxrp02c1pdjt9wgv1kbklv MediaWiki:Gadget-twinklewelcome.js 8 24476 268713 2026-04-27T16:45:12Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinklewelcome.js: Welcome module **************************************** * Mode of invocation: Tab ("Wel"), or from links on diff pages * Active on: Any page with relevant user name (userspace, * contribs, etc.) and diff pages */ Twinkle.welcome = function twinklewelcome() { if (Twinkle.getPrefill('twinklewelcome')) { if (Twinkle.getPr...' 268713 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinklewelcome.js: Welcome module **************************************** * Mode of invocation: Tab ("Wel"), or from links on diff pages * Active on: Any page with relevant user name (userspace, * contribs, etc.) and diff pages */ Twinkle.welcome = function twinklewelcome() { if (Twinkle.getPrefill('twinklewelcome')) { if (Twinkle.getPrefill('twinklewelcome') === 'auto') { Twinkle.welcome.auto(); } else { Twinkle.welcome.semiauto(); } } else { Twinkle.welcome.normal(); } }; Twinkle.welcome.auto = function() { if (mw.util.getParamValue('action') !== 'edit') { // userpage not empty, aborting auto-welcome return; } Twinkle.welcome.welcomeUser(); }; Twinkle.welcome.semiauto = function() { Twinkle.welcome.callback(mw.config.get('wgRelevantUserName')); }; Twinkle.welcome.normal = function() { const isDiff = mw.util.getParamValue('diff'); if (isDiff) { // check whether the contributors' talk pages exist yet const $oldDiffUsernameLine = $('#mw-diff-otitle2'); const $newDiffUsernameLine = $('#mw-diff-ntitle2'); const $oldDiffHasRedlinkedTalkPage = $oldDiffUsernameLine.find('span.mw-usertoollinks a.new:contains(talk)').first(); const $newDiffHasRedlinkedTalkPage = $newDiffUsernameLine.find('span.mw-usertoollinks a.new:contains(talk)').first(); const diffHasRedlinkedTalkPage = $oldDiffHasRedlinkedTalkPage.length > 0 || $newDiffHasRedlinkedTalkPage.length > 0; if (diffHasRedlinkedTalkPage) { const spanTag = function(color, content) { const span = document.createElement('span'); span.style.color = color; span.appendChild(document.createTextNode(content)); return span; }; const welcomeNode = document.createElement('strong'); const welcomeLink = document.createElement('a'); welcomeLink.appendChild(spanTag('Black', '[')); welcomeLink.appendChild(spanTag('Goldenrod', 'welcome')); welcomeLink.appendChild(spanTag('Black', ']')); welcomeNode.appendChild(welcomeLink); if ($oldDiffHasRedlinkedTalkPage.length > 0) { const oHref = $oldDiffHasRedlinkedTalkPage.attr('href'); const oWelcomeNode = welcomeNode.cloneNode(true); oWelcomeNode.firstChild.setAttribute('href', oHref + '&' + $.param({ twinklewelcome: Twinkle.getPref('quickWelcomeMode') === 'auto' ? 'auto' : 'norm', vanarticle: Morebits.pageNameNorm })); $oldDiffHasRedlinkedTalkPage[0].parentNode.parentNode.appendChild(document.createTextNode(' ')); $oldDiffHasRedlinkedTalkPage[0].parentNode.parentNode.appendChild(oWelcomeNode); } if ($newDiffHasRedlinkedTalkPage.length > 0) { const nHref = $newDiffHasRedlinkedTalkPage.attr('href'); const nWelcomeNode = welcomeNode.cloneNode(true); nWelcomeNode.firstChild.setAttribute('href', nHref + '&' + $.param({ twinklewelcome: Twinkle.getPref('quickWelcomeMode') === 'auto' ? 'auto' : 'norm', vanarticle: Morebits.pageNameNorm })); $newDiffHasRedlinkedTalkPage[0].parentNode.parentNode.appendChild(document.createTextNode(' ')); $newDiffHasRedlinkedTalkPage[0].parentNode.parentNode.appendChild(nWelcomeNode); } } } // Users and IPs but not IP ranges if (mw.config.exists('wgRelevantUserName') && !Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) { Twinkle.addPortletLink(() => { Twinkle.welcome.callback(mw.config.get('wgRelevantUserName')); }, 'Wel', 'twinkle-welcome', 'Welcome user'); } }; Twinkle.welcome.welcomeUser = function welcomeUser() { Morebits.Status.init(document.getElementById('mw-content-text')); $('#catlinks').remove(); const params = { template: Twinkle.getPref('quickWelcomeTemplate'), article: Twinkle.getPrefill('vanarticle') || '', mode: 'auto' }; const userTalkPage = mw.config.get('wgFormattedNamespaces')[3] + ':' + mw.config.get('wgRelevantUserName'); Morebits.wiki.actionCompleted.redirect = userTalkPage; Morebits.wiki.actionCompleted.notice = 'Welcoming complete, reloading talk page in a few seconds'; const wikipedia_page = new Morebits.wiki.Page(userTalkPage, 'User talk page modification'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; Twinkle.welcome.callback = function twinklewelcomeCallback(uid) { if (uid === mw.config.get('wgUserName') && !confirm('Are you really sure you want to welcome yourself?...')) { return; } const Window = new Morebits.SimpleWindow(600, 420); Window.setTitle('Welcome user'); Window.setScriptName('Twinkle'); Window.addFooterLink('Welcoming Committee', 'WP:WC'); Window.addFooterLink('Welcome prefs', 'WP:TW/PREF#welcome'); Window.addFooterLink('Twinkle help', 'WP:TW/DOC#welcome'); Window.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.welcome.callback.evaluate); form.append({ type: 'select', name: 'type', label: 'Type of welcome:', event: Twinkle.welcome.populateWelcomeList, list: [ { type: 'option', value: 'standard', label: 'Standard welcomes', selected: !mw.util.isIPAddress(mw.config.get('wgRelevantUserName')) }, { type: 'option', value: 'unregistered', label: 'Unregistered user welcomes', selected: mw.util.isIPAddress(mw.config.get('wgRelevantUserName')) || mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName')) }, { type: 'option', value: 'wikiProject', label: 'WikiProject welcomes' }, { type: 'option', value: 'nonEnglish', label: 'Non-English welcomes' } ] }); form.append({ type: 'div', id: 'welcomeWorkArea', className: 'morebits-scrollbox' }); form.append({ type: 'input', name: 'article', label: '* Linked article (if supported by template):', value: Twinkle.getPrefill('vanarticle') || '', tooltip: 'An article might be linked from within the welcome if the template supports it. Leave empty for no article to be linked. Templates that support a linked article are marked with an asterisk.' }); const previewlink = document.createElement('a'); $(previewlink).on('click', () => { Twinkle.welcome.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = 'pointer'; previewlink.textContent = 'Preview'; form.append({ type: 'div', name: 'welcomepreview', label: [ previewlink ] }); form.append({ type: 'submit' }); var result = form.render(); Window.setContent(result); Window.display(); // initialize the welcome list const evt = document.createEvent('Event'); evt.initEvent('change', true, true); result.type.dispatchEvent(evt); }; Twinkle.welcome.populateWelcomeList = function(e) { const type = e.target.value; const container = new Morebits.QuickForm.Element({ type: 'fragment' }); if ((type === 'standard' || type === 'unregistered') && Twinkle.getPref('customWelcomeList').length) { container.append({ type: 'header', label: 'Custom welcome templates' }); container.append({ type: 'radio', name: 'template', list: Twinkle.getPref('customWelcomeList'), event: function() { e.target.form.article.disabled = false; } }); } const sets = Twinkle.welcome.templates[type]; $.each(sets, (label, templates) => { container.append({ type: 'header', label: label }); container.append({ type: 'radio', name: 'template', list: $.map(templates, (properties, template) => ({ value: template, label: '{{' + template + '}}: ' + properties.description + (properties.linkedArticle ? '\u00A0*' : ''), // U+00A0 NO-BREAK SPACE tooltip: properties.tooltip // may be undefined })), event: function(ev) { ev.target.form.article.disabled = !templates[ev.target.value].linkedArticle; } }); }); const rendered = container.render(); $(e.target.form).find('div#welcomeWorkArea').empty().append(rendered); const firstRadio = e.target.form.template[0]; firstRadio.checked = true; const vals = Object.values(sets)[0]; e.target.form.article.disabled = vals[firstRadio.value] ? !vals[firstRadio.value].linkedArticle : true; }; // A list of welcome templates and their properties and syntax // The four fields that are available are: // - "description" // - "linkedArticle" - when set to true, adds a "Linked article (if supported by template):" label and text box. The value typed into this text box is used to populate the $ARTICLE$ magic word below. // - "syntax" // - "tooltip" // The three magic words that can be used in the "syntax" field are: // - $USERNAME$ - replaced by the welcomer's username, depending on user's preferences // - $ARTICLE$ - replaced by an article name, if "linkedArticle" is true // - $HEADER$ - adds a level 2 header (most templates already include this) // - $EXTRA$ - custom message to be added at the end of the template. not implemented yet. Twinkle.welcome.templates = { standard: { 'General welcome templates': { welcome: { description: 'standard welcome', linkedArticle: true, syntax: '{{subst:welcome|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-retro': { description: 'a welcome message with a small list of helpful links', linkedArticle: true, syntax: '{{subst:welcome-retro|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-short': { description: 'a shorter welcome message', syntax: '{{subst:W-short|$EXTRA$}}' }, 'welcome-cookie': { description: 'a welcome message with some helpful links and a plate of cookies', syntax: '{{subst:welcome cookie}} ~~~~' }, welcoming: { description: 'welcome message with tutorial links and basic editing tips', syntax: '{{subst:Welcoming}}' } }, 'Specific welcome templates': { 'welcome-belated': { description: 'welcome for users with more substantial contributions', syntax: '{{subst:welcome-belated|$USERNAME$}}' }, 'welcome student': { description: 'welcome for students editing as part of an educational class project', syntax: '$HEADER$ {{subst:welcome student|$USERNAME$}} ~~~~' }, 'welcome teacher': { description: 'welcome for course instructors involved in an educational class project', syntax: '$HEADER$ {{subst:welcome teacher|$USERNAME$}} ~~~~' }, 'welcome non-latin': { description: 'welcome for users with a username containing non-Latin characters', syntax: '{{subst:welcome non-latin|$USERNAME$}} ~~~~' }, 'welcome mentor': { description: 'welcome for mentor users to give to their mentees', syntax: '{{subst:mentor welcome|$USERNAME$}} ~~~~' }, 'welcome draft': { description: 'welcome for users who write draft articles', linkedArticle: true, syntax: '{{subst:welcome draft|art=$ARTICLE$}} ~~~~' } }, 'Problem user welcome templates': { 'first article': { description: 'for someone whose first article did not meet page creation guidelines', linkedArticle: true, syntax: '{{subst:first article|$ARTICLE$|$USERNAME$}}' }, 'welcome-COI': { description: 'for someone who has edited in areas where they may have a conflict of interest', linkedArticle: true, syntax: '{{subst:welcome-COI|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-auto': { description: 'for someone who created an autobiographical article', linkedArticle: true, syntax: '{{subst:welcome-auto|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-copyright': { description: 'for someone who has been adding copyright violations to articles', linkedArticle: true, syntax: '{{subst:welcome-copyright|$ARTICLE$|$USERNAME$}} ~~~~' }, 'welcome-delete': { description: 'for someone who has been removing information from articles', linkedArticle: true, syntax: '{{subst:welcome-delete|$ARTICLE$|$USERNAME$}} ~~~~' }, 'welcome-image': { description: 'welcome with additional information about images (policy and procedure)', linkedArticle: true, syntax: '{{subst:welcome-image|$USERNAME$|art=$ARTICLE$}}' }, 'welcome-LLM': { description: 'for someone whose initial efforts seem to be made with a large language model', syntax: '{{subst:welcome-LLM}} ~~~~' }, 'welcome-translation': { description: 'for someone whose initial efforts are unattributed translations from another language Wikipedia', syntax: '{{subst:welcome-translation}}' }, 'welcome-unsourced': { description: 'for someone whose initial efforts are unsourced', linkedArticle: true, syntax: '{{subst:welcome-unsourced|$ARTICLE$|$USERNAME$}} ~~~~' }, welcomelaws: { description: 'welcome with information about copyrights, NPOV, the sandbox, and vandalism', syntax: '{{subst:welcomelaws|$USERNAME$}} ~~~~' }, welcomenpov: { description: 'for someone whose initial efforts do not adhere to the neutral point of view policy', linkedArticle: true, syntax: '{{subst:welcomenpov|$ARTICLE$|$USERNAME$}} ~~~~' }, welcomevandal: { description: 'for someone whose initial efforts appear to be vandalism', linkedArticle: true, syntax: '{{subst:welcomevandal|$ARTICLE$|$USERNAME$}}' }, welcomespam: { description: 'welcome with additional discussion of anti-spamming policies', linkedArticle: true, syntax: '{{subst:welcomespam|$ARTICLE$|$USERNAME$}} ~~~~' }, welcometest: { description: 'for someone whose initial efforts appear to be tests', linkedArticle: true, syntax: '{{subst:welcometest|$ARTICLE$|$USERNAME$}} ~~~~' } } }, unregistered: { 'Unregistered user welcome templates': { 'welcome-unregistered': { description: 'for unregistered users; encourages creating an account', linkedArticle: true, syntax: '{{subst:welcome-unregistered|art=$ARTICLE$}} ~~~~' }, thanks: { description: 'for unregistered users; short; encourages creating an account', linkedArticle: true, syntax: '== Welcome! ==\n{{subst:thanks|page=$ARTICLE$}} ~~~~' }, 'welcome-unregistered-test': { description: 'for unregistered users who have performed test edits', linkedArticle: true, syntax: '{{subst:welcome-unregistered-test|$ARTICLE$|$USERNAME$}} ~~~~' }, 'welcome-unregistered-unconstructive': { description: 'for unregistered users who have vandalized or made unhelpful edits', linkedArticle: true, syntax: '{{subst:welcome-unregistered-unconstructive|$ARTICLE$|$USERNAME$}}' }, 'welcome-unregistered-constructive': { description: 'for unregistered users who fight vandalism or edit constructively', linkedArticle: true, syntax: '{{subst:welcome-unregistered-constructive|art=$ARTICLE$}}' }, 'welcome-unregistered-delete': { description: 'for unregistered users who have removed content from pages', linkedArticle: true, syntax: '{{subst:welcome-unregistered-delete|$ARTICLE$|$USERNAME$}} ~~~~' }, 'welcome-unregistered-unsourced': { description: 'for anonymous users who have added unsourced content', linkedArticle: true, syntax: '{{subst:welcome-unregistered-unsourced|$ARTICLE$|$USERNAME$}}' } } }, wikiProject: { 'WikiProject-specific welcome templates': { 'TWA invite': { description: 'invite the user to The Wikipedia Adventure (not a welcome template)', syntax: '{{subst:WP:TWA/InviteTW|signature=~~~~}}' }, 'welcome-anatomy': { description: 'welcome for users with an apparent interest in anatomy topics', syntax: '{{subst:welcome-anatomy}} ~~~~' }, 'welcome-athletics': { description: 'welcome for users with an apparent interest in athletics (track and field) topics', syntax: '{{subst:welcome-athletics}}' }, 'welcome-au': { description: 'welcome for users with an apparent interest in Australia topics', syntax: '{{subst:welcome-au}} ~~~~' }, 'welcome-bd': { description: 'welcome for users with an apparent interest in Bangladesh topics', linkedArticle: true, syntax: '{{subst:welcome-bd|$USERNAME$||$EXTRA$|art=$ARTICLE$}} ~~~~' }, 'welcome-bio': { description: 'welcome for users with an apparent interest in biographical topics', syntax: '{{subst:welcome-bio}} ~~~~' }, 'welcome-cal': { description: 'welcome for users with an apparent interest in California topics', syntax: '{{subst:welcome-cal}} ~~~~' }, 'welcome-cath': { description: 'welcome for users with an apparent interest in Catholic topics', syntax: '{{subst:welcome-cath}}' }, 'welcome-conserv': { description: 'welcome for users with an apparent interest in conservatism topics', syntax: '{{subst:welcome-conserv}}' }, 'welcome-cycling': { description: 'welcome for users with an apparent interest in cycling topics', syntax: '{{subst:welcome-cycling}} ~~~~' }, 'welcome-dbz': { description: 'welcome for users with an apparent interest in Dragon Ball topics', syntax: '{{subst:welcome-dbz|$EXTRA$|sig=~~~~}}' }, 'welcome-et': { description: 'welcome for users with an apparent interest in Estonia topics', syntax: '{{subst:welcome-et}}' }, 'welcome-de': { description: 'welcome for users with an apparent interest in Germany topics', syntax: '{{subst:welcome-de}} ~~~~' }, 'welcome-in': { description: 'welcome for users with an apparent interest in India topics', syntax: '{{subst:welcome-in|$USERNAME$}} ~~~~' }, 'welcome-math': { description: 'welcome for users with an apparent interest in mathematical topics', linkedArticle: true, syntax: '{{subst:welcome-math|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-med': { description: 'welcome for users with an apparent interest in medicine topics', linkedArticle: true, syntax: '{{subst:welcome-med|$ARTICLE$}} ~~~~' }, 'welcome-no': { description: 'welcome for users with an apparent interest in Norway topics', syntax: '{{subst:welcome-no}} ~~~~' }, 'welcome-pk': { description: 'welcome for users with an apparent interest in Pakistan topics', syntax: '{{subst:welcome-pk|$USERNAME$}} ~~~~' }, 'welcome-phys': { description: 'welcome for users with an apparent interest in physics topics', linkedArticle: true, syntax: '{{subst:welcome-phys|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-pl': { description: 'welcome for users with an apparent interest in Poland topics', syntax: '{{subst:welcome-pl}} ~~~~' }, 'welcome-rugbyunion': { description: 'welcome for users with an apparent interest in rugby union topics', syntax: '{{subst:welcome-rugbyunion}} ~~~~' }, 'welcome-ru': { description: 'welcome for users with an apparent interest in Russia topics', syntax: '{{subst:welcome-ru}} ~~~~' }, 'welcome-starwars': { description: 'welcome for users with an apparent interest in Star Wars topics', syntax: '{{subst:welcome-starwars}} ~~~~' }, 'welcome-ch': { description: 'welcome for users with an apparent interest in Switzerland topics', linkedArticle: true, syntax: '{{subst:welcome-ch|$USERNAME$|art=$ARTICLE$}} ~~~~' }, 'welcome-uk': { description: 'welcome for users with an apparent interest in Ukraine topics', syntax: '{{subst:welcome-uk}} ~~~~' }, 'welcome-roads': { description: 'welcome for users with an apparent interest in roads and highways topics', syntax: '{{subst:welcome-roads}}' }, 'welcome-videogames': { description: 'welcome for users with an apparent interest in video game topics', syntax: '{{subst:welcome-videogames}}' }, 'WikiProject Women in Red invite': { description: 'welcome for users with an interest in writing about women', syntax: '{{subst:WikiProject Women in Red invite|1=~~~~}}' } } }, nonEnglish: { 'Non-English welcome templates': { welcomeen: { description: 'welcome for users whose first language is not listed here', syntax: '{{subst:welcomeen}}' }, 'welcomeen-ar': { description: 'welcome for users whose first language appears to be Arabic', syntax: '== Welcome! ==\n{{subst:welcomeen-ar}}' }, 'welcomeen-sq': { description: 'welcome for users whose first language appears to be Albanian', syntax: '== Welcome! ==\n{{subst:welcomeen-sq}}' }, 'welcomeen-zh': { description: 'welcome for users whose first language appears to be Chinese', syntax: '== Welcome! ==\n{{subst:welcomeen-zh}}' }, 'welcomeen-nl': { description: 'welcome for users whose first language appears to be Dutch', syntax: '== Welcome! ==\n{{subst:welcomeen-nl}}' }, 'welcomeen-fi': { description: 'welcome for users whose first language appears to be Finnish', syntax: '== Welcome! ==\n{{subst:welcomeen-fi}}' }, 'welcomeen-fr': { description: 'welcome for users whose first language appears to be French', syntax: '== Welcome! ==\n{{subst:welcomeen-fr}}' }, 'welcomeen-de': { description: 'welcome for users whose first language appears to be German', syntax: '== Welcome! ==\n{{subst:welcomeen-de}}' }, 'welcomeen-ha': { description: 'welcome for users whose first language appears to be Hausa', syntax: '== Welcome! ==\n{{subst:welcomeen-ha}}' }, 'welcomeen-he': { description: 'welcome for users whose first language appears to be Hebrew', syntax: '== Welcome! ==\n{{subst:welcomeen-he}}' }, 'welcomeen-hi': { description: 'welcome for users whose first language appears to be Hindi', syntax: '== Welcome! ==\n{{subst:welcomeen-hi}}' }, 'welcomeen-id': { description: 'welcome for users whose first language appears to be Indonesian', syntax: '== Welcome! ==\n{{subst:welcomeen-id}}' }, 'welcomeen-it': { description: 'welcome for users whose first language appears to be Italian', syntax: '== Welcome! ==\n{{subst:welcomeen-it}}' }, 'welcomeen-ja': { description: 'welcome for users whose first language appears to be Japanese', syntax: '== Welcome! ==\n{{subst:welcomeen-ja}}' }, 'welcomeen-ko': { description: 'welcome for users whose first language appears to be Korean', syntax: '== Welcome! ==\n{{subst:welcomeen-ko}}' }, 'welcomeen-ms': { description: 'welcome for users whose first language appears to be Malay', syntax: '== Welcome! ==\n{{subst:welcomeen-ms}}' }, 'welcomeen-ml': { description: 'welcome for users whose first language appears to be Malayalam', syntax: '== Welcome! ==\n{{subst:welcomeen-ml}}' }, 'welcomeen-mr': { description: 'welcome for users whose first language appears to be Marathi', syntax: '== Welcome! ==\n{{subst:welcomeen-mr}}' }, 'welcomeen-no': { description: 'welcome for users whose first language appears to be Norwegian', syntax: '== Welcome! ==\n{{subst:welcomeen-no}}' }, 'welcomeen-or': { description: 'welcome for users whose first language appears to be Oriya (Odia)', syntax: '== Welcome! ==\n{{subst:welcomeen-or}}' }, 'welcomeen-fa': { description: 'welcome for users whose first language appears to be Persian', syntax: '== Welcome! ==\n{{subst:welcomeen-fa}}' }, 'welcomeen-pl': { description: 'welcome for users whose first language appears to be Polish', syntax: '== Welcome! ==\n{{subst:welcomeen-pl}}' }, 'welcomeen-pt': { description: 'welcome for users whose first language appears to be Portuguese', syntax: '== Welcome! ==\n{{subst:welcomeen-pt}}' }, 'welcomeen-ro': { description: 'welcome for users whose first language appears to be Romanian', syntax: '== Welcome! ==\n{{subst:welcomeen-ro}}' }, 'welcomeen-ru': { description: 'welcome for users whose first language appears to be Russian', syntax: '== Welcome! ==\n{{subst:welcomeen-ru}}' }, 'welcomeen-es': { description: 'welcome for users whose first language appears to be Spanish', syntax: '== Welcome! ==\n{{subst:welcomeen-es}}' }, 'welcomeen-sv': { description: 'welcome for users whose first language appears to be Swedish', syntax: '== Welcome! ==\n{{subst:welcomeen-sv}}' }, 'welcomeen-th': { description: 'welcome for users whose first language appears to be Thai', syntax: '== Welcome! ==\n{{subst:welcomeen-th}}' }, 'welcomeen-tl': { description: 'welcome for users whose first language appears to be Tagalog', syntax: '== Welcome! ==\n{{subst:welcomeen-tl}}' }, 'welcomeen-tr': { description: 'welcome for users whose first language appears to be Turkish', syntax: '== Welcome! ==\n{{subst:welcomeen-tr}}' }, 'welcomeen-uk': { description: 'welcome for users whose first language appears to be Ukrainian', syntax: '== Welcome! ==\n{{subst:welcomeen-uk}}' }, 'welcomeen-ur': { description: 'welcome for users whose first language appears to be Urdu', syntax: '== Welcome! ==\n{{subst:welcomeen-ur}}' }, 'welcomeen-vi': { description: 'welcome for users whose first language appears to be Vietnamese', syntax: '== Welcome! ==\n{{subst:welcomeen-vi}}' } } } }; Twinkle.welcome.getTemplateWikitext = function(type, template, article) { // the iteration is required as the type=standard has two groups let properties; $.each(Twinkle.welcome.templates[type], (label, templates) => { properties = templates[template]; if (properties) { return false; // break } }); if (properties) { return properties.syntax .replace('$USERNAME$', Twinkle.getPref('insertUsername') ? mw.config.get('wgUserName') : '') .replace('$ARTICLE$', article || '') .replace(/\$HEADER\$\s*/, '== Welcome ==\n\n') .replace('$EXTRA$', ''); // EXTRA is not implemented yet } return '{{subst:' + template + (article ? '|art=' + article : '') + '}}' + (Twinkle.getPref('customWelcomeSignature') ? ' ~~~~' : ''); }; Twinkle.welcome.callbacks = { preview: function(form) { const previewDialog = new Morebits.SimpleWindow(750, 400); previewDialog.setTitle('Welcome template preview'); previewDialog.setScriptName('Welcome user'); previewDialog.setModality(true); const previewdiv = document.createElement('div'); previewdiv.style.marginLeft = previewdiv.style.marginRight = '0.5em'; previewdiv.style.fontSize = 'small'; previewDialog.setContent(previewdiv); const previewer = new Morebits.wiki.Preview(previewdiv); const input = Morebits.QuickForm.getInputData(form); previewer.beginRender(Twinkle.welcome.getTemplateWikitext(input.type, input.template, input.article), 'User talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username const submit = document.createElement('input'); submit.setAttribute('type', 'submit'); submit.setAttribute('value', 'Close'); previewDialog.addContent(submit); previewDialog.display(); $(submit).on('click', () => { previewDialog.close(); }); }, main: function(pageobj) { const params = pageobj.getCallbackParameters(); let text = pageobj.getPageText(); // abort if mode is auto and form is not empty if (pageobj.exists() && params.mode === 'auto') { Morebits.Status.info('Warning', 'User talk page not empty; aborting automatic welcome'); Morebits.wiki.actionCompleted.event(); return; } const welcomeText = Twinkle.welcome.getTemplateWikitext(params.type, params.template, params.article); if (Twinkle.getPref('topWelcomes')) { const hasTalkHeader = /^\{\{Talk ?header\}\}/i.test(text); if (hasTalkHeader) { text = text.replace(/^\{\{Talk ?header\}\}\n{0,2}/i, ''); text = '{{Talk header}}\n\n' + welcomeText + '\n\n' + text; text = text.trim(); } else { text = welcomeText + '\n\n' + text; } } else { text += '\n' + welcomeText; } const summaryText = 'Welcome to Wikipedia!'; pageobj.setPageText(text); pageobj.setEditSummary(summaryText); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('watchWelcomes')); pageobj.setCreateOption('recreate'); pageobj.save(); } }; Twinkle.welcome.callback.evaluate = function twinklewelcomeCallbackEvaluate(e) { const form = e.target; const params = Morebits.QuickForm.getInputData(form); // : type, template, article params.mode = 'manual'; Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); const userTalkPage = mw.config.get('wgFormattedNamespaces')[3] + ':' + mw.config.get('wgRelevantUserName'); Morebits.wiki.actionCompleted.redirect = userTalkPage; Morebits.wiki.actionCompleted.notice = 'Welcoming complete, reloading talk page in a few seconds'; const wikipedia_page = new Morebits.wiki.Page(userTalkPage, 'User talk page modification'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; Twinkle.addInitCallback(Twinkle.welcome, 'welcome'); }()); // </nowiki> 2kmkxcvj0mj6itnwuf5d0itlfgogiwu MediaWiki:Gadget-twinklexfd.js 8 24477 268714 2026-04-27T16:46:07Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinklexfd.js: XFD module **************************************** * Mode of invocation: Tab ("XFD") * Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects */ Twinkle.xfd = function twinklexfd() { // Disable on: // * special pages // * non-existent pages // * files on Commons, whether there is a...' 268714 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinklexfd.js: XFD module **************************************** * Mode of invocation: Tab ("XFD") * Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects */ Twinkle.xfd = function twinklexfd() { // Disable on: // * special pages // * non-existent pages // * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2, or R4 if it's a redirect) if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && document.getElementById('mw-sharedupload'))) { return; } let tooltip = 'Start a discussion for deleting'; if (mw.config.get('wgIsRedirect')) { tooltip += ' or retargeting this redirect'; } else { switch (mw.config.get('wgNamespaceNumber')) { case 0: tooltip += ' or moving this article'; break; case 10: tooltip += ' or merging this template'; break; case 828: tooltip += ' or merging this module'; break; case 6: tooltip += ' this file'; break; case 14: tooltip += ', merging or renaming this category'; break; default: tooltip += ' this page'; break; } } Twinkle.addPortletLink(Twinkle.xfd.callback, 'XFD', 'tw-xfd', tooltip); }; const utils = { /** Get ordinal number figure */ num2order: function(num) { switch (num) { case 1: return ''; case 2: return '2nd'; case 3: return '3rd'; default: return num + 'th'; } }, /** * Remove namespace name from title if present * Exception-safe wrapper around mw.Title * * @param {string} title */ stripNs: function(title) { const title_obj = mw.Title.newFromUserInput(title); if (!title_obj) { return title; // user entered invalid input; do nothing } return title_obj.getMainText(); }, /** * Add namespace name to page title if not already given * CAUTION: namespace name won't be added if a namespace (*not* necessarily * the same as the one given) already is there in the title * * @param {string} title * @param {number} namespaceNumber */ addNs: function(title, namespaceNumber) { const title_obj = mw.Title.newFromUserInput(title, namespaceNumber); if (!title_obj) { return title; // user entered invalid input; do nothing } return title_obj.toText(); }, /** * Provide Wikipedian TLA style: AfD, RfD, CfDS, RM, SfD, etc. * * @param {string} venue * @return {string} */ toTLACase: function(venue) { return venue .toString() // Everybody up, inclduing rm and the terminal s in cfds .toUpperCase() // Lowercase the central f in a given TLA and normalize sfd-t and sfr-t .replace(/(.)F(.)(?:-.)?/, '$1f$2'); } }; Twinkle.xfd.currentRationale = null; // error callback on Morebits.Status.object Twinkle.xfd.printRationale = function twinklexfdPrintRationale() { if (Twinkle.xfd.currentRationale) { Morebits.Status.printUserText(Twinkle.xfd.currentRationale, 'Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:'); // only need to print the rationale once Twinkle.xfd.currentRationale = null; } }; Twinkle.xfd.callback = function twinklexfdCallback() { const Window = new Morebits.SimpleWindow(700, 400); Window.setTitle('Start a deletion discussion (XfD)'); Window.setScriptName('Twinkle'); Window.addFooterLink('About deletion discussions', 'WP:XFD'); Window.addFooterLink('XfD prefs', 'WP:TW/PREF#xfd'); Window.addFooterLink('Twinkle help', 'WP:TW/DOC#xfd'); Window.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.xfd.callback.evaluate); const categories = form.append({ type: 'select', name: 'venue', label: 'Deletion discussion venue:', tooltip: 'When activated, a default choice is made, based on what namespace you are in. This default should be the most appropriate.', event: Twinkle.xfd.callback.change_category }); const namespace = mw.config.get('wgNamespaceNumber'); categories.append({ type: 'option', label: 'AfD (Articles for deletion)', selected: namespace === 0, // Main namespace value: 'afd' }); categories.append({ type: 'option', label: 'TfD (Templates for discussion)', selected: [ 10, 828 ].includes(namespace), // Template and module namespaces value: 'tfd' }); categories.append({ type: 'option', label: 'FfD (Files for discussion)', selected: namespace === 6, // File namespace value: 'ffd' }); categories.append({ type: 'option', label: 'CfD (Categories for discussion)', selected: namespace === 14 || (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)), // Category namespace and stub templates value: 'cfd' }); categories.append({ type: 'option', label: 'CfD/S (Categories for speedy renaming)', value: 'cfds' }); categories.append({ type: 'option', label: 'MfD (Miscellany for deletion)', selected: ![ 0, 6, 10, 14, 828 ].includes(namespace) || Morebits.pageNameNorm.indexOf('Template:User ', 0) === 0, // Other namespaces, and userboxes in template namespace value: 'mfd' }); categories.append({ type: 'option', label: 'RfD (Redirects for discussion)', selected: mw.config.get('wgIsRedirect'), value: 'rfd' }); categories.append({ type: 'option', label: 'RM (Requested moves)', selected: false, value: 'rm' }); form.append({ type: 'div', id: 'wrong-venue-warn', style: 'color: red; font-style: italic' }); form.append({ type: 'checkbox', list: [ { label: 'Notify page creator if possible', value: 'notify', name: 'notifycreator', tooltip: "A notification template will be placed on the creator's talk page if this is true.", checked: true } ] }); form.append({ type: 'field', label: 'Work area', name: 'work_area' }); const previewlink = document.createElement('a'); $(previewlink).on('click', () => { Twinkle.xfd.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = 'pointer'; previewlink.textContent = 'Preview'; form.append({ type: 'div', id: 'xfdpreview', label: [ previewlink ] }); form.append({ type: 'div', id: 'twinklexfd-previewbox', style: 'display: none' }); form.append({ type: 'submit' }); var result = form.render(); Window.setContent(result); Window.display(); result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklexfd-previewbox').last()[0]); // We must init the controls const evt = document.createEvent('Event'); evt.initEvent('change', true, true); result.venue.dispatchEvent(evt); }; Twinkle.xfd.callback.wrongVenueWarning = function twinklexfdWrongVenueWarning(venue) { let text = ''; const namespace = mw.config.get('wgNamespaceNumber'); switch (venue) { case 'afd': if (namespace !== 0) { text = 'AfD is generally appropriate only for articles.'; } else if (mw.config.get('wgIsRedirect')) { text = 'Please use RfD for redirects.'; } break; case 'tfd': if (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)) { text = 'Use CfD for stub templates.'; } else if (Morebits.pageNameNorm.indexOf('Template:User ') === 0) { text = 'Please use MfD for userboxes'; } break; case 'cfd': if (![ 10, 14 ].includes(namespace)) { text = 'CfD is only for categories and stub templates.'; } break; case 'cfds': if (namespace !== 14) { text = 'CfDS is only for categories.'; } break; case 'ffd': if (namespace !== 6) { text = 'FFD is selected but this page doesn\'t look like a file!'; } break; case 'rm': if (namespace === 14) { // category text = 'Please use CfD or CfDS for category renames.'; } else if ([118, 119, 2, 3].includes(namespace)) { // draft, draft talk, user, user talk text = 'RMs are not permitted in draft and userspace, unless they are uncontroversial technical requests.'; } break; default: // mfd or rfd break; } $('#wrong-venue-warn').text(text); }; Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) { const value = e.target.value; const form = e.target.form; const old_area = Morebits.QuickForm.getElements(e.target.form, 'work_area')[0]; let work_area = null; const oldreasontextbox = form.getElementsByTagName('textarea')[0]; const oldreason = oldreasontextbox ? oldreasontextbox.value : ''; const appendReasonBox = function twinklexfdAppendReasonBox() { work_area.append({ type: 'textarea', name: 'reason', label: 'Reason:', value: oldreason, tooltip: 'You can use wikimarkup in your reason. Twinkle will automatically sign your post.' }); }; Twinkle.xfd.callback.wrongVenueWarning(value); form.previewer.closePreview(); switch (value) { case 'afd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Articles for deletion', name: 'work_area' }); work_area.append({ type: 'div', label: '', // Added later by Twinkle.makeFindSourcesDiv() id: 'twinkle-xfd-findsources', style: 'margin-bottom: 5px; margin-top: -5px;' }); work_area.append({ type: 'select', name: 'outcome', label: 'Desired outcome:', event: Twinkle.xfd.callbacks.changeAfdOutcome, list: [ { label: 'Delete', value: 'deletion' }, { label: 'Merge', value: 'merging' }, { label: 'Redirect', value: 'redirecting' }, { label: 'Draftify', value: 'draftification' } ] }); work_area.append({ name: 'afdtarget', type: 'input', label: 'Target page:', tooltip: 'Target page for the redirect or merge.', event: Twinkle.xfd.callbacks.changeAfdTarget }); work_area.append({ type: 'checkbox', list: [ { label: 'Wrap deletion tag with &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; tags, so that it won\'t transclude. This option is not normally required.' } ] }); work_area.append({ type: 'select', name: 'xfdcat', label: 'Choose what category this nomination belongs in:', list: [ { label: 'Unknown', value: '?', selected: true }, { label: 'Media and music', value: 'M' }, { label: 'Organisation, corporation, or product', value: 'O' }, { label: 'Biographical', value: 'B' }, { label: 'Society topics', value: 'S' }, { label: 'Web or internet', value: 'W' }, { label: 'Games or sports', value: 'G' }, { label: 'Science and technology', value: 'T' }, { label: 'Fiction and the arts', value: 'F' }, { label: 'Places and transportation', value: 'P' }, { label: 'Indiscernible or unclassifiable topic', value: 'I' }, { label: 'Debate not yet sorted', value: 'U' } ] }); work_area.append({ type: 'select', multiple: true, name: 'delsortCats', label: 'Choose deletion sorting categories:', tooltip: 'Select a few categories that are specifically relevant to the subject of the article. Be as precise as possible; categories like People and USA should only be used when no other categories apply.' }); // grab deletion sort categories from en-wiki Morebits.wiki.getCachedJson('Wikipedia:WikiProject_Deletion_sorting/Computer-readable.json').then((delsortCategories) => { const $select = $('[name="delsortCats"]'); $.each(delsortCategories, (groupname, list) => { const $optgroup = $('<optgroup>').attr('label', groupname); const $delsortCat = $select.append($optgroup); list.forEach((item) => { const $option = $('<option>').val(item).text(item); $delsortCat.append($option); }); }); }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); // Now that we've rendered the form, hide the target text box. Unhide it later for certain outcomes, using a callback. $('[name="afdtarget"]').parent().hide(); Twinkle.makeFindSourcesDiv('#twinkle-xfd-findsources'); $(work_area).find('[name=delsortCats]') .attr('data-placeholder', 'Select delsort pages') .select2({ theme: 'default select2-morebits', width: '100%', matcher: Morebits.select2.matcher, templateResult: Morebits.select2.highlightSearchMatches, language: { searching: Morebits.select2.queryInterceptor }, // Link text to the page itself templateSelection: function(choice) { return $('<a>').text(choice.text).attr({ href: mw.util.getUrl('Wikipedia:WikiProject_Deletion_sorting/' + choice.text), target: '_blank' }); } }); mw.util.addCSS( // Remove black border '.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' + // Reduce padding '.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' + '.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' + // Adjust font size '.select2-container .select2-dropdown .select2-results { font-size: 13px; }' + '.select2-container .selection .select2-selection__rendered { font-size: 13px; }' + // Make the tiny cross larger '.select2-selection__choice__remove { font-size: 130%; }' ); break; case 'tfd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Templates for discussion', name: 'work_area' }); var templateOrModule = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'module' : 'template'; work_area.append({ type: 'select', label: 'Choose type of action wanted:', name: 'xfdcat', event: function(e) { const target = e.target; let tfdtarget = target.form.tfdtarget; // add/remove extra input box if (target.value === 'tfm' && !tfdtarget) { tfdtarget = new Morebits.QuickForm.Element({ name: 'tfdtarget', type: 'input', label: 'Other ' + templateOrModule + ' to be merged:', tooltip: 'Required. Should not include the ' + Morebits.string.toUpperCaseFirstChar(templateOrModule) + ': namespace prefix.', required: true }); target.parentNode.appendChild(tfdtarget.render()); } else { $(Morebits.QuickForm.getElementContainer(tfdtarget)).remove(); tfdtarget = null; } }, list: [ { label: 'Deletion', value: 'tfd', selected: true }, { label: 'Merge', value: 'tfm' } ] }); work_area.append({ type: 'select', name: 'templatetype', label: 'Deletion tag display style:', tooltip: 'Which <code>type=</code> parameter to pass to the TfD tag template.', list: templateOrModule === 'module' ? [ { value: 'module', label: 'Module', selected: true } ] : [ { value: 'standard', label: 'Standard', selected: true }, { value: 'sidebar', label: 'Sidebar/infobox', selected: $('.infobox').length }, { value: 'inline', label: 'Inline template', selected: $('.mw-parser-output > p .Inline-Template').length }, { value: 'tiny', label: 'Tiny inline' }, { value: 'disabled', label: 'Disabled' } ] }); work_area.append({ type: 'checkbox', list: [ { label: 'Wrap deletion tag with &lt;noinclude&gt; (for substituted templates only)', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; tags, so that it won\'t get substituted along with the template.', disabled: templateOrModule === 'module', checked: !!$('.box-Subst_only').length // Default to checked if page carries {{subst only}} } ] }); work_area.append({ type: 'checkbox', list: [ { label: 'Notify talk pages of affected user scripts', value: 'devpages', name: 'devpages', tooltip: 'A notification will be sent to Twinkle, AWB, and Ultraviolet\'s talk pages if those user scripts are marked as using this template.', checked: true } ] }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'mfd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Miscellany for deletion', name: 'work_area' }); if (mw.config.get('wgNamespaceNumber') !== 710) { // TimedText cannot be tagged, so asking whether to noinclude the tag is pointless work_area.append({ type: 'checkbox', list: [ { label: 'Wrap deletion tag with &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; tags, so that it won\'t transclude. Select this option for userboxes.' } ] }); } if ((mw.config.get('wgNamespaceNumber') === 2 /* User: */ || mw.config.get('wgNamespaceNumber') === 3 /* User talk: */) && mw.config.exists('wgRelevantUserName')) { work_area.append({ type: 'checkbox', list: [ { label: 'Notify owner of userspace (if they are not the page creator)', value: 'notifyuserspace', name: 'notifyuserspace', tooltip: 'If the user in whose userspace this page is located is not the page creator (for example, the page is a rescued article stored as a userspace draft), notify the userspace owner as well.', checked: true } ] }); } appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'ffd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Discussion venues for files', name: 'work_area' }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'cfd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Categories for discussion', name: 'work_area' }); var isCategory = mw.config.get('wgNamespaceNumber') === 14; work_area.append({ type: 'select', label: 'Choose type of action wanted:', name: 'xfdcat', event: function(e) { const value = e.target.value, cfdtarget = e.target.form.cfdtarget; let cfdtarget2 = e.target.form.cfdtarget2; // update enabled status cfdtarget.disabled = value === 'cfd' || value === 'sfd-t'; if (isCategory) { // update label if (value === 'cfs') { Morebits.QuickForm.setElementLabel(cfdtarget, 'Target categories: '); } else if (value === 'cfc') { Morebits.QuickForm.setElementLabel(cfdtarget, 'Target article: '); } else { Morebits.QuickForm.setElementLabel(cfdtarget, 'Target category: '); } // add/remove extra input box if (value === 'cfs') { if (cfdtarget2) { cfdtarget2.disabled = false; $(cfdtarget2).show(); } else { cfdtarget2 = document.createElement('input'); cfdtarget2.setAttribute('name', 'cfdtarget2'); cfdtarget2.setAttribute('type', 'text'); cfdtarget2.setAttribute('required', 'true'); cfdtarget.parentNode.appendChild(cfdtarget2); } } else { $(cfdtarget2).prop('disabled', true); $(cfdtarget2).hide(); } } else { // Update stub template label Morebits.QuickForm.setElementLabel(cfdtarget, 'Target stub template: '); } }, list: isCategory ? [ { label: 'Deletion', value: 'cfd', selected: true }, { label: 'Merge', value: 'cfm' }, { label: 'Renaming', value: 'cfr' }, { label: 'Split', value: 'cfs' }, { label: 'Convert into article', value: 'cfc' } ] : [ { label: 'Stub Deletion', value: 'sfd-t', selected: true }, { label: 'Stub Renaming', value: 'sfr-t' } ] }); work_area.append({ type: 'input', name: 'cfdtarget', label: 'Target category:', // default, changed above disabled: true, required: true, // only when enabled value: '' }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'cfds': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Categories for speedy renaming', name: 'work_area' }); work_area.append({ type: 'select', label: 'C2 sub-criterion:', name: 'xfdcat', tooltip: 'See WP:CFDS for full explanations.', list: [ { label: 'C2A: Typographic and spelling fixes', value: 'C2A', selected: true }, { label: 'C2B: Naming conventions and disambiguation', value: 'C2B' }, { label: 'C2C: Consistency with names of similar categories', value: 'C2C' }, { label: 'C2D: Rename to match article name', value: 'C2D' }, { label: 'C2E: Author request', value: 'C2E' }, { label: 'C2F: One eponymous article', value: 'C2F' } ] }); work_area.append({ type: 'input', name: 'cfdstarget', label: 'New name:', size: 70, value: '', required: true }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'rfd': work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Redirects for discussion', name: 'work_area' }); work_area.append({ type: 'checkbox', list: [ { label: 'Notify target page if possible', value: 'relatedpage', name: 'relatedpage', tooltip: "A notification template will be placed on the talk page of this redirect's target if this is true.", checked: true } ] }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; case 'rm': { work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Requested moves', name: 'work_area' }); work_area.append({ type: 'checkbox', list: [ { label: 'Uncontroversial technical request', value: 'rmtr', name: 'rmtr', tooltip: 'Use this option when you are unable to perform this uncontroversial move yourself because of a technical reason (e.g. a page already exists at the new title, or the page is protected)', checked: false, event: function() { $('input[name="newname"]', form).prop('required', this.checked); $('input[type="button"][value="more"]', form)[0].sublist.inputs[1].required = this.checked; }, subgroup: { type: 'checkbox', list: [ { label: 'Opt out of discussion if the request is contested', value: 'rmtr-discuss', name: 'rmtr-discuss', tooltip: 'Use this option if you prefer to withdraw the request if contested, rather than discuss it. This suppresses the "discuss" link, which may be used to convert your request to a discussion on the talk page.', checked: false } ] } } ] }); work_area.append({ type: 'dyninput', inputs: [ { label: 'From:', name: 'currentname', required: true }, { label: 'To:', name: 'newname', tooltip: 'Required for technical requests. Otherwise, if unsure of the appropriate title, you may leave it blank.' } ], min: 1 }); appendReasonBox(); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); const currentNonTalkPage = mw.Title.newFromText(Morebits.pageNameNorm).getSubjectPage().toText(); form.currentname.value = currentNonTalkPage; break; } default: work_area = new Morebits.QuickForm.Element({ type: 'field', label: 'Nothing for anything', name: 'work_area' }); work_area = work_area.render(); old_area.parentNode.replaceChild(work_area, old_area); break; } // Return to checked state when switching, but no creator notification for CFDS or RM form.notifycreator.disabled = value === 'cfds' || value === 'rm'; form.notifycreator.checked = !form.notifycreator.disabled; }; Twinkle.xfd.callbacks = { /** If the user hasn't modified the reason much, modify the reason to include the target article. If the user has modified the reason box with a custom reason, do nothing, since we don't want to blank their work. */ changeAfdTarget: function() { const $afdTarget = $('[name="afdtarget"]'); const $reason = $('[name="reason"]'); const $outcome = $('[name="outcome"]'); if ($reason.val().endsWith('because ')) { // Target has something typed in if ($afdTarget.val()) { if ($outcome.val() === 'redirecting') { $reason.val(`I propose '''redirecting''' to [[${$afdTarget.val()}]] because `); } else if ($outcome.val() === 'merging') { $reason.val(`I propose '''merging''' to [[${$afdTarget.val()}]] because `); } // Target is blank } else { if ($outcome.val() === 'redirecting') { $reason.val("I propose '''redirecting''' because "); } else if ($outcome.val() === 'merging') { $reason.val("I propose '''merging''' because "); } } } }, /** Print a default reason in the reason textarea, depending on which outcome is selected from the outcome dropdown list. */ changeAfdOutcome: function() { const $outcome = $('[name="outcome"]'); const $reason = $('[name="reason"]'); const $afdTarget = $('[name="afdtarget"]'); $afdTarget.val(''); if ($outcome.val() === 'redirecting') { $reason.val("I propose '''redirecting''' because "); $afdTarget.parent().show(); } else if ($outcome.val() === 'merging') { $reason.val("I propose '''merging''' because "); $afdTarget.parent().show(); } else if ($outcome.val() === 'draftification') { $reason.val("I propose '''draftifying''' because "); $afdTarget.parent().hide(); } else if ($outcome.val() === 'deletion') { $reason.val(''); $afdTarget.parent().hide(); } }, /** Requires having the tag text (params.tagText) set ahead of time */ autoEditRequest: function(pageobj, params) { const talkName = new mw.Title(pageobj.getPageName()).getTalkPage().toText(); if (talkName === pageobj.getPageName()) { pageobj.getStatusElement().error('Page protected and nowhere to add an edit request, aborting'); } else { pageobj.getStatusElement().warn('Page protected, requesting edit'); const editRequest = '{{subst:Xfd edit protected|page=' + pageobj.getPageName() + '|discussion=' + params.discussionpage + (params.venue === 'rfd' ? '|rfd=yes' : '') + '|tag=<nowiki>' + params.tagText + '\u003C/nowiki>}}'; // U+003C: < const talk_page = new Morebits.wiki.Page(talkName, 'Automatically posting edit request on talk page'); talk_page.setNewSectionTitle('Edit request to complete ' + utils.toTLACase(params.venue) + ' nomination'); talk_page.setNewSectionText(editRequest); talk_page.setCreateOption('recreate'); talk_page.setWatchlist(Twinkle.getPref('xfdWatchPage')); talk_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect talk_page.setChangeTags(Twinkle.changeTags); talk_page.setCallbackParameters(params); talk_page.newSection(null, () => { talk_page.getStatusElement().warn('Unable to add edit request, the talk page may be protected'); }); } }, getDiscussionWikitext: function(venue, params) { if (venue === 'cfds') { // CfD/S takes a completely different style return '* [[:' + Morebits.pageNameNorm + ']] to [[:' + params.cfdstarget + ']]\u00A0\u2013 ' + params.xfdcat + (params.reason ? ': ' + Morebits.string.formatReasonText(params.reason) : '.') + ' ~~~~'; // U+00A0 NO-BREAK SPACE; U+2013 EN RULE } if (venue === 'rm') { if (params.rmtr) { const rmtrDiscuss = params['rmtr-discuss'] ? '|discuss=no' : ''; return params.currentname .map((currentname, i) => `{{subst:RMassist|1=${currentname}|2=${params.newname[i]}${rmtrDiscuss}|reason=${params.reason}}}`) .join('\n'); } return `{{subst:Requested move${ params.currentname .map((currentname, i) => `|current${i + 1}=${currentname}|new${i + 1}=${params.newname[i]}`) .join('') }|reason=${params.reason}}}`; } let text = '{{subst:' + venue + '2'; const reasonKey = venue === 'ffd' ? 'Reason' : 'text'; // Add a reason unconditionally, so that at least a signature is added text += '|' + reasonKey + '=' + Morebits.string.formatReasonText(params.reason, true); if (venue === 'afd' || venue === 'mfd') { text += '|pg=' + Morebits.pageNameNorm; if (venue === 'afd') { text += '|cat=' + params.xfdcat; } } else if (venue === 'rfd') { text += '|redirect=' + Morebits.pageNameNorm; } else { text += '|1=' + mw.config.get('wgTitle'); if (mw.config.get('wgPageContentModel') === 'Scribunto') { text += '|module=Module:'; } } if (params.rfdtarget) { text += '|target=' + params.rfdtarget + (params.section ? '#' + params.section : ''); } else if (params.tfdtarget) { text += '|2=' + params.tfdtarget; } else if (params.cfdtarget) { text += '|2=' + params.cfdtarget; if (params.cfdtarget2) { text += '|3=' + params.cfdtarget2; } } else if (params.uploader) { text += '|Uploader=' + params.uploader; } text += '}}'; if (venue === 'rfd' || venue === 'tfd' || venue === 'cfd') { text += '\n'; } // Don't delsort if delsortCats is undefined (TFD, FFD, etc.) // Don't delsort if delsortCats is an empty array (AFD where user chose no categories) if (Array.isArray(params.delsortCats) && params.delsortCats.length) { text += '\n{{subst:Deletion sorting/multi|' + params.delsortCats.join('|') + '|sig=~~~~}}'; } return text; }, showPreview: function(form, venue, params) { const templatetext = Twinkle.xfd.callbacks.getDiscussionWikitext(venue, params); if (venue === 'rm') { // RM templates are sensitive to page title form.previewer.beginRender(templatetext, params.rmtr ? 'Wikipedia:Requested moves/Technical requests' : new mw.Title(Morebits.pageNameNorm).getTalkPage().toText()); } else { form.previewer.beginRender(templatetext, 'WP:TW'); // Force wikitext } }, preview: function(form) { // venue, reason, xfdcat, tfdtarget, cfdtarget, cfdtarget2, cfdstarget, delsortCats, newname const params = Morebits.QuickForm.getInputData(form); const venue = params.venue; // Remove CfD or TfD namespace prefixes if given if (params.tfdtarget) { params.tfdtarget = utils.stripNs(params.tfdtarget); } else if (params.cfdtarget) { params.cfdtarget = utils.stripNs(params.cfdtarget); if (params.cfdtarget2) { params.cfdtarget2 = utils.stripNs(params.cfdtarget2); } } else if (params.cfdstarget) { // Add namespace if not given (CFDS) params.cfdstarget = utils.addNs(params.cfdstarget, 14); } if (venue === 'ffd') { // Fetch the uploader const page = new Morebits.wiki.Page(mw.config.get('wgPageName')); page.lookupCreation(() => { params.uploader = page.getCreator(); Twinkle.xfd.callbacks.showPreview(form, venue, params); }); } else if (venue === 'rfd') { // Find the target Twinkle.xfd.callbacks.rfd.findTarget(params, (params) => { Twinkle.xfd.callbacks.showPreview(form, venue, params); }); } else if (venue === 'cfd') { // Swap in CfD subactions Twinkle.xfd.callbacks.showPreview(form, params.xfdcat, params); } else { Twinkle.xfd.callbacks.showPreview(form, venue, params); } }, /** * Unified handler for sending {{Xfd notice}} notifications * Also handles userspace logging * * @param {Object} params * @param {string} notifyTarget The user or page being notified * @param {boolean} [noLog=false] Whether to skip logging to userspace * XfD log, especially useful in cases in where multiple notifications * may be sent out (MfD, TfM, RfD) * @param {string} [actionName] Alternative description of the action * being undertaken. Required if not notifying a user talk page. */ notifyUser: function(params, notifyTarget, noLog, actionName) { // Ensure items with User talk or no namespace prefix both end // up at user talkspace as expected, but retain the // prefix-less username for addToLog const userTalkNamespace = 3; notifyTarget = mw.Title.newFromText(notifyTarget, userTalkNamespace); const targetNS = notifyTarget.getNamespaceId(); const usernameOrTarget = notifyTarget.getRelativeText(userTalkNamespace); notifyTarget = notifyTarget.toText(); if (targetNS === userTalkNamespace) { // Disallow warning yourself if (usernameOrTarget === mw.config.get('wgUserName')) { Morebits.Status.warn('You (' + usernameOrTarget + ') created this page; skipping user notification'); // if we thought we would notify someone but didn't, // then jump to logging. Twinkle.xfd.callbacks.addToLog(params, null); return; } // Default is notifying the initial contributor, but MfD also // notifies userspace page owner actionName = actionName || 'Notifying initial contributor (' + usernameOrTarget + ')'; } const notifyText = Twinkle.xfd.callbacks.generateUserTalkNoticeWikitext(params); // Link to the venue; object used here rather than repetitive items in switch const venueNames = { afd: 'Articles for deletion', tfd: 'Templates for discussion', mfd: 'Miscellany for deletion', cfd: 'Categories for discussion', ffd: 'Files for discussion', rfd: 'Redirects for discussion' }; const editSummary = 'Notification: [[' + params.discussionpage + '|listing]] of [[:' + Morebits.pageNameNorm + ']] at [[WP:' + venueNames[params.venue] + ']].'; const usertalkpage = new Morebits.wiki.Page(notifyTarget, actionName); usertalkpage.setAppendText(notifyText); usertalkpage.setEditSummary(editSummary); usertalkpage.setChangeTags(Twinkle.changeTags); usertalkpage.setCreateOption('recreate'); // Different pref for RfD target notifications if (params.venue === 'rfd' && targetNS !== 3) { usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchRelated')); } else { usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchUser')); } usertalkpage.setFollowRedirect(true, false); if (noLog) { usertalkpage.append(); } else { usertalkpage.append(() => { // Don't treat RfD target or MfD userspace owner as initialContrib in log if (!params.notifycreator) { notifyTarget = null; } // add this nomination to the user's userspace log Twinkle.xfd.callbacks.addToLog(params, usernameOrTarget); }, () => { // if user could not be notified, log nomination without mentioning that notification was sent Twinkle.xfd.callbacks.addToLog(params, null); }); } }, generateUserTalkNoticeWikitext: function(params) { // For grep: Afd notice, Mfd notice, Tfd notice, Cfd notice, Ffd notice, Rfd notice let notifytext = '\n{{subst:' + params.venue + ' notice'; const templateNamespace = 10; // Venue-specific parameters switch (params.venue) { case 'afd': notifytext += params.outcome !== 'deletion' ? '|outcome=' + params.outcome : ''; notifytext += params.afdtarget ? '|target=' + params.afdtarget : ''; // Tell the template to add " (Xnd nomination)" to the XFD title, if needed. // The &#32; (HTML space character) is needed to overcome MediaWiki's parameter auto-trim. notifytext += params.numbering !== '' ? '|order=&#32;' + params.numbering : ''; break; case 'mfd': // tell the template to add " (Xnd nomination)" to the XFD title, if needed notifytext += params.numbering !== '' ? '|order=&#32;' + params.numbering : ''; break; case 'tfd': if (params.xfdcat === 'tfm') { notifytext = '\n{{subst:Tfm notice|2=' + params.tfdtarget; } break; case 'cfd': notifytext += '|action=' + params.action + (mw.config.get('wgNamespaceNumber') === templateNamespace ? '|stub=yes' : ''); break; default: // ffd, rfd break; } notifytext += '|1=' + Morebits.pageNameNorm + '}} ~~~~'; return notifytext; }, addToLog: function(params, initialContrib) { if (!Twinkle.getPref('logXfdNominations') || Twinkle.getPref('noLogOnXfdNomination').includes(params.venue)) { return; } const usl = new Morebits.UserspaceLogger(Twinkle.getPref('xfdLogPageName'));// , 'Adding entry to userspace log'); usl.initialText = "This is a log of all [[WP:XFD|deletion discussion]] nominations made by this user using [[WP:TW|Twinkle]]'s XfD module.\n\n" + 'If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and ' + 'nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].' + (Morebits.userIsSysop ? '\n\nThis log does not track XfD-related deletions made using Twinkle.' : ''); let editsummary; if (params.discussionpage) { editsummary = 'Logging [[' + params.discussionpage + '|' + utils.toTLACase(params.venue) + ' nomination]] of [[:' + Morebits.pageNameNorm + ']].'; } else { editsummary = 'Logging ' + utils.toTLACase(params.venue) + ' nomination of [[:' + Morebits.pageNameNorm + ']].'; } // If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log const fileLogLink = mw.config.get('wgNamespaceNumber') === 6 ? ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} log])' : ''; // CFD/S and RM don't have canonical links const nominatedLink = params.discussionpage ? '[[' + params.discussionpage + '|nominated]]' : 'nominated'; let appendText = '# [[:' + Morebits.pageNameNorm + ']]:' + fileLogLink + ' ' + nominatedLink + ' at [[WP:' + params.venue.toUpperCase() + '|' + utils.toTLACase(params.venue) + ']]'; switch (params.venue) { case 'tfd': if (params.xfdcat === 'tfm') { appendText += ' (merge)'; if (params.tfdtarget) { const contentModel = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'Module:' : 'Template:'; appendText += '; Other ' + contentModel.toLowerCase() + ' [['; if (!new RegExp('^:?' + Morebits.namespaceRegex([10, 828]) + ':', 'i').test(params.tfdtarget)) { appendText += contentModel; } appendText += params.tfdtarget + ']]'; } } break; case 'mfd': if (params.notifyuserspace && params.userspaceOwner && params.userspaceOwner !== initialContrib) { appendText += '; notified {{user|1=' + params.userspaceOwner + '}}'; } break; case 'cfd': appendText += ' (' + utils.toTLACase(params.xfdcat) + ')'; if (params.cfdtarget) { const categoryOrTemplate = params.xfdcat.charAt(0) === 's' ? 'Template:' : ':Category:'; appendText += '; ' + params.action + ' to [[' + categoryOrTemplate + params.cfdtarget + ']]'; if (params.xfdcat === 'cfs' && params.cfdtarget2) { appendText += ', [[' + categoryOrTemplate + params.cfdtarget2 + ']]'; } } break; case 'cfds': appendText += ' (' + utils.toTLACase(params.xfdcat) + ')'; // Ensure there's more than just 'Category:' if (params.cfdstarget && params.cfdstarget.length > 9) { appendText += '; New name: [[:' + params.cfdstarget + ']]'; } break; case 'rfd': if (params.rfdtarget) { appendText += '; Target: [[:' + params.rfdtarget + ']]'; if (params.relatedpage) { appendText += ' (notified)'; } } break; case 'rm': appendText = params.currentname .map((currentname, i) => `# [[:${currentname}]]: ${nominatedLink} at [[WP:RM${params.rmtr ? '/TR' : ''}|]]${params.newname[i] ? `; New name: [[:${params.newname[i]}]]` : ''}`) .join('\n'); break; default: // afd or ffd break; } if (initialContrib && params.notifycreator) { appendText += '; notified {{user|1=' + initialContrib + '}}'; } appendText += ' ~~~~~'; if (params.reason) { appendText += "\n#* '''Reason''': " + Morebits.string.formatReasonForLog(params.reason); } usl.changeTags = Twinkle.changeTags; usl.log(appendText, editsummary); }, afd: { main: function(apiobj) { const response = apiobj.getResponse(); const titles = response.query.allpages; // There has been no earlier entries with this prefix, just go on. if (titles.length <= 0) { apiobj.params.numbering = apiobj.params.number = ''; } else { let number = 0; for (let i = 0; i < titles.length; ++i) { const title = titles[i].title; // First, simple test, is there an instance with this exact name? if (title === 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) { number = Math.max(number, 1); continue; } const order_re = new RegExp('^' + Morebits.string.escapeRegExp('Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) + '\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$'); const match = order_re.exec(title); // No match; A non-good value // Or the match is an unrealistically high number. Avoid false positives such as Wikipedia:Articles for deletion/The Basement (2014), by ignoring matches greater than 100 if (!match || match[1] > 100) { continue; } // A match, set number to the max of current number = Math.max(number, Number(match[1])); } apiobj.params.number = utils.num2order(parseInt(number, 10) + 1); apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : ''; } apiobj.params.discussionpage = 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering; Morebits.Status.info('Next discussion page', '[[' + apiobj.params.discussionpage + ']]'); // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage; Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page'; // Tagging article const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to article'); wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect wikipedia_page.setChangeTags(Twinkle.changeTags); // Here to apply to triage wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle); }, // Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page taggingArticle: function(pageobj) { let text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); if (!pageobj.exists()) { statelem.error("It seems that the page doesn't exist; perhaps it has already been deleted"); return; } // Check for existing AfD tag, for the benefit of new page patrollers const textNoAfd = text.replace(/<!--.*AfD.*\n\{\{(?:Article for deletion\/dated|AfDM).*\}\}\n<!--.*(?:\n<!--.*)?AfD.*(?:\s*\n)?/g, ''); if (text !== textNoAfd) { if (confirm('An AfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current AfD tag (not recommended), or Cancel to abandon your nomination.')) { text = textNoAfd; } else { statelem.error('Article already tagged with AfD tag, and you chose to abort'); window.location.reload(); return; } } // Now we know we want to go ahead with it, trigger the other AJAX requests // Mark the page as curated/patrolled, if wanted if (Twinkle.getPref('markXfdPagesAsPatrolled')) { pageobj.triage(); } // Start discussion page, will also handle pagetriage and delsort listings let wikipedia_page = new Morebits.wiki.Page(params.discussionpage, 'Creating article deletion discussion page'); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage); // Today's list const date = new Morebits.Date(pageobj.getLoadTime()); wikipedia_page = new Morebits.wiki.Page('Wikipedia:Articles for deletion/Log/' + date.format('YYYY MMMM D', 'utc'), "Adding discussion to today's list"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList); // Notification to first contributor if (params.notifycreator) { const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName')); thispage.setCallbackParameters(params); thispage.setLookupNonRedirectCreator(true); // Look for author of first non-redirect revision thispage.lookupCreation((pageobj) => { Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator()); }); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(params, null); } // Add AFD tag to article params.tagText = Twinkle.xfd.callbacks.afd.generateArticleTagWikitext( params.noinclude, params.outcome, params.afdtarget, params.number ); // If the selected outcome is merge, add {{Merge from}} to the target page if (params.outcome === 'merging' && params.afdtarget) { wikipedia_page = new Morebits.wiki.Page(params.afdtarget, 'Tagging the target page with {{Merge from}}'); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.tagTargetPageWithMergeFromTag); } if (pageobj.canEdit()) { // Remove some tags that should always be removed on AfD. text = text.replace(/\{\{\s*(dated prod|dated prod blp|Prod blp\/dated|Proposed deletion\/dated|prod2|Proposed deletion endorsed|Userspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, ''); // Then, test if there are speedy deletion-related templates on the article. const textNoSd = text.replace(/\{\{\s*(db(-\w*)?|delete|(?:hang|hold)[- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, ''); if (text !== textNoSd && confirm('A speedy deletion tag was found on this page. Should it be removed?')) { text = textNoSd; } // Insert tag after short description or any hatnotes const wikipage = new Morebits.wikitext.Page(text); text = wikipage.insertAfterTemplates(params.tagText, Twinkle.hatnoteRegex).getText(); pageobj.setPageText(text); pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].'); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('nocreate'); pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, tagTargetPageWithMergeFromTag: function(pageobj) { const statelem = pageobj.getStatusElement(); if (!pageobj.exists()) { statelem.warn('Failed. Target page not found.'); return; } else if (!pageobj.canEdit()) { statelem.warn('Failed. Target page is protected from editing.'); return; } const params = pageobj.getCallbackParameters(); const tag = `{{Merge from |1=${Morebits.pageNameNorm} |target=${params.afdtarget} |afd=${Morebits.pageNameNorm + params.numbering} |date ={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}} }}`; const wikipage = new Morebits.wikitext.Page(pageobj.getPageText()); const text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText(); pageobj.setPageText(text); pageobj.setEditSummary('Nominated for merging; see [[:' + params.discussionpage + ']].'); pageobj.setCreateOption('nocreate'); pageobj.save(); }, generateArticleTagWikitext: function(noinclude, outcome, afdtarget, number) { let noIncludeStart = ''; let noIncludeEnd = ''; if (noinclude) { noIncludeStart = '<noinclude>'; noIncludeEnd = '</noinclude>'; } let templateAndParams = ''; const outcomeParam = outcome !== 'deletion' ? '|outcome=' + outcome : ''; const targetParam = afdtarget ? '|target=' + afdtarget : ''; const isFirstNomination = number === ''; if (isFirstNomination) { templateAndParams = 'subst:afd|help=off' + outcomeParam + targetParam; } else { templateAndParams = 'subst:afdx|' + number + '|help=off' + outcomeParam + targetParam; } return noIncludeStart + '{{' + templateAndParams + '}}' + noIncludeEnd + '\n'; }, discussionPage: function(pageobj) { const params = pageobj.getCallbackParameters(); pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('afd', params)); pageobj.setEditSummary('Creating deletion discussion page for [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('createonly'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki // Actions that should wait on the discussion page actually being created // and whose errors shouldn't output the user rationale // List at deletion sorting pages if (params.delsortCats) { params.delsortCats.forEach((cat) => { const delsortPage = new Morebits.wiki.Page('Wikipedia:WikiProject Deletion sorting/' + cat, 'Adding to list of ' + cat + '-related deletion discussions'); delsortPage.setFollowRedirect(true); // In case a category gets renamed delsortPage.setCallbackParameters({discussionPage: params.discussionpage}); delsortPage.load(Twinkle.xfd.callbacks.afd.delsortListing); }); } }); }, todaysList: function(pageobj) { const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const added_data = '{{subst:afd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}\n'; let text; // add date header if the log is found to be empty (a bot should do this automatically) if (!pageobj.exists()) { text = '{{subst:AfD log}}\n' + added_data; } else { const old_text = pageobj.getPageText() + '\n'; // MW strips trailing blanks, but we like them, so we add a fake one text = old_text.replace(/(<!-- Add new entries to the TOP of the following list -->\n+)/, '$1' + added_data); if (text === old_text) { const linknode = document.createElement('a'); linknode.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing AFD') + '?action=purge'); linknode.appendChild(document.createTextNode('How to fix AFD')); statelem.error([ 'Could not find the target spot for the discussion. To fix this problem, please see ', linknode, '.' ]); return; } } pageobj.setPageText(text); pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchList')); pageobj.setCreateOption('recreate'); pageobj.setDiscussionToolsAutoSubscribe(false); pageobj.save(); }, delsortListing: function(pageobj) { const discussionPage = pageobj.getCallbackParameters().discussionPage; const text = pageobj.getPageText().replace('directly below this line -->', 'directly below this line -->\n{{' + discussionPage + '}}'); pageobj.setPageText(text); pageobj.setEditSummary('Listing [[:' + discussionPage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setCreateOption('nocreate'); pageobj.setDiscussionToolsAutoSubscribe(false); pageobj.save(); } }, tfd: { main: function(pageobj) { const params = pageobj.getCallbackParameters(); const date = new Morebits.Date(pageobj.getLoadTime()); params.logpage = 'Wikipedia:Templates for discussion/Log/' + date.format('YYYY MMMM D', 'utc'); params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm; // Add log/discussion page params to the already-loaded page object pageobj.setCallbackParameters(params); // Defined here rather than below to reduce duplication let watchModule, watch_query; if (params.scribunto) { const watchPref = Twinkle.getPref('xfdWatchPage'); // action=watch has no way to rely on user // preferences (T262912), so we do it manually. // The watchdefault pref appears to reliably return '1' (string), // but that's not consistent among prefs so might as well be "correct" watchModule = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10)); if (watchModule) { watch_query = { action: 'watch', titles: [ mw.config.get('wgPageName') ], token: mw.user.tokens.get('watchToken') }; // Only add the expiry if page is unwatched or already temporarily watched if (pageobj.getWatched() !== true && watchPref !== 'default' && watchPref !== 'yes') { watch_query.expiry = watchPref; } } } // Tagging template(s)/module(s) if (params.xfdcat === 'tfm') { // Merge let wikipedia_otherpage; if (params.scribunto) { wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName + '/doc', 'Tagging other module documentation with merge tag'); // Watch tagged module pages as well if (watchModule) { watch_query.titles.push(params.otherTemplateName); new Morebits.wiki.Api('Adding Modules to watchlist', watch_query).post(); } } else { wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName, 'Tagging other template with merge tag'); } // Tag this template/module Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge(pageobj); // Tag other template/module wikipedia_otherpage.setFollowRedirect(true); const otherParams = $.extend({}, params); otherParams.otherTemplateName = Morebits.pageNameNorm; wikipedia_otherpage.setCallbackParameters(otherParams); wikipedia_otherpage.load(Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge); } else { // delete if (params.scribunto && Twinkle.getPref('xfdWatchPage') !== 'no') { // Watch tagged module page as well if (watchModule) { new Morebits.wiki.Api('Adding Module to watchlist', watch_query).post(); } } Twinkle.xfd.callbacks.tfd.taggingTemplate(pageobj); } // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = params.logpage; Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log"; // Adding discussion const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.tfd.todaysList); // Notification to first contributors if (params.notifycreator) { const involvedpages = []; const seenusers = []; involvedpages.push(new Morebits.wiki.Page(mw.config.get('wgPageName'))); if (params.xfdcat === 'tfm') { if (params.scribunto) { involvedpages.push(new Morebits.wiki.Page('Module:' + params.tfdtarget)); } else { involvedpages.push(new Morebits.wiki.Page('Template:' + params.tfdtarget)); } } involvedpages.forEach((page) => { page.setCallbackParameters(params); page.lookupCreation((innerpage) => { const username = innerpage.getCreator(); if (!seenusers.includes(username)) { seenusers.push(username); // Only log once on merge nominations, for the initial template Twinkle.xfd.callbacks.notifyUser(innerpage.getCallbackParameters(), username, params.xfdcat === 'tfm' && innerpage.getPageName() !== Morebits.pageNameNorm); } }); }); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(params, null); } // Notify developer(s) of script(s) that use(s) the nominated template if (params.devpages) { const inCategories = mw.config.get('wgCategories'); const categoryNotificationPageMap = { 'Templates used by Twinkle': 'Wikipedia talk:Twinkle', 'Templates used by AutoWikiBrowser': 'Wikipedia talk:AutoWikiBrowser', 'Templates used by Ultraviolet': 'Wikipedia talk:Ultraviolet' }; $.each(categoryNotificationPageMap, (category, page) => { if (inCategories.includes(category)) { Twinkle.xfd.callbacks.notifyUser(params, page, true, 'Notifying ' + page + ' of template nomination'); } }); } }, taggingTemplate: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); params.tagText = '{{subst:template for discussion|help=off' + (params.templatetype !== 'standard' ? '|type=' + params.templatetype : '') + '}}'; if (pageobj.getContentModel() === 'sanitized-css') { params.tagText = '/* ' + params.tagText + ' */'; } else { if (params.noinclude) { params.tagText = '<noinclude>' + params.tagText + '</noinclude>'; } params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline } if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) { pageobj.setPageText(params.tagText + text); pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); if (params.scribunto) { pageobj.setCreateOption('recreate'); // Module /doc might not exist } pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, taggingTemplateForMerge: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); params.tagText = '{{subst:tfm|help=off|' + (params.templatetype !== 'standard' ? 'type=' + params.templatetype + '|' : '') + '1=' + params.otherTemplateName.replace(new RegExp('^' + Morebits.namespaceRegex([10, 828]) + ':'), '') + '}}'; if (pageobj.getContentModel() === 'sanitized-css') { params.tagText = '/* ' + params.tagText + ' */'; } else { if (params.noinclude) { params.tagText = '<noinclude>' + params.tagText + '</noinclude>'; } params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline } if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) { pageobj.setPageText(params.tagText + text); pageobj.setEditSummary('Listed for merging with [[:' + params.otherTemplateName + ']]; see [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); if (params.scribunto) { pageobj.setCreateOption('recreate'); // Module /doc might not exist } pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, todaysList: function(pageobj) { const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params); let text; // add date header if the log is found to be empty (a bot should do this automatically) if (!pageobj.exists()) { text = '{{subst:TfD log}}\n' + added_data; } else { const old_text = pageobj.getPageText(); text = old_text.replace('-->', '-->\n' + added_data); if (text === old_text) { statelem.error('failed to find target spot for the discussion'); return; } } pageobj.setPageText(text); pageobj.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Adding ' + (params.xfdcat === 'tfd' ? 'deletion nomination' : 'merge listing') + ' of [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('recreate'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); } }, mfd: { main: function(apiobj) { const response = apiobj.getResponse(); const titles = response.query.allpages; // There has been no earlier entries with this prefix, just go on. if (titles.length <= 0) { apiobj.params.numbering = apiobj.params.number = ''; } else { let number = 0; for (let i = 0; i < titles.length; ++i) { const title = titles[i].title; // First, simple test, is there an instance with this exact name? if (title === 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) { number = Math.max(number, 1); continue; } const order_re = new RegExp('^' + Morebits.string.escapeRegExp('Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) + '\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$'); const match = order_re.exec(title); // No match; A non-good value if (!match) { continue; } // A match, set number to the max of current number = Math.max(number, Number(match[1])); } apiobj.params.number = utils.num2order(parseInt(number, 10) + 1); apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : ''; } apiobj.params.discussionpage = 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering; apiobj.statelem.info('next in order is [[' + apiobj.params.discussionpage + ']]'); let wikipedia_page; // Tagging page if (mw.config.get('wgNamespaceNumber') !== 710) { // cannot tag TimedText pages wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging page with deletion tag'); wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.mfd.taggingPage); } // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage; Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page'; // Discussion page wikipedia_page = new Morebits.wiki.Page(apiobj.params.discussionpage, 'Creating deletion discussion page'); wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.mfd.discussionPage); // Today's list wikipedia_page = new Morebits.wiki.Page('Wikipedia:Miscellany for deletion', "Adding discussion to today's list"); wikipedia_page.setPageSection(2); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.mfd.todaysList); // Notification to first contributor and/or notification to owner of userspace if (apiobj.params.notifycreator || apiobj.params.notifyuserspace) { const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName')); thispage.setCallbackParameters(apiobj.params); thispage.lookupCreation(Twinkle.xfd.callbacks.mfd.sendNotifications); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(apiobj.params, null); } }, taggingPage: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); params.tagText = '{{' + (params.number === '' ? 'mfd' : 'mfdx|' + params.number) + '|help=off}}'; if (['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'))) { params.tagText = '/* ' + params.tagText + ' */\n'; } else { params.tagText += '\n'; if (params.noinclude) { params.tagText = '<noinclude>' + params.tagText + '</noinclude>'; } } if (pageobj.canEdit() && ['wikitext', 'javascript', 'css', 'sanitized-css'].includes(pageobj.getContentModel())) { pageobj.setPageText(params.tagText + text); pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('nocreate'); pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, discussionPage: function(pageobj) { const params = pageobj.getCallbackParameters(); pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('mfd', params)); pageobj.setEditSummary('Creating deletion discussion page for [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('createonly'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); }, todaysList: function(pageobj) { let text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const date = new Morebits.Date(pageobj.getLoadTime()); const date_header = date.format('===MMMM D, YYYY===\n', 'utc'); const date_header_regex = new RegExp(date.format('(===[\\s]*MMMM[\\s]+D,[\\s]+YYYY[\\s]*===)', 'utc')); const added_data = '{{subst:mfd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}'; if (date_header_regex.test(text)) { // we have a section already statelem.info('Found today\'s section, proceeding to add new entry'); text = text.replace(date_header_regex, '$1\n' + added_data); } else { // we need to create a new section statelem.info('No section for today found, proceeding to create one'); text = text.replace('===', date_header + added_data + '\n\n==='); } pageobj.setPageText(text); pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchList')); pageobj.setCreateOption('recreate'); pageobj.setDiscussionToolsAutoSubscribe(false); pageobj.save(); }, sendNotifications: function(pageobj) { const initialContrib = pageobj.getCreator(); const params = pageobj.getCallbackParameters(); // Notify the creator if (params.notifycreator) { Twinkle.xfd.callbacks.notifyUser(params, initialContrib); } // Notify the user who owns the subpage if they are not the creator params.userspaceOwner = mw.config.get('wgRelevantUserName'); if (params.notifyuserspace) { if (params.userspaceOwner !== initialContrib) { // Don't log if notifying creator above, will log then Twinkle.xfd.callbacks.notifyUser(params, params.userspaceOwner, params.notifycreator, 'Notifying owner of userspace (' + params.userspaceOwner + ')'); } else if (!params.notifycreator) { // If we thought we would notify the owner but didn't, // then we need to log if we didn't notify the creator // Twinkle.xfd.callbacks.addToLog(params, null); Twinkle.xfd.callbacks.addToLog(params, initialContrib); } } } }, ffd: { taggingImage: function(pageobj) { let text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); const date = new Morebits.Date(pageobj.getLoadTime()).format('YYYY MMMM D', 'utc'); params.logpage = 'Wikipedia:Files for discussion/' + date; params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm; params.tagText = '{{ffd|log=' + date + '|help=off}}\n'; if (pageobj.canEdit()) { text = Twinkle.removeMoveToCommonsTagsFromWikicode( text ); pageobj.setPageText(params.tagText + text); pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('recreate'); // it might be possible for a file to exist without a description page pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = params.logpage; Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page'; // Contributor specific edits const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName')); wikipedia_page.setCallbackParameters(params); wikipedia_page.lookupCreation(Twinkle.xfd.callbacks.ffd.main); }, main: function(pageobj) { // this is coming in from lookupCreation...! const params = pageobj.getCallbackParameters(); const initialContrib = pageobj.getCreator(); params.uploader = initialContrib; // Adding discussion const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.ffd.todaysList); // Notification to first contributor if (params.notifycreator) { Twinkle.xfd.callbacks.notifyUser(params, initialContrib); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(params, null); } }, todaysList: function(pageobj) { let text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); // add date header if the log is found to be empty (a bot should do this automatically) if (!pageobj.exists()) { text = '{{subst:FfD log}}'; } pageobj.setPageText(text + '\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('ffd', params)); pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('recreate'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); } }, cfd: { main: function(pageobj) { const params = pageobj.getCallbackParameters(); const date = new Morebits.Date(pageobj.getLoadTime()); params.logpage = 'Wikipedia:Categories for discussion/Log/' + date.format('YYYY MMMM D', 'utc'); params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm; // Add log/discussion page params to the already-loaded page object pageobj.setCallbackParameters(params); // Tagging category Twinkle.xfd.callbacks.cfd.taggingCategory(pageobj); // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = params.logpage; Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log"; // Adding discussion to list let wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.cfd.todaysList); // Notification to first contributor if (params.notifycreator) { wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName')); wikipedia_page.setCallbackParameters(params); wikipedia_page.lookupCreation((pageobj) => { Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator()); }); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(params, null); } }, taggingCategory: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); params.tagText = '{{subst:' + params.xfdcat; let editsummary = (mw.config.get('wgNamespaceNumber') === 14 ? 'Category' : 'Stub template') + ' being considered for ' + params.action; switch (params.xfdcat) { case 'cfd': case 'sfd-t': break; case 'cfc': editsummary += ' to an article'; // falls through case 'cfm': case 'cfr': case 'sfr-t': params.tagText += '|' + params.cfdtarget; break; case 'cfs': params.tagText += '|' + params.cfdtarget + '|' + params.cfdtarget2; break; default: alert('twinklexfd in taggingCategory(): unknown CFD action'); break; } params.tagText += '}}\n'; editsummary += '; see [[:' + params.discussionpage + ']].'; if (pageobj.canEdit()) { pageobj.setPageText(params.tagText + text); pageobj.setEditSummary(editsummary); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, todaysList: function(pageobj) { const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params); let text; // add date header if the log is found to be empty (a bot should do this automatically) if (!pageobj.exists()) { text = '{{subst:CfD log}}\n' + added_data; } else { const old_text = pageobj.getPageText(); text = old_text.replace('below this line -->', 'below this line -->\n' + added_data); if (text === old_text) { statelem.error('failed to find target spot for the discussion'); return; } } pageobj.setPageText(text); pageobj.setEditSummary('Adding ' + params.action + ' nomination of [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('recreate'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); } }, cfds: { taggingCategory: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); if (params.xfdcat === 'C2F') { params.tagText = '{{subst:cfm-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n'; } else { params.tagText = '{{subst:cfr-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n'; } params.discussionpage = ''; // CFDS is just a bullet in a bulleted list. There's no section to link to, so we set this to blank. Blank will be recognized by both the generate userspace log code and the generate userspace log edit summary code as "don't wikilink to a section". if (pageobj.canEdit()) { pageobj.setPageText(params.tagText + text); pageobj.setEditSummary('Listed for speedy renaming; see [[WP:CFDS|Categories for discussion/Speedy]].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title pageobj.save(() => { // No user notification for CfDS, so just add this nomination to the user's userspace log Twinkle.xfd.callbacks.addToLog(params, null); }); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); // No user notification for CfDS, so just add this nomination to the user's userspace log Twinkle.xfd.callbacks.addToLog(params, null); } }, addToList: function(pageobj) { const old_text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const text = old_text.replace('BELOW THIS LINE -->', 'BELOW THIS LINE -->\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('cfds', params)); if (text === old_text) { statelem.error('failed to find target spot for the discussion'); return; } pageobj.setPageText(text); pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('recreate'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); } }, rfd: { // This gets called both on submit and preview to determine the redirect target findTarget: function(params, callback) { // Used by regular redirects to find the target, but for all redirects, // avoid relying on the client clock to build the log page const query = { action: 'query', curtimestamp: true, format: 'json' }; if (document.getElementById('softredirect')) { // For soft redirects, define the target early // to skip target checks in findTargetCallback params.rfdtarget = document.getElementById('softredirect').textContent.replace(/^:+/, ''); } else { // Find current target of redirect query.titles = mw.config.get('wgPageName'); query.redirects = true; } const wikipedia_api = new Morebits.wiki.Api('Finding target of redirect', query, Twinkle.xfd.callbacks.rfd.findTargetCallback(callback)); wikipedia_api.params = params; wikipedia_api.post(); }, // This is a closure for the callback from the above API request, which gets the target of the redirect findTargetCallback: function(callback) { return function(apiobj) { const response = apiobj.getResponse(); apiobj.params.curtimestamp = response.curtimestamp; if (!apiobj.params.rfdtarget) { // Not a softredirect const target = response.query.redirects && response.query.redirects[0].to; if (!target) { let message = 'No target found. this page does not appear to be a redirect, aborting'; if (mw.config.get('wgAction') === 'history') { message += '. If this is a soft redirect, try again from the content page, not the page history.'; } apiobj.statelem.error(message); return; } apiobj.params.rfdtarget = target; const section = response.query.redirects[0].tofragment; apiobj.params.section = section; } callback(apiobj.params); }; }, main: function(params) { const date = new Morebits.Date(params.curtimestamp); params.logpage = 'Wikipedia:Redirects for discussion/Log/' + date.format('YYYY MMMM D', 'utc'); params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm; // Tagging redirect let wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to redirect'); wikipedia_page.setFollowRedirect(false); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.rfd.taggingRedirect); // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = params.logpage; Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log"; // Adding discussion wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.rfd.todaysList); // Notifications if (params.notifycreator || params.relatedpage) { const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName')); thispage.setCallbackParameters(params); thispage.lookupCreation(Twinkle.xfd.callbacks.rfd.sendNotifications); // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name } else { Twinkle.xfd.callbacks.addToLog(params, null); } }, taggingRedirect: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); // Imperfect for edit request but so be it params.tagText = '{{subst:rfd|' + (mw.config.get('wgNamespaceNumber') === 10 ? 'showontransclusion=1|' : '') + 'content=\n'; if (pageobj.canEdit()) { pageobj.setPageText(params.tagText + text + '\n}}'); pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage')); pageobj.setCreateOption('nocreate'); pageobj.save(); } else { Twinkle.xfd.callbacks.autoEditRequest(pageobj, params); } }, todaysList: function(pageobj) { const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext('rfd', params); let text; // add date header if the log is found to be empty (a bot should do this automatically) if (!pageobj.exists()) { text = '{{subst:RfD log}}' + added_data; } else { const old_text = pageobj.getPageText(); text = old_text.replace(/(<!-- Add new entries directly below this line\.? -->)/, '$1\n' + added_data); if (text === old_text) { statelem.error('failed to find target spot for the discussion'); return; } } pageobj.setPageText(text); pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.setCreateOption('recreate'); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); }, sendNotifications: function(pageobj) { const initialContrib = pageobj.getCreator(); const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); // Notifying initial contributor if (params.notifycreator) { Twinkle.xfd.callbacks.notifyUser(params, initialContrib); } // Notifying target page's watchers, if not a soft redirect if (params.relatedpage) { const targetTalk = new mw.Title(params.rfdtarget).getTalkPage(); // On the offchance it's a circular redirect if (params.rfdtarget === mw.config.get('wgPageName')) { statelem.warn('Circular redirect; skipping target page notification'); } else if (document.getElementById('softredirect')) { statelem.warn('Soft redirect; skipping target page notification'); // Don't issue if target talk is the initial contributor's talk or your own } else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === initialContrib) { statelem.warn('Target is initial contributor; skipping target page notification'); } else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === mw.config.get('wgUserName')) { statelem.warn('You (' + mw.config.get('wgUserName') + ') are the target; skipping target page notification'); } else { // Don't log if notifying creator above, will log then Twinkle.xfd.callbacks.notifyUser(params, targetTalk.toText(), params.notifycreator, 'Notifying redirect target of the discussion'); return; } // If we thought we would notify the target but didn't, // we need to log if we didn't notify the creator if (!params.notifycreator) { Twinkle.xfd.callbacks.addToLog(params, null); } } } }, rm: { listAtTalk: function(pageobj) { const params = pageobj.getCallbackParameters(); params.discussionpage = pageobj.getPageName(); pageobj.setAppendText('\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params)); pageobj.setEditSummary(`Proposing move of ${ params.currentname .map((currentname, i) => `[[:${currentname}]]${params.newname[i] ? ` to [[:${params.newname[i]}]]` : ''}`) .join(', ') }.`); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setCreateOption('recreate'); // since the talk page need not exist pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion')); pageobj.append(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki // add this nomination to the user's userspace log Twinkle.xfd.callbacks.addToLog(params, null); }); }, listAtRMTR: function(pageobj) { const text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); const statelem = pageobj.getStatusElement(); const discussionWikitext = Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params); const newtext = Twinkle.xfd.insertRMTR(text, discussionWikitext); if (text === newtext) { statelem.error('failed to find target spot for the entry'); return; } pageobj.setPageText(newtext); pageobj.setEditSummary(`Adding [[:${params.currentname.join(']], [[:')}]].`); pageobj.setChangeTags(Twinkle.changeTags); pageobj.save(() => { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki // add this nomination to the user's userspace log Twinkle.xfd.callbacks.addToLog(params, null); }); } } }; /** * Given the wikitext of the WP:RM/TR page and the wikitext to insert, insert it at the bottom of the ==== Uncontroversial technical requests ==== section. * * @param {string} pageWikitext * @param {string} wikitextToInsert Will typically be `{{subst:RMassist|1=From|2=To|reason=Reason}}`, which expands out to `* {{RMassist/core | 1 = From | 2 = To | discuss = yes | reason = Reason | sig = Signature | requester = YourUserName}}` * @return {string} pageWikitext */ Twinkle.xfd.insertRMTR = function(pageWikitext, wikitextToInsert) { const placementRE = /\n{1,}(==== ?Requests to revert undiscussed moves ?====)/i; return pageWikitext.replace(placementRE, '\n' + wikitextToInsert + '\n\n$1'); }; Twinkle.xfd.callback.evaluate = function(e) { const form = e.target; const params = Morebits.QuickForm.getInputData(form); Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); Twinkle.xfd.currentRationale = params.reason; Morebits.Status.onError(Twinkle.xfd.printRationale); let query, wikipedia_page, wikipedia_api; switch (params.venue) { case 'afd': // AFD query = { action: 'query', list: 'allpages', apprefix: 'Articles for deletion/' + Morebits.pageNameNorm, apnamespace: 4, apfilterredir: 'nonredirects', aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops format: 'json' }; wikipedia_api = new Morebits.wiki.Api('Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main); wikipedia_api.params = params; wikipedia_api.post(); break; case 'tfd': // TFD if (params.tfdtarget) { // remove namespace name params.tfdtarget = utils.stripNs(params.tfdtarget); } // Modules can't be tagged, TfD instructions are to place on /doc subpage params.scribunto = mw.config.get('wgPageContentModel') === 'Scribunto'; if (params.xfdcat === 'tfm') { // Merge // Tag this template/module if (params.scribunto) { wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging this module documentation with merge tag'); params.otherTemplateName = 'Module:' + params.tfdtarget; } else { wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging this template with merge tag'); params.otherTemplateName = 'Template:' + params.tfdtarget; } } else { // delete if (params.scribunto) { wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging module documentation with deletion tag'); } else { wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging template with deletion tag'); } } wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.tfd.main); break; case 'mfd': // MFD query = { action: 'query', list: 'allpages', apprefix: 'Miscellany for deletion/' + Morebits.pageNameNorm, apnamespace: 4, apfilterredir: 'nonredirects', aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops format: 'json' }; wikipedia_api = new Morebits.wiki.Api('Looking for prior nominations of this page', query, Twinkle.xfd.callbacks.mfd.main); wikipedia_api.params = params; wikipedia_api.post(); break; case 'ffd': // FFD // Tagging file // A little out of order with this coming before 'main', // but tagging doesn't need the uploader parameter, // while everything else does, so tag then get the uploader wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to file page'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.ffd.taggingImage); break; case 'cfd': if (params.cfdtarget) { params.cfdtarget = utils.stripNs(params.cfdtarget); } else { params.cfdtarget = ''; // delete } if (params.cfdtarget2) { // split params.cfdtarget2 = utils.stripNs(params.cfdtarget2); } // Used for customized actions in edit summaries and the notification template var summaryActions = { cfd: 'deletion', 'sfd-t': 'deletion', cfm: 'merging', cfr: 'renaming', 'sfr-t': 'renaming', cfs: 'splitting', cfc: 'conversion' }; params.action = summaryActions[params.xfdcat]; // Tagging category wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with ' + params.action + ' tag'); wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.cfd.main); break; case 'cfds': // add namespace name if missing params.cfdstarget = utils.addNs(params.cfdstarget, 14); var logpage = 'Wikipedia:Categories for discussion/Speedy'; // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = logpage; Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page'; // Tagging category wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with rename tag'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.cfds.taggingCategory); // Adding discussion to list wikipedia_page = new Morebits.wiki.Page(logpage, 'Adding discussion to the list'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.cfds.addToList); break; case 'rfd': // find target and pass main as the callback Twinkle.xfd.callbacks.rfd.findTarget(params, Twinkle.xfd.callbacks.rfd.main); break; case 'rm': var nomPageName = params.rmtr ? 'Wikipedia:Requested moves/Technical requests' : new mw.Title(Morebits.pageNameNorm).getTalkPage().toText(); Morebits.wiki.actionCompleted.redirect = nomPageName; Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page'; wikipedia_page = new Morebits.wiki.Page(nomPageName, params.rmtr ? 'Adding entry at WP:RM/TR' : 'Adding entry on talk page'); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); if (params.rmtr) { wikipedia_page.load(Twinkle.xfd.callbacks.rm.listAtRMTR); } else { // listAtTalk uses .append(), so no need to load the page Twinkle.xfd.callbacks.rm.listAtTalk(wikipedia_page); } break; default: alert('twinklexfd: unknown XFD discussion venue'); break; } }; Twinkle.addInitCallback(Twinkle.xfd, 'xfd'); }()); // </nowiki> k9077j6ndeytmp74u9yl2dxaqw885t0 MediaWiki:Gadget-twinklerollback.js 8 24478 268715 2026-04-27T16:47:03Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinklerollback.js: Revert/rollback module **************************************** * Mode of invocation: Links on contributions, recent changes, history, and diff pages * Active on: Diff pages, history pages, Special:RecentChanges(Linked), and Special:Contributions */ /** * Twinklerollback revert and antivandalism utility */ Twinkle.r...' 268715 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinklerollback.js: Revert/rollback module **************************************** * Mode of invocation: Links on contributions, recent changes, history, and diff pages * Active on: Diff pages, history pages, Special:RecentChanges(Linked), and Special:Contributions */ /** * Twinklerollback revert and antivandalism utility */ Twinkle.rollback = function twinklerollback() { // Only proceed if the user can actually edit the page in question // (see #632 for contribs issue). wgIsProbablyEditable should take // care of namespace/contentModel restrictions as well as explicit // protections; it won't take care of cascading or TitleBlacklist. if (mw.config.get('wgIsProbablyEditable')) { if (mw.config.get('wgAction') === 'view' && mw.config.get('wgRevisionId') && mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId')) { Twinkle.rollback.addLinks.oldid(); } else if (mw.config.get('wgAction') === 'history' && mw.config.get('wgArticleId')) { Twinkle.rollback.addLinks.history(); } } else if (mw.config.get('wgNamespaceNumber') === -1) { Twinkle.rollback.skipTalk = !Twinkle.getPref('openTalkPageOnAutoRevert'); Twinkle.rollback.rollbackInPlace = Twinkle.getPref('rollbackInPlace'); switch (mw.config.get('wgCanonicalSpecialPageName')) { case 'Contributions': case 'IPContributions': Twinkle.rollback.addLinks.contributions(); break; case 'Recentchanges': case 'Recentchangeslinked': // Reload with recent changes updates // structuredChangeFilters.ui.initialized is just on load mw.hook('wikipage.content').add(($context) => { if (!$context || !$context.is('div')) { return; } Twinkle.rollback.addLinks.recentchanges($context); }); break; } } // Reload when revision slider or other scripts dynamically load diff content. mw.hook('wikipage.diff').add(($context) => { if (!$context) { return; } // Only proceed if the user can actually edit the page in question, // wgDiffOldId included for clarity in if else loop [[phab:T214985]] if (mw.config.get('wgIsProbablyEditable') && (mw.config.get('wgDiffNewId') || mw.config.get('wgDiffOldId'))) { Twinkle.rollback.addLinks.diff($context); } }); }; // A list of usernames, usually only bots, that vandalism revert is jumped // over; that is, if vandalism revert was chosen on such username, then its // target is on the revision before. This is for handling quick bots that // makes edits seconds after the original edit is made. This only affects // vandalism rollback; for good faith rollback, it will stop, indicating a bot // has no faith, and for normal rollback, it will rollback that edit. Twinkle.rollback.trustedBots = ['AnomieBOT', 'SineBot', 'MajavahBot']; Twinkle.rollback.skipTalk = null; Twinkle.rollback.rollbackInPlace = null; // String to insert when a username is hidden Twinkle.rollback.hiddenName = 'an unknown user'; // Consolidated construction of rollback links Twinkle.rollback.linkBuilder = { spanTag: function(color, content) { const span = document.createElement('span'); span.style.color = color; span.appendChild(document.createTextNode(content)); return span; }, buildLink: function(color, text) { const link = document.createElement('a'); link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', '[')); link.appendChild(Twinkle.rollback.linkBuilder.spanTag(color, text)); link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', ']')); link.href = '#'; return link; }, /** * @param {string} [vandal=null] - Username of the editor being reverted * Provide a falsey value if the username is hidden, defaults to null * @param {boolean} inline - True to create two links in a span, false * to create three links in a div (optional) * @param {number|string} [rev=wgCurRevisionId] - Revision ID being reverted (optional) * @param {string} [page=wgPageName] - Page being reverted (optional) */ rollbackLinks: function(vandal, inline, rev, page) { vandal = vandal || null; const elem = inline ? 'span' : 'div'; const revNode = document.createElement(elem); rev = parseInt(rev, 10); if (rev) { revNode.setAttribute('id', 'tw-revert' + rev); } else { revNode.setAttribute('id', 'tw-revert'); } const separator = inline ? ' ' : ' || '; const sepNode1 = document.createElement('span'); const sepText = document.createTextNode(separator); sepNode1.setAttribute('class', 'tw-rollback-link-separator'); sepNode1.appendChild(sepText); const sepNode2 = sepNode1.cloneNode(true); const normNode = document.createElement('span'); const vandNode = document.createElement('span'); const normLink = Twinkle.rollback.linkBuilder.buildLink('SteelBlue', 'rollback'); const vandLink = Twinkle.rollback.linkBuilder.buildLink('Red', 'vandalism'); normLink.style.fontWeight = 'bold'; vandLink.style.fontWeight = 'bold'; $(normLink).on('click', (e) => { e.preventDefault(); Twinkle.rollback.revert('norm', vandal, rev, page); Twinkle.rollback.disableLinks(revNode); }); $(vandLink).on('click', (e) => { e.preventDefault(); Twinkle.rollback.revert('vand', vandal, rev, page); Twinkle.rollback.disableLinks(revNode); }); normNode.setAttribute('class', 'tw-rollback-link-normal'); vandNode.setAttribute('class', 'tw-rollback-link-vandalism'); normNode.appendChild(sepNode1); vandNode.appendChild(sepNode2); normNode.appendChild(normLink); vandNode.appendChild(vandLink); if (!inline) { const agfNode = document.createElement('span'); const agfLink = Twinkle.rollback.linkBuilder.buildLink('DarkOliveGreen', 'rollback (AGF)'); $(agfLink).on('click', (e) => { e.preventDefault(); Twinkle.rollback.revert('agf', vandal, rev, page); // Twinkle.rollback.disableLinks(revNode); // rollbackInPlace not relevant for any inline situations }); agfNode.setAttribute('class', 'tw-rollback-link-agf'); agfLink.style.fontWeight = 'bold'; agfNode.appendChild(agfLink); revNode.appendChild(agfNode); } revNode.appendChild(normNode); revNode.appendChild(vandNode); return revNode; }, // Build [restore this revision] links restoreThisRevisionLink: function(revisionRef, inline) { // If not a specific revision number, should be wgDiffNewId/wgDiffOldId/wgRevisionId revisionRef = typeof revisionRef === 'number' ? revisionRef : mw.config.get(revisionRef); const elem = inline ? 'span' : 'div'; const revertToRevisionNode = document.createElement(elem); revertToRevisionNode.setAttribute('id', 'tw-revert-to-' + revisionRef); revertToRevisionNode.style.fontWeight = 'bold'; const revertToRevisionLink = Twinkle.rollback.linkBuilder.buildLink('SaddleBrown', 'restore this version'); $(revertToRevisionLink).on('click', (e) => { e.preventDefault(); Twinkle.rollback.revertToRevision(revisionRef); }); if (inline) { revertToRevisionNode.appendChild(document.createTextNode(' ')); } revertToRevisionNode.appendChild(revertToRevisionLink); return revertToRevisionNode; } }; Twinkle.rollback.addLinks = { contributions: function() { // $('sp-contributions-footer-anon-range') relies on the fmbox // id in [[MediaWiki:Sp-contributions-footer-anon-range]] and // is used to show rollback/vandalism links for IP ranges const isRange = !!$('#sp-contributions-footer-anon-range')[0]; if (mw.config.exists('wgRelevantUserName') || isRange) { // Get the username these contributions are for let username = mw.config.get('wgRelevantUserName'); if (Twinkle.getPref('showRollbackLinks').includes('contribs') || (mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').includes('others')) || (mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').includes('mine'))) { const $list = $('#mw-content-text').find('ul li:has(span.mw-uctop):has(.mw-changeslist-diff)'); $list.each((key, current) => { // revid is also available in the href of both // .mw-changeslist-date or .mw-changeslist-diff const page = $(current).find('.mw-contributions-title').text(); // Get username for IP ranges (wgRelevantUserName is null) if (isRange) { // The :not is possibly unnecessary, as it appears that // .mw-userlink is simply not present if the username is hidden username = $(current).find('.mw-userlink:not(.history-deleted)').text(); } // It's unlikely, but we can't easily check for revdel'd usernames // since only a strong element is provided, with no easy selector [[phab:T255903]] current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(username, true, current.dataset.mwRevid, page)); }); } } }, recentchanges: function($context) { if (Twinkle.getPref('showRollbackLinks').includes('recent')) { // Latest and revertable (not page creations, logs, categorizations, etc.) const selector = '.mw-changeslist-last.mw-changeslist-src-mw-edit'; let $list = $context.hasClass('mw-changeslist') ? $context.find(selector) : $context.find('.mw-changeslist ' + selector); if (!$list.length) { return; } // Exclude top-level header if "group changes" preference is used // and find only individual lines or nested lines $list = $list.not('.mw-rcfilters-ui-highlights-enhanced-toplevel').find('.mw-changeslist-line-inner, td.mw-enhanced-rc-nested'); $list.each((key, current) => { // The :not is possibly unnecessary, as it appears that // .mw-userlink is simply not present if the username is hidden const vandal = $(current).find('.mw-userlink:not(.history-deleted)').text(); const href = $(current).find('.mw-changeslist-diff').attr('href'); const rev = mw.util.getParamValue('diff', href); const page = current.dataset.targetPage; current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(vandal, true, rev, page)); }); } }, history: function() { if (Twinkle.getPref('showRollbackLinks').includes('history')) { // All revs const histList = $('#pagehistory li').toArray(); // On first page of results, so add revert/rollback // links to the top revision if (!$('a.mw-firstlink').length) { const firstRow = histList.shift(); const firstUser = $(firstRow).find('.mw-userlink:not(.history-deleted)').text(); // Check for first username different than the top user, // only apply rollback links if/when found // for() faster than every() for (let i = 0; i < histList.length; i++) { const hasMoreThanOneUser = $(histList[i]).find('.mw-userlink').text() !== firstUser; if (hasMoreThanOneUser) { firstRow.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(firstUser, true)); break; } } } // oldid histList.forEach((rev) => { // From restoreThisRevision, non-transferable // If the text has been revdel'd, it gets wrapped in a span with .history-deleted, // and href will be undefined (and thus oldid is NaN) const href = rev.querySelector('.mw-changeslist-date').href; const oldid = parseInt(mw.util.getParamValue('oldid', href), 10); if (!isNaN(oldid)) { rev.appendChild(Twinkle.rollback.linkBuilder.restoreThisRevisionLink(oldid, true)); } }); } }, diff: function($context) { // Autofill user talk links on diffs with vanarticle for easy warning, but don't autowarn const warnFromTalk = function(xtitle) { const $talkLink = $context.find('#mw-diff-' + xtitle + '2 .mw-usertoollinks a').first(); if ($talkLink.length) { let extraParams = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm) + '&noautowarn=true'; // diffIDs for vanarticlerevid extraParams += '&vanarticlerevid='; extraParams += xtitle === 'otitle' ? mw.config.get('wgDiffOldId') : mw.config.get('wgDiffNewId'); const href = $talkLink.attr('href'); if (!href.includes('?')) { $talkLink.attr('href', href + '?' + extraParams); } else { $talkLink.attr('href', href + '&' + extraParams); } } }; // Older revision warnFromTalk('otitle'); // Add quick-warn link to user talk link // Don't load if there's a single revision or weird diff (cur on latest) if (mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId'))) { // Add a [restore this revision] link to the older revision const oldTitle = $context.find('#mw-diff-otitle1').parent().get(0); if (oldTitle) { oldTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffOldId'), oldTitle.firstChild); } } // Newer revision warnFromTalk('ntitle'); // Add quick-warn link to user talk link // Add either restore or rollback links to the newer revision // Don't show if there's a single revision or weird diff (prev on first) if ($context.find('#differences-nextlink').length) { // Not latest revision, add [restore this revision] link to newer revision const newTitle = $context.find('#mw-diff-ntitle1').parent().get(0); if (newTitle) { newTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffNewId'), newTitle.firstChild); } } else if (Twinkle.getPref('showRollbackLinks').includes('diff') && mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId') || $context.find('#differences-prevlink').length)) { // Normally .mw-userlink is a link, but if the // username is hidden, it will be a span with // .history-deleted as well. When a sysop views the // hidden content, the span contains the username in a // link element, which will *just* have // .mw-userlink. The below thus finds the first // instance of the class, which if hidden is the span // and thus text returns undefined. Technically, this // is a place where sysops *could* have more // information available to them (as above, via // &unhide=1), since the username will be available by // checking a.mw-userlink instead, but revert() will // need reworking around userHidden let vandal = $context.find('#mw-diff-ntitle2').find('.mw-userlink')[0]; // See #1337 vandal = vandal ? vandal.text : ''; const ntitle = $context.find('#mw-diff-ntitle1').parent().get(0); if (ntitle) { ntitle.insertBefore(Twinkle.rollback.linkBuilder.rollbackLinks(vandal), ntitle.firstChild); } } }, oldid: function() { // Add a [restore this revision] link on old revisions const revisionInfo = document.getElementById('mw-revision-info'); if (revisionInfo) { const title = revisionInfo.parentNode; title.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgRevisionId'), title.firstChild); } } }; Twinkle.rollback.disableLinks = function disablelinks(parentNode) { $(parentNode).children().each((_ix, node) => { node.innerHTML = node.textContent; // Feels like cheating $(node).css('font-weight', 'normal').css('color', 'darkgray'); }); }; Twinkle.rollback.revert = function revertPage(type, vandal, rev, page) { if (mw.util.isIPv6Address(vandal)) { vandal = Morebits.ip.sanitizeIPv6(vandal); } const pagename = page || mw.config.get('wgPageName'); const revid = rev || mw.config.get('wgCurRevisionId'); if (Twinkle.rollback.rollbackInPlace) { const notifyStatus = document.createElement('span'); mw.notify(notifyStatus, { autoHide: false, title: 'Rollback on ' + page, tag: 'twinklerollback_' + rev // Shouldn't be necessary given disableLink }); Morebits.Status.init(notifyStatus); } else { Morebits.Status.init(document.getElementById('mw-content-text')); $('#catlinks').remove(); } const params = { type: type, user: vandal, userHidden: !vandal, // Keep track of whether the username was hidden pagename: pagename, revid: revid }; const query = { action: 'query', prop: ['info', 'revisions', 'flagged'], titles: pagename, inprop: 'watched', intestactions: 'edit', rvlimit: Twinkle.getPref('revertMaxRevisions'), rvprop: [ 'ids', 'timestamp', 'user' ], curtimestamp: '', meta: 'tokens', type: 'csrf', format: 'json' }; const wikipedia_api = new Morebits.wiki.Api('Grabbing data of earlier revisions', query, Twinkle.rollback.callbacks.main); wikipedia_api.params = params; wikipedia_api.post(); }; Twinkle.rollback.revertToRevision = function revertToRevision(oldrev) { Morebits.Status.init(document.getElementById('mw-content-text')); const query = { action: 'query', prop: ['info', 'revisions'], titles: mw.config.get('wgPageName'), inprop: 'watched', rvlimit: 1, rvstartid: oldrev, rvprop: [ 'ids', 'user' ], curtimestamp: '', meta: 'tokens', type: 'csrf', format: 'json' }; const wikipedia_api = new Morebits.wiki.Api('Grabbing data of the earlier revision', query, Twinkle.rollback.callbacks.toRevision); wikipedia_api.params = { rev: oldrev }; wikipedia_api.post(); }; Twinkle.rollback.callbacks = { toRevision: function(apiobj) { const response = apiobj.getResponse(); const loadtimestamp = response.curtimestamp; const csrftoken = response.query.tokens.csrftoken; const page = response.query.pages[0]; const lastrevid = parseInt(page.lastrevid, 10); const touched = page.touched; const rev = page.revisions[0]; const revertToRevID = parseInt(rev.revid, 10); const revertToUser = rev.user; const revertToUserHidden = !!rev.userhidden; if (revertToRevID !== apiobj.params.rev) { apiobj.statelem.error('The retrieved revision does not match the requested revision. Stopping revert.'); return; } const optional_summary = prompt('Please specify a reason for the revert: ', ''); // padded out to widen prompt in Firefox if (optional_summary === null) { apiobj.statelem.error('Aborted by user.'); return; } const summary = Twinkle.rollback.formatSummary('Restored revision ' + revertToRevID + ' by $USER', revertToUserHidden ? null : revertToUser, optional_summary); const query = { action: 'edit', title: mw.config.get('wgPageName'), summary: summary, tags: Twinkle.changeTags, token: csrftoken, undo: lastrevid, undoafter: revertToRevID, basetimestamp: touched, starttimestamp: loadtimestamp, minor: Twinkle.getPref('markRevertedPagesAsMinor').includes('torev') ? true : undefined, format: 'json' }; // Handle watching, possible expiry if (Twinkle.getPref('watchRevertedPages').includes('torev')) { const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry'); if (!watchOrExpiry || watchOrExpiry === 'no') { query.watchlist = 'nochange'; } else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') { query.watchlist = 'preferences'; } else { query.watchlist = 'watch'; // number allowed but not used in Twinkle.config.watchlistEnums if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') { query.watchlistexpiry = watchOrExpiry; } } } Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = 'Reversion completed'; const wikipedia_api = new Morebits.wiki.Api('Saving reverted contents', query, Twinkle.rollback.callbacks.complete, apiobj.statelem); wikipedia_api.params = apiobj.params; wikipedia_api.post(); }, main: function(apiobj) { const response = apiobj.getResponse(); const loadtimestamp = response.curtimestamp; const csrftoken = response.query.tokens.csrftoken; const page = response.query.pages[0]; if (!page.actions.edit) { apiobj.statelem.error("Unable to edit the page, it's probably protected."); return; } const lastrevid = parseInt(page.lastrevid, 10); const touched = page.touched; const revs = page.revisions; const statelem = apiobj.statelem; const params = apiobj.params; if (revs.length < 1) { statelem.error('We have less than one additional revision, thus impossible to revert.'); return; } const top = revs[0]; const lastuser = top.user; if (lastrevid < params.revid) { Morebits.Status.error('Error', [ 'The most recent revision ID received from the server, ', Morebits.htmlNode('strong', lastrevid), ', is less than the ID of the displayed revision. This could indicate that the current revision has been deleted, the server is lagging, or that bad data has been received. Stopping revert.' ]); return; } // Used for user-facing alerts, messages, etc., not edits or summaries let userNorm = params.user || Twinkle.rollback.hiddenName; let index = 1; if (params.revid !== lastrevid) { Morebits.Status.warn('Warning', [ 'Latest revision ', Morebits.htmlNode('strong', lastrevid), ' doesn\'t equal our revision ', Morebits.htmlNode('strong', params.revid) ]); // Treat ipv6 users on same 64 block as the same if (lastuser === params.user || (mw.util.isIPv6Address(params.user) && Morebits.ip.get64(lastuser) === Morebits.ip.get64(params.user))) { switch (params.type) { case 'vand': var diffUser = lastuser !== params.user; Morebits.Status.info('Info', [ 'Latest revision was ' + (diffUser ? '' : 'also ') + 'made by ', Morebits.htmlNode('strong', userNorm), diffUser ? ', which is on the same /64 subnet' : '', '. As we assume vandalism, we will proceed to revert.' ]); break; case 'agf': Morebits.Status.warn('Warning', [ 'Latest revision was made by ', Morebits.htmlNode('strong', userNorm), '. As we assume good faith, we will stop the revert, as the problem might have been fixed.' ]); return; default: Morebits.Status.warn('Notice', [ 'Latest revision was made by ', Morebits.htmlNode('strong', userNorm), ', but we will stop the revert.' ]); return; } } else if (params.type === 'vand' && // Okay to test on user since it will either fail or sysop will correctly access it // Besides, none of the trusted bots are going to be revdel'd Twinkle.rollback.trustedBots.includes(top.user) && revs.length > 1 && revs[1].revid === params.revid) { Morebits.Status.info('Info', [ 'Latest revision was made by ', Morebits.htmlNode('strong', lastuser), ', a trusted bot, and the revision before was made by our vandal, so we will proceed with the revert.' ]); index = 2; } else { Morebits.Status.error('Error', [ 'Latest revision was made by ', Morebits.htmlNode('strong', lastuser), ', so it might have already been reverted, we will stop the revert.']); return; } } else { // Expected revision is the same, so the users must match; // this allows sysops to know whether the users are the same params.user = lastuser; userNorm = params.user || Twinkle.rollback.hiddenName; } if (Twinkle.rollback.trustedBots.includes(params.user)) { switch (params.type) { case 'vand': Morebits.Status.info('Info', [ 'Vandalism revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. As this is a trusted bot, we assume you wanted to revert vandalism made by the previous user instead.' ]); index = 2; params.user = revs[1].user; params.userHidden = !!revs[1].userhidden; break; case 'agf': Morebits.Status.warn('Notice', [ 'Good faith revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot and thus AGF rollback will not proceed.' ]); return; case 'norm': /* falls through */ default: var cont = confirm('Normal revert was chosen, but the most recent edit was made by a trusted bot (' + userNorm + '). Do you want to revert the revision before instead?'); if (cont) { Morebits.Status.info('Info', [ 'Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, and per confirmation, we\'ll revert the previous revision instead.' ]); index = 2; params.user = revs[1].user; params.userHidden = !!revs[1].userhidden; userNorm = params.user || Twinkle.rollback.hiddenName; } else { Morebits.Status.warn('Notice', [ 'Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, but per confirmation, revert on selected revision will proceed.' ]); } break; } } let found = false; let count = 0; let seen64 = false; for (let i = index; i < revs.length; ++i) { ++count; if (revs[i].user !== params.user) { // Treat ipv6 users on same 64 block as the same if (mw.util.isIPv6Address(revs[i].user) && Morebits.ip.get64(revs[i].user) === Morebits.ip.get64(params.user)) { if (!seen64) { new Morebits.Status('Note', 'Treating consecutive IPv6 addresses in the same /64 as the same user'); seen64 = true; } continue; } found = i; break; } } if (!found) { statelem.error([ 'No previous revision found. Perhaps ', Morebits.htmlNode('strong', userNorm), ' is the only contributor, or they have made more than ' + mw.language.convertNumber(Twinkle.getPref('revertMaxRevisions')) + ' edits in a row.' ]); return; } if (!count) { Morebits.Status.error('Error', 'As it is not possible to revert zero revisions, we will stop this revert. It could be that the edit has already been reverted, but the revision ID was still the same.'); return; } const good_revision = revs[found]; let userHasAlreadyConfirmedAction = false; if (params.type !== 'vand' && count > 1) { if (!confirm(userNorm + ' has made ' + mw.language.convertNumber(count) + ' edits in a row. Are you sure you want to revert them all?')) { Morebits.Status.info('Notice', 'Stopping revert.'); return; } userHasAlreadyConfirmedAction = true; } params.count = count; params.goodid = good_revision.revid; params.gooduser = good_revision.user; params.gooduserHidden = !!good_revision.userhidden; statelem.status([ ' revision ', Morebits.htmlNode('strong', params.goodid), ' that was made ', Morebits.htmlNode('strong', mw.language.convertNumber(count)), ' revisions ago by ', Morebits.htmlNode('strong', params.gooduserHidden ? Twinkle.rollback.hiddenName : params.gooduser) ]); let summary, extra_summary; switch (params.type) { case 'agf': extra_summary = prompt('An optional comment for the edit summary: ', ''); // padded out to widen prompt in Firefox if (extra_summary === null) { statelem.error('Aborted by user.'); return; } userHasAlreadyConfirmedAction = true; summary = Twinkle.rollback.formatSummary('Reverted [[WP:AGF|good faith]] edits by $USER', params.userHidden ? null : params.user, extra_summary); break; case 'vand': summary = Twinkle.rollback.formatSummary('Reverted ' + params.count + (params.count > 1 ? ' edits' : ' edit') + ' by $USER to last revision by ' + (params.gooduserHidden ? Twinkle.rollback.hiddenName : params.gooduser), params.userHidden ? null : params.user); break; case 'norm': /* falls through */ default: if (Twinkle.getPref('offerReasonOnNormalRevert')) { extra_summary = prompt('An optional comment for the edit summary: ', ''); // padded out to widen prompt in Firefox if (extra_summary === null) { statelem.error('Aborted by user.'); return; } userHasAlreadyConfirmedAction = true; } summary = Twinkle.rollback.formatSummary('Reverted ' + params.count + (params.count > 1 ? ' edits' : ' edit') + ' by $USER', params.userHidden ? null : params.user, extra_summary); break; } const needToDisplayConfirmation = ( Twinkle.getPref('confirmOnRollback') || ( Twinkle.getPref('confirmOnMobileRollback') && // Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]] /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent) ) ) && !userHasAlreadyConfirmedAction; if (needToDisplayConfirmation && !confirm('Reverting page: are you sure?')) { statelem.error('Aborted by user.'); return; } // Decide whether to notify the user on success if (!Twinkle.rollback.skipTalk && Twinkle.getPref('openTalkPage').includes(params.type) && !params.userHidden && mw.config.get('wgUserName') !== params.user) { params.notifyUser = true; // Pass along to the warn module params.vantimestamp = top.timestamp; } // figure out whether we need to/can review the edit const flagged = page.flagged; if ((Morebits.userIsInGroup('reviewer') || Morebits.userIsSysop) && !!flagged && flagged.stable_revid >= params.goodid && !!flagged.pending_since) { params.reviewRevert = true; params.csrftoken = csrftoken; } const query = { action: 'edit', title: params.pagename, summary: summary, tags: Twinkle.changeTags, token: csrftoken, undo: lastrevid, undoafter: params.goodid, basetimestamp: touched, starttimestamp: loadtimestamp, minor: Twinkle.getPref('markRevertedPagesAsMinor').includes(params.type) ? true : undefined, format: 'json' }; // Handle watching, possible expiry if (Twinkle.getPref('watchRevertedPages').includes(params.type)) { const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry'); if (!watchOrExpiry || watchOrExpiry === 'no') { query.watchlist = 'nochange'; } else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') { query.watchlist = 'preferences'; } else { query.watchlist = 'watch'; // number allowed but not used in Twinkle.config.watchlistEnums if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') { query.watchlistexpiry = watchOrExpiry; } } } if (!Twinkle.rollback.rollbackInPlace) { Morebits.wiki.actionCompleted.redirect = params.pagename; } Morebits.wiki.actionCompleted.notice = 'Reversion completed'; const wikipedia_api = new Morebits.wiki.Api('Saving reverted contents', query, Twinkle.rollback.callbacks.complete, statelem); wikipedia_api.params = params; wikipedia_api.post(); }, complete: function (apiobj) { // TODO Most of this is copy-pasted from Morebits.wiki.Page#fnSaveSuccess. Unify it const response = apiobj.getResponse(); const edit = response.edit; if (edit.captcha) { apiobj.statelem.error('Could not rollback, because the wiki server wanted you to fill out a CAPTCHA.'); } else if (edit.nochange) { apiobj.statelem.error('Revision we are reverting to is identical to current revision, stopping revert.'); } else { apiobj.statelem.info('done'); const params = apiobj.params; if (params.notifyUser && !params.userHidden) { // notifyUser only from main, not from toRevision Morebits.Status.info('Info', [ 'Opening user talk page edit form for user ', Morebits.htmlNode('strong', params.user) ]); const url = mw.util.getUrl('User talk:' + params.user, { action: 'edit', preview: 'yes', vanarticle: params.pagename.replace(/_/g, ' '), vanarticlerevid: params.revid, vantimestamp: params.vantimestamp, vanarticlegoodrevid: params.goodid, type: params.type, count: params.count }); switch (Twinkle.getPref('userTalkPageMode')) { case 'tab': window.open(url, '_blank'); break; case 'blank': window.open(url, '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800'); break; case 'window': /* falls through */ default: window.open(url, window.name === 'twinklewarnwindow' ? '_blank' : 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800'); break; } // prefill Wel/ARV/Warn when rollback used on Special:Contributions page } else if (Twinkle.rollback.rollbackInPlace && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') { Twinkle.setPrefill('vanarticle', params.pagename.replace(/_/g, ' ')); Twinkle.setPrefill('vanarticlerevid', params.revid); Twinkle.setPrefill('vantimestamp', params.vantimestamp); Twinkle.setPrefill('vanarticlegoodrevid', params.goodid); } // review the revert, if needed if (apiobj.params.reviewRevert) { const query = { action: 'review', revid: edit.newrevid, token: apiobj.params.csrftoken, comment: 'Automatically reviewing reversion' + Twinkle.summaryAd // until the below // 'tags': Twinkle.changeTags // flaggedrevs tag support: [[phab:T247721]] }; const wikipedia_api = new Morebits.wiki.Api('Automatically accepting your changes', query); wikipedia_api.post(); } } } }; // If builtInString contains the string "$USER", it will be replaced // by an appropriate user link if a user name is provided Twinkle.rollback.formatSummary = function(builtInString, userName, customString) { let result = builtInString; // append user's custom reason if (customString) { result += ': ' + Morebits.string.toUpperCaseFirstChar(customString); } // find number of UTF-8 bytes the resulting string takes up, and possibly add // a contributions or contributions+talk link if it doesn't push the edit summary // over the 499-byte limit if (/\$USER/.test(builtInString)) { if (userName) { const resultLen = unescape(encodeURIComponent(result.replace('$USER', ''))).length; const contribsLink = '[[Special:Contributions/' + userName + '|' + userName + ']]'; const contribsLen = unescape(encodeURIComponent(contribsLink)).length; if (resultLen + contribsLen <= 499) { const talkLink = ' ([[User talk:' + userName + '|talk]])'; if (resultLen + contribsLen + unescape(encodeURIComponent(talkLink)).length <= 499) { result = Morebits.string.safeReplace(result, '$USER', contribsLink + talkLink); } else { result = Morebits.string.safeReplace(result, '$USER', contribsLink); } } else { result = Morebits.string.safeReplace(result, '$USER', userName); } } else { result = Morebits.string.safeReplace(result, '$USER', Twinkle.rollback.hiddenName); } } return result; }; Twinkle.addInitCallback(Twinkle.rollback, 'rollback'); }()); // </nowiki> n4y7sxaiw6oha1vgn1jepsiqrpugdg5 MediaWiki:Gadget-twinkleprotect.js 8 24479 268716 2026-04-27T16:47:56Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinkleprotect.js: Protect/RPP module **************************************** * Mode of invocation: Tab ("PP"/"RPP") * Active on: Non-special, non-MediaWiki pages */ // Note: a lot of code in this module is re-used/called by batchprotect. Twinkle.protect = function twinkleprotect() { if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgNamespaceNumber'...' 268716 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinkleprotect.js: Protect/RPP module **************************************** * Mode of invocation: Tab ("PP"/"RPP") * Active on: Non-special, non-MediaWiki pages */ // Note: a lot of code in this module is re-used/called by batchprotect. Twinkle.protect = function twinkleprotect() { if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgNamespaceNumber') === 8) { return; } Twinkle.addPortletLink(Twinkle.protect.callback, Morebits.userIsSysop ? 'PP' : 'RPP', 'tw-rpp', Morebits.userIsSysop ? 'Protect page' : 'Request page protection'); }; Twinkle.protect.callback = function twinkleprotectCallback() { const Window = new Morebits.SimpleWindow(620, 530); Window.setTitle(Morebits.userIsSysop ? 'Apply, request or tag page protection' : 'Request or tag page protection'); Window.setScriptName('Twinkle'); Window.addFooterLink('Protection templates', 'Template:Protection templates'); Window.addFooterLink('Protection policy', 'WP:PROT'); Window.addFooterLink('Twinkle help', 'WP:TW/DOC#protect'); Window.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.protect.callback.evaluate); const actionfield = form.append({ type: 'field', label: 'Type of action' }); if (Morebits.userIsSysop) { actionfield.append({ type: 'radio', name: 'actiontype', event: Twinkle.protect.callback.changeAction, list: [ { label: 'Protect page', value: 'protect', tooltip: 'Apply actual protection to the page.', checked: true } ] }); } actionfield.append({ type: 'radio', name: 'actiontype', event: Twinkle.protect.callback.changeAction, list: [ { label: 'Request page protection', value: 'request', tooltip: 'If you want to request protection via WP:RPP' + (Morebits.userIsSysop ? ' instead of doing the protection by yourself.' : '.'), checked: !Morebits.userIsSysop }, { label: 'Tag page with protection template', value: 'tag', tooltip: 'If the protecting admin forgot to apply a protection template, or you have just protected the page without tagging, you can use this to apply the appropriate protection tag.', disabled: mw.config.get('wgArticleId') === 0 || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710 // TimedText } ] }); form.append({ type: 'field', label: 'Preset', name: 'field_preset' }); form.append({ type: 'field', label: '1', name: 'field1' }); form.append({ type: 'field', label: '2', name: 'field2' }); form.append({ type: 'submit' }); const result = form.render(); Window.setContent(result); Window.display(); // We must init the controls const evt = document.createEvent('Event'); evt.initEvent('change', true, true); result.actiontype[0].dispatchEvent(evt); // get current protection level asynchronously Twinkle.protect.fetchProtectionLevel(); }; // A list of bots who may be the protecting sysop, for whom we shouldn't // remind the user contact before requesting unprotection (evaluate) Twinkle.protect.trustedBots = ['MusikBot II', 'TFA Protector Bot']; // Customizable namespace and FlaggedRevs settings // In theory it'd be nice to have restrictionlevels defined here, // but those are only available via a siteinfo query // mw.loader.getState('ext.flaggedRevs.review') returns null if the // FlaggedRevs extension is not registered. Previously, this was done with // wgFlaggedRevsParams, but after 1.34-wmf4 it is no longer exported if empty // (https://gerrit.wikimedia.org/r/c/mediawiki/extensions/FlaggedRevs/+/508427) const hasFlaggedRevs = mw.loader.getState('ext.flaggedRevs.review') && // FlaggedRevs only valid in some namespaces, hardcoded until [[phab:T218479]] (mw.config.get('wgNamespaceNumber') === 0 || mw.config.get('wgNamespaceNumber') === 4); // Limit template editor; a Twinkle restriction, not a site setting const isTemplate = mw.config.get('wgNamespaceNumber') === 10 || mw.config.get('wgNamespaceNumber') === 828; // Contains the current protection level in an object // Once filled, it will look something like: // { edit: { level: "sysop", expiry: <some date>, cascade: true }, ... } Twinkle.protect.currentProtectionLevels = {}; // returns a jQuery Deferred object, usage: // Twinkle.protect.fetchProtectingAdmin(apiObject, pageName, protect/stable).done(function(admin_username) { ...code... }); Twinkle.protect.fetchProtectingAdmin = function twinkleprotectFetchProtectingAdmin(api, pageName, protType, logIds) { logIds = logIds || []; return api.get({ format: 'json', action: 'query', list: 'logevents', letitle: pageName, letype: protType }).then((data) => { // don't check log entries that have already been checked (e.g. don't go into an infinite loop!) const event = data.query ? $.grep(data.query.logevents, (le) => $.inArray(le.logid, logIds))[0] : null; if (!event) { // fail gracefully return null; } else if (event.action === 'move_prot' || event.action === 'move_stable') { return twinkleprotectFetchProtectingAdmin(api, protType === 'protect' ? event.params.oldtitle_title : event.params.oldtitle, protType, logIds.concat(event.logid)); } return event.user; }); }; Twinkle.protect.fetchProtectionLevel = function twinkleprotectFetchProtectionLevel() { const api = new mw.Api(); const protectDeferred = api.get({ format: 'json', indexpageids: true, action: 'query', list: 'logevents', letype: 'protect', letitle: mw.config.get('wgPageName'), prop: hasFlaggedRevs ? 'info|flagged' : 'info', inprop: 'protection|watched', titles: mw.config.get('wgPageName') }); const stableDeferred = api.get({ format: 'json', action: 'query', list: 'logevents', letype: 'stable', letitle: mw.config.get('wgPageName') }); const earlyDecision = [protectDeferred]; if (hasFlaggedRevs) { earlyDecision.push(stableDeferred); } $.when.apply($, earlyDecision).done((protectData, stableData) => { // $.when.apply is supposed to take an unknown number of promises // via an array, which it does, but the type of data returned varies. // If there are two or more deferreds, it returns an array (of objects), // but if there's just one deferred, it retuns a simple object. // This is annoying. protectData = $(protectData).toArray(); const pageid = protectData[0].query.pageids[0]; const page = protectData[0].query.pages[pageid]; const current = {}; let adminEditDeferred; // Save requested page's watched status for later in case needed when filing request Twinkle.protect.watched = page.watchlistexpiry || page.watched === ''; $.each(page.protection, (index, protection) => { // Don't overwrite actual page protection with cascading protection if (!protection.source) { current[protection.type] = { level: protection.level, expiry: protection.expiry, cascade: protection.cascade === '' }; // logs report last admin who made changes to either edit/move/create protection, regardless if they only modified one of them if (!adminEditDeferred) { adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'protect'); } } else { // Account for the page being covered by cascading protection current.cascading = { expiry: protection.expiry, source: protection.source, level: protection.level // should always be sysop, unused }; } }); if (page.flagged) { current.stabilize = { level: page.flagged.protection_level, expiry: page.flagged.protection_expiry }; adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'stable'); } // show the protection level and log info Twinkle.protect.hasProtectLog = !!protectData[0].query.logevents.length; Twinkle.protect.protectLog = Twinkle.protect.hasProtectLog && protectData[0].query.logevents; Twinkle.protect.hasStableLog = hasFlaggedRevs ? !!stableData[0].query.logevents.length : false; Twinkle.protect.stableLog = Twinkle.protect.hasStableLog && stableData[0].query.logevents; Twinkle.protect.currentProtectionLevels = current; if (adminEditDeferred) { adminEditDeferred.done((admin) => { if (admin) { $.each(['edit', 'move', 'create', 'stabilize', 'cascading'], (i, type) => { if (Twinkle.protect.currentProtectionLevels[type]) { Twinkle.protect.currentProtectionLevels[type].admin = admin; } }); } Twinkle.protect.callback.showLogAndCurrentProtectInfo(); }); } else { Twinkle.protect.callback.showLogAndCurrentProtectInfo(); } }); }; Twinkle.protect.callback.showLogAndCurrentProtectInfo = function twinkleprotectCallbackShowLogAndCurrentProtectInfo() { const currentlyProtected = !$.isEmptyObject(Twinkle.protect.currentProtectionLevels); if (Twinkle.protect.hasProtectLog || Twinkle.protect.hasStableLog) { const $linkMarkup = $('<span>'); if (Twinkle.protect.hasProtectLog) { $linkMarkup.append( $('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'protect'}) + '">protection log</a>')); if (!currentlyProtected || (!Twinkle.protect.currentProtectionLevels.edit && !Twinkle.protect.currentProtectionLevels.move)) { const lastProtectAction = Twinkle.protect.protectLog[0]; if (lastProtectAction.action === 'unprotect') { $linkMarkup.append(' (unprotected ' + new Morebits.Date(lastProtectAction.timestamp).calendar('utc') + ')'); } else { // protect or modify $linkMarkup.append(' (expired ' + new Morebits.Date(lastProtectAction.params.details[0].expiry).calendar('utc') + ')'); } } $linkMarkup.append(Twinkle.protect.hasStableLog ? $('<span> &bull; </span>') : null); } if (Twinkle.protect.hasStableLog) { $linkMarkup.append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'stable'}) + '">pending changes log</a>)')); if (!currentlyProtected || !Twinkle.protect.currentProtectionLevels.stabilize) { const lastStabilizeAction = Twinkle.protect.stableLog[0]; if (lastStabilizeAction.action === 'reset') { $linkMarkup.append(' (reset ' + new Morebits.Date(lastStabilizeAction.timestamp).calendar('utc') + ')'); } else { // config or modify $linkMarkup.append(' (expired ' + new Morebits.Date(lastStabilizeAction.params.expiry).calendar('utc') + ')'); } } } Morebits.Status.init($('div[name="hasprotectlog"] span')[0]); Morebits.Status.warn( currentlyProtected ? 'Previous protections' : 'This page has been protected in the past', $linkMarkup[0] ); } Morebits.Status.init($('div[name="currentprot"] span')[0]); let protectionNode = [], statusLevel = 'info'; if (currentlyProtected) { $.each(Twinkle.protect.currentProtectionLevels, (type, settings) => { let label = type === 'stabilize' ? 'Pending Changes' : Morebits.string.toUpperCaseFirstChar(type); if (type === 'cascading') { // Covered by another page label = 'Cascading protection '; protectionNode.push($('<b>' + label + '</b>')[0]); if (settings.source) { // Should by definition exist const sourceLink = '<a target="_blank" href="' + mw.util.getUrl(settings.source) + '">' + settings.source + '</a>'; protectionNode.push($('<span>from ' + sourceLink + '</span>')[0]); } } else { let level = settings.level; // Make cascading protection more prominent if (settings.cascade) { level += ' (cascading)'; } protectionNode.push($('<b>' + label + ': ' + level + '</b>')[0]); } if (settings.expiry === 'infinity') { protectionNode.push(' (indefinite) '); } else { protectionNode.push(' (expires ' + new Morebits.Date(settings.expiry).calendar('utc') + ') '); } if (settings.admin) { const adminLink = '<a target="_blank" href="' + mw.util.getUrl('User talk:' + settings.admin) + '">' + settings.admin + '</a>'; protectionNode.push($('<span>by ' + adminLink + '</span>')[0]); } protectionNode.push($('<span> \u2022 </span>')[0]); }); protectionNode = protectionNode.slice(0, -1); // remove the trailing bullet statusLevel = 'warn'; } else { protectionNode.push($('<b>no protection</b>')[0]); } Morebits.Status[statusLevel]('Current protection level', protectionNode); }; Twinkle.protect.callback.changeAction = function twinkleprotectCallbackChangeAction(e) { let field_preset; let field1; let field2; switch (e.target.values) { case 'protect': field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'Preset', name: 'field_preset' }); field_preset.append({ type: 'select', name: 'category', label: 'Choose a preset:', event: Twinkle.protect.callback.changePreset, list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate }); field2 = new Morebits.QuickForm.Element({ type: 'field', label: 'Protection options', name: 'field2' }); field2.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback field2.append({ type: 'div', name: 'hasprotectlog', label: ' ' }); // for existing pages if (mw.config.get('wgArticleId')) { field2.append({ type: 'checkbox', event: Twinkle.protect.formevents.editmodify, list: [ { label: 'Modify edit protection', name: 'editmodify', tooltip: 'If this is turned off, the edit protection level, and expiry time, will be left as is.', checked: true } ] }); field2.append({ type: 'select', name: 'editlevel', label: 'Who can edit:', event: Twinkle.protect.formevents.editlevel, // Filter TE outside of templates and modules list: Twinkle.protect.protectionLevels.filter((level) => isTemplate || level.value !== 'templateeditor') }); field2.append({ type: 'select', name: 'editexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, // default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset list: Twinkle.protect.protectionLengths }); field2.append({ type: 'checkbox', event: Twinkle.protect.formevents.movemodify, list: [ { label: 'Modify move protection', name: 'movemodify', tooltip: 'If this is turned off, the move protection level, and expiry time, will be left as is.', checked: true } ] }); field2.append({ type: 'select', name: 'movelevel', label: 'Who can move:', event: Twinkle.protect.formevents.movelevel, // Autoconfirmed is required for a move, redundant list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed' && (isTemplate || level.value !== 'templateeditor')) }); field2.append({ type: 'select', name: 'moveexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, // default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset list: Twinkle.protect.protectionLengths }); if (hasFlaggedRevs) { field2.append({ type: 'checkbox', event: Twinkle.protect.formevents.pcmodify, list: [ { label: 'Modify pending changes protection', name: 'pcmodify', tooltip: 'If this is turned off, the pending changes level, and expiry time, will be left as is.', checked: true } ] }); field2.append({ type: 'select', name: 'pclevel', label: 'Pending changes:', event: Twinkle.protect.formevents.pclevel, list: [ { label: 'None', value: 'none' }, { label: 'Pending change', value: 'autoconfirmed', selected: true } ] }); field2.append({ type: 'select', name: 'pcexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, // default expiry selection (1 month) is conditionally set in Twinkle.protect.callback.changePreset list: Twinkle.protect.protectionLengths }); } } else { // for non-existing pages field2.append({ type: 'select', name: 'createlevel', label: 'Create protection:', event: Twinkle.protect.formevents.createlevel, // Filter TE always, and autoconfirmed in mainspace, redundant since WP:ACPERM list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'templateeditor' && (mw.config.get('wgNamespaceNumber') !== 0 || level.value !== 'autoconfirmed')) }); field2.append({ type: 'select', name: 'createexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, // default expiry selection (indefinite) is conditionally set in Twinkle.protect.callback.changePreset list: Twinkle.protect.protectionLengths }); } field2.append({ type: 'textarea', name: 'protectReason', label: 'Reason (for protection log):' }); field2.append({ type: 'div', name: 'protectReason_notes', label: 'Notes:', style: 'display:inline-block; margin-top:4px;', tooltip: 'Add a note to the protection log that this was requested at RfPP.' }); field2.append({ type: 'checkbox', event: Twinkle.protect.callback.annotateProtectReason, style: 'display:inline-block; margin-top:4px;', list: [ { label: 'RfPP request', name: 'protectReason_notes_rfpp', checked: false, value: 'requested at [[WP:RfPP]]' } ] }); field2.append({ type: 'input', event: Twinkle.protect.callback.annotateProtectReason, label: 'RfPP revision ID', name: 'protectReason_notes_rfppRevid', value: '', tooltip: 'Optional revision ID of the RfPP page where protection was requested.' }); if (!mw.config.get('wgArticleId') || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710) { // tagging isn't relevant for non-existing, module, or TimedText pages break; } /* falls through */ case 'tag': field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'Tagging options', name: 'field1' }); field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' }); field1.append({ type: 'select', name: 'tagtype', label: 'Choose protection template:', list: Twinkle.protect.protectionTags, event: Twinkle.protect.formevents.tagtype }); var isTemplateNamespace = mw.config.get('wgNamespaceNumber') === 10; var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:Articles for deletion/'); var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel')); field1.append({ type: 'checkbox', list: [ { name: 'small', label: 'Iconify (small=yes)', tooltip: 'Will use the |small=yes feature of the template, and only render it as a keylock', checked: true }, { name: 'noinclude', label: 'Wrap protection template with &lt;noinclude&gt;', tooltip: 'Will wrap the protection template in &lt;noinclude&gt; tags, so that it won\'t transclude', checked: (isTemplateNamespace || isAFD) && !isCode } ] }); break; case 'request': field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'Type of protection', name: 'field_preset' }); field_preset.append({ type: 'select', name: 'category', label: 'Type and reason:', event: Twinkle.protect.callback.changePreset, list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate }); field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'Options', name: 'field1' }); field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' }); field1.append({ type: 'select', name: 'expiry', label: 'Duration:', list: [ { label: '', selected: true, value: '' }, { label: 'Temporary', value: 'temporary' }, { label: 'Indefinite', value: 'infinity' } ] }); field1.append({ type: 'textarea', name: 'reason', label: 'Reason:' }); break; default: alert("Something's afoot in twinkleprotect"); break; } let oldfield; if (field_preset) { oldfield = $(e.target.form).find('fieldset[name="field_preset"]')[0]; oldfield.parentNode.replaceChild(field_preset.render(), oldfield); } else { $(e.target.form).find('fieldset[name="field_preset"]').css('display', 'none'); } if (field1) { oldfield = $(e.target.form).find('fieldset[name="field1"]')[0]; oldfield.parentNode.replaceChild(field1.render(), oldfield); } else { $(e.target.form).find('fieldset[name="field1"]').css('display', 'none'); } if (field2) { oldfield = $(e.target.form).find('fieldset[name="field2"]')[0]; oldfield.parentNode.replaceChild(field2.render(), oldfield); } else { $(e.target.form).find('fieldset[name="field2"]').css('display', 'none'); } if (e.target.values === 'protect') { // fake a change event on the preset dropdown const evt = document.createEvent('Event'); evt.initEvent('change', true, true); e.target.form.category.dispatchEvent(evt); // reduce vertical height of dialog $(e.target.form).find('fieldset[name="field2"] select').parent().css({ display: 'inline-block', marginRight: '0.5em' }); $(e.target.form).find('fieldset[name="field2"] input[name="protectReason_notes_rfppRevid"]').parent().css({display: 'inline-block', marginLeft: '15px'}).hide(); } // re-add protection level and log info, if it's available Twinkle.protect.callback.showLogAndCurrentProtectInfo(); }; // NOTE: This function is used by batchprotect as well Twinkle.protect.formevents = { editmodify: function twinkleprotectFormEditmodifyEvent(e) { e.target.form.editlevel.disabled = !e.target.checked; e.target.form.editexpiry.disabled = !e.target.checked || (e.target.form.editlevel.value === 'all'); e.target.form.editlevel.style.color = e.target.form.editexpiry.style.color = e.target.checked ? '' : 'transparent'; }, editlevel: function twinkleprotectFormEditlevelEvent(e) { e.target.form.editexpiry.disabled = e.target.value === 'all'; }, movemodify: function twinkleprotectFormMovemodifyEvent(e) { // sync move settings with edit settings if applicable if (e.target.form.movelevel.disabled && !e.target.form.editlevel.disabled) { e.target.form.movelevel.value = e.target.form.editlevel.value; e.target.form.moveexpiry.value = e.target.form.editexpiry.value; } else if (e.target.form.editlevel.disabled) { e.target.form.movelevel.value = 'sysop'; e.target.form.moveexpiry.value = 'infinity'; } e.target.form.movelevel.disabled = !e.target.checked; e.target.form.moveexpiry.disabled = !e.target.checked || (e.target.form.movelevel.value === 'all'); e.target.form.movelevel.style.color = e.target.form.moveexpiry.style.color = e.target.checked ? '' : 'transparent'; }, movelevel: function twinkleprotectFormMovelevelEvent(e) { e.target.form.moveexpiry.disabled = e.target.value === 'all'; }, pcmodify: function twinkleprotectFormPcmodifyEvent(e) { e.target.form.pclevel.disabled = !e.target.checked; e.target.form.pcexpiry.disabled = !e.target.checked || (e.target.form.pclevel.value === 'none'); e.target.form.pclevel.style.color = e.target.form.pcexpiry.style.color = e.target.checked ? '' : 'transparent'; }, pclevel: function twinkleprotectFormPclevelEvent(e) { e.target.form.pcexpiry.disabled = e.target.value === 'none'; }, createlevel: function twinkleprotectFormCreatelevelEvent(e) { e.target.form.createexpiry.disabled = e.target.value === 'all'; }, tagtype: function twinkleprotectFormTagtypeEvent(e) { e.target.form.small.disabled = e.target.form.noinclude.disabled = (e.target.value === 'none') || (e.target.value === 'noop'); } }; Twinkle.protect.doCustomExpiry = function twinkleprotectDoCustomExpiry(target) { const custom = prompt('Enter a custom expiry time. \nYou can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).', ''); if (custom) { const option = document.createElement('option'); option.setAttribute('value', custom); option.textContent = custom; target.appendChild(option); target.value = custom; } else { target.selectedIndex = 0; } }; // NOTE: This list is used by batchprotect as well Twinkle.protect.protectionLevels = [ { label: 'All', value: 'all' }, { label: 'Autoconfirmed', value: 'autoconfirmed' }, { label: 'Extended confirmed', value: 'extendedconfirmed' }, { label: 'Template editor', value: 'templateeditor' }, { label: 'Sysop', value: 'sysop', selected: true } ]; // default expiry selection is conditionally set in Twinkle.protect.callback.changePreset // NOTE: This list is used by batchprotect as well Twinkle.protect.protectionLengths = [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '10 days', value: '10 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '6 months', value: '6 months' }, { label: '1 year', value: '1 year' }, { label: '2 years', value: '2 years' }, { label: 'indefinite', value: 'infinity' }, { label: 'Custom...', value: 'custom' } ]; Twinkle.protect.protectionTypes = [ { label: 'Unprotection', value: 'unprotect' }, { label: 'Full protection', list: [ { label: 'Generic (full)', value: 'pp-protected' }, { label: 'Content dispute/edit warring (full)', value: 'pp-dispute' }, { label: 'Persistent vandalism (full)', value: 'pp-vandalism' }, { label: 'User talk of blocked user (full)', value: 'pp-usertalk' } ] }, { label: 'Template protection', list: [ { label: 'Highly visible template (TE)', value: 'pp-template' } ] }, { label: 'Extended confirmed protection', list: [ { label: 'Generic (ECP)', value: 'pp-30-500' }, { label: 'Arbitration enforcement (ECP)', selected: true, value: 'pp-30-500-arb' }, { label: 'Persistent vandalism (ECP)', value: 'pp-30-500-vandalism' }, { label: 'Disruptive editing (ECP)', value: 'pp-30-500-disruptive' }, { label: 'BLP policy violations (ECP)', value: 'pp-30-500-blp' }, { label: 'Sockpuppetry (ECP)', value: 'pp-30-500-sock' } ] }, { label: 'Semi-protection', list: [ { label: 'Generic (semi)', value: 'pp-semi-protected' }, { label: 'Persistent vandalism (semi)', selected: true, value: 'pp-semi-vandalism' }, { label: 'Disruptive editing (semi)', value: 'pp-semi-disruptive' }, { label: 'Adding unsourced content (semi)', value: 'pp-semi-unsourced' }, { label: 'BLP policy violations (semi)', value: 'pp-semi-blp' }, { label: 'Sockpuppetry (semi)', value: 'pp-semi-sock' }, { label: 'User talk of blocked user (semi)', value: 'pp-semi-usertalk' }, { label: 'Noticeboard LTA (semi)', value: 'pp-sock-noticeboard' } ] }, { label: 'Pending changes', list: [ { label: 'Generic (PC)', value: 'pp-pc-protected' }, { label: 'Persistent vandalism (PC)', value: 'pp-pc-vandalism' }, { label: 'Disruptive editing (PC)', value: 'pp-pc-disruptive' }, { label: 'Adding unsourced content (PC)', value: 'pp-pc-unsourced' }, { label: 'BLP policy violations (PC)', value: 'pp-pc-blp' } ] }, { label: 'Move protection', list: [ { label: 'Generic (move)', value: 'pp-move' }, { label: 'Dispute/move warring (move)', value: 'pp-move-dispute' }, { label: 'Page-move vandalism (move)', value: 'pp-move-vandalism' }, { label: 'Highly visible page (move)', value: 'pp-move-indef' } ] } ] // Filter for templates and flaggedrevs .filter((type) => (isTemplate || type.label !== 'Template protection') && (hasFlaggedRevs || type.label !== 'Pending changes')); Twinkle.protect.protectionTypesCreate = [ { label: 'Unprotection', value: 'unprotect' }, { label: 'Create protection', list: [ { label: 'Offensive name', value: 'pp-create-offensive' }, { label: 'Repeatedly recreated', selected: true, value: 'pp-create-salt' }, { label: 'Recently deleted BLP', value: 'pp-create-blp' } ] } ]; // A page with both regular and PC protection will be assigned its regular // protection weight plus 2 Twinkle.protect.protectionWeight = { sysop: 40, templateeditor: 30, extendedconfirmed: 20, autoconfirmed: 10, flaggedrevs_autoconfirmed: 5, // Pending Changes protection alone all: 0, flaggedrevs_none: 0 // just in case }; // NOTICE: keep this synched with [[MediaWiki:Protect-dropdown]] // Also note: stabilize = Pending Changes level // expiry will override any defaults Twinkle.protect.protectionPresetsInfo = { 'pp-protected': { edit: 'sysop', move: 'sysop', reason: null }, 'pp-dispute': { edit: 'sysop', move: 'sysop', reason: '[[WP:PP#Content disputes|Edit warring / content dispute]]' }, 'pp-sock-noticeboard': { edit: 'autoconfirmed', expiry: '2 hours', reason: 'Persistent [[WP:Sock puppetry|sock puppetry]]', template: 'pp-sock' }, 'pp-vandalism': { edit: 'sysop', move: 'sysop', reason: 'Persistent [[WP:Vandalism|vandalism]]' }, 'pp-usertalk': { edit: 'sysop', move: 'sysop', expiry: 'infinity', reason: '[[WP:PP#Talk-page protection|Inappropriate use of user talk page while blocked]]' }, 'pp-template': { edit: 'templateeditor', move: 'templateeditor', expiry: 'infinity', reason: '[[WP:High-risk templates|Highly visible template]]' }, 'pp-30-500-arb': { edit: 'extendedconfirmed', move: 'extendedconfirmed', expiry: 'infinity', reason: '[[WP:30/500|Arbitration enforcement]]', template: 'pp-extended' }, 'pp-30-500-vandalism': { edit: 'extendedconfirmed', move: 'extendedconfirmed', reason: 'Persistent [[WP:Vandalism|vandalism]] from (auto)confirmed accounts', template: 'pp-extended' }, 'pp-30-500-disruptive': { edit: 'extendedconfirmed', move: 'extendedconfirmed', reason: 'Persistent [[WP:Disruptive editing|disruptive editing]] from (auto)confirmed accounts', template: 'pp-extended' }, 'pp-30-500-blp': { edit: 'extendedconfirmed', move: 'extendedconfirmed', reason: 'Persistent violations of the [[WP:BLP|biographies of living persons policy]] from (auto)confirmed accounts', template: 'pp-extended' }, 'pp-30-500-sock': { edit: 'extendedconfirmed', move: 'extendedconfirmed', reason: 'Persistent [[WP:Sock puppetry|sock puppetry]]', template: 'pp-extended' }, 'pp-30-500': { edit: 'extendedconfirmed', move: 'extendedconfirmed', reason: null, template: 'pp-extended' }, 'pp-semi-vandalism': { edit: 'autoconfirmed', reason: 'Persistent [[WP:Vandalism|vandalism]]', template: 'pp-vandalism' }, 'pp-semi-disruptive': { edit: 'autoconfirmed', reason: 'Persistent [[WP:Disruptive editing|disruptive editing]]', template: 'pp-protected' }, 'pp-semi-unsourced': { edit: 'autoconfirmed', reason: 'Persistent addition of [[WP:INTREF|unsourced or poorly sourced content]]', template: 'pp-protected' }, 'pp-semi-blp': { edit: 'autoconfirmed', reason: 'Violations of the [[WP:BLP|biographies of living persons policy]]', template: 'pp-blp' }, 'pp-semi-usertalk': { edit: 'autoconfirmed', expiry: 'infinity', reason: '[[WP:PP#Talk-page protection|Inappropriate use of user talk page while blocked]]', template: 'pp-usertalk' }, 'pp-semi-template': { // removed for now edit: 'autoconfirmed', expiry: 'infinity', reason: '[[WP:High-risk templates|Highly visible template]]', template: 'pp-template' }, 'pp-semi-sock': { edit: 'autoconfirmed', reason: 'Persistent [[WP:Sock puppetry|sock puppetry]]', template: 'pp-sock' }, 'pp-semi-protected': { edit: 'autoconfirmed', reason: null, template: 'pp-protected' }, 'pp-pc-vandalism': { stabilize: 'autoconfirmed', // stabilize = Pending Changes reason: 'Persistent [[WP:Vandalism|vandalism]]', template: 'pp-pc' }, 'pp-pc-disruptive': { stabilize: 'autoconfirmed', reason: 'Persistent [[WP:Disruptive editing|disruptive editing]]', template: 'pp-pc' }, 'pp-pc-unsourced': { stabilize: 'autoconfirmed', reason: 'Persistent addition of [[WP:INTREF|unsourced or poorly sourced content]]', template: 'pp-pc' }, 'pp-pc-blp': { stabilize: 'autoconfirmed', reason: 'Violations of the [[WP:BLP|biographies of living persons policy]]', template: 'pp-pc' }, 'pp-pc-protected': { stabilize: 'autoconfirmed', reason: null, template: 'pp-pc' }, 'pp-move': { move: 'sysop', reason: null }, 'pp-move-dispute': { move: 'sysop', reason: '[[WP:MOVP|Move warring]]' }, 'pp-move-vandalism': { move: 'sysop', reason: '[[WP:MOVP|Page-move vandalism]]' }, 'pp-move-indef': { move: 'sysop', expiry: 'infinity', reason: '[[WP:MOVP|Highly visible page]]' }, unprotect: { edit: 'all', move: 'all', stabilize: 'none', create: 'all', reason: null, template: 'none' }, 'pp-create-offensive': { create: 'sysop', reason: '[[WP:SALT|Offensive name]]' }, 'pp-create-salt': { create: 'extendedconfirmed', reason: '[[WP:SALT|Repeatedly recreated]]' }, 'pp-create-blp': { create: 'extendedconfirmed', reason: '[[WP:BLPDEL|Recently deleted BLP]]' } }; Twinkle.protect.protectionTags = [ { label: 'None (remove existing protection templates)', value: 'none' }, { label: 'None (do not remove existing protection templates)', value: 'noop' }, { label: 'Edit protection templates', list: [ { label: '{{pp-vandalism}}: vandalism', value: 'pp-vandalism' }, { label: '{{pp-dispute}}: dispute/edit war', value: 'pp-dispute' }, { label: '{{pp-blp}}: BLP violations', value: 'pp-blp' }, { label: '{{pp-sock}}: sockpuppetry', value: 'pp-sock' }, { label: '{{pp-template}}: high-risk template', value: 'pp-template' }, { label: '{{pp-usertalk}}: blocked user talk', value: 'pp-usertalk' }, { label: '{{pp-protected}}: general protection', value: 'pp-protected' }, { label: '{{pp-semi-indef}}: general long-term semi-protection', value: 'pp-semi-indef' }, { label: '{{pp-extended}}: extended confirmed protection', value: 'pp-extended' } ] }, { label: 'Pending changes templates', list: [ { label: '{{pp-pc}}: pending changes', value: 'pp-pc' } ] }, { label: 'Move protection templates', list: [ { label: '{{pp-move-dispute}}: dispute/move war', value: 'pp-move-dispute' }, { label: '{{pp-move-vandalism}}: page-move vandalism', value: 'pp-move-vandalism' }, { label: '{{pp-move-indef}}: general long-term', value: 'pp-move-indef' }, { label: '{{pp-move}}: other', value: 'pp-move' } ] } ] // Filter FlaggedRevs .filter((type) => hasFlaggedRevs || type.label !== 'Pending changes templates'); Twinkle.protect.callback.changePreset = function twinkleprotectCallbackChangePreset(e) { const form = e.target.form; const actiontypes = form.actiontype; let actiontype; for (let i = 0; i < actiontypes.length; i++) { if (!actiontypes[i].checked) { continue; } actiontype = actiontypes[i].values; break; } if (actiontype === 'protect') { // actually protecting the page const item = Twinkle.protect.protectionPresetsInfo[form.category.value]; if (mw.config.get('wgArticleId')) { if (item.edit) { form.editmodify.checked = true; Twinkle.protect.formevents.editmodify({ target: form.editmodify }); form.editlevel.value = item.edit; Twinkle.protect.formevents.editlevel({ target: form.editlevel }); } else { form.editmodify.checked = false; Twinkle.protect.formevents.editmodify({ target: form.editmodify }); } if (item.move) { form.movemodify.checked = true; Twinkle.protect.formevents.movemodify({ target: form.movemodify }); form.movelevel.value = item.move; Twinkle.protect.formevents.movelevel({ target: form.movelevel }); } else { form.movemodify.checked = false; Twinkle.protect.formevents.movemodify({ target: form.movemodify }); } form.editexpiry.value = form.moveexpiry.value = item.expiry || '2 days'; if (form.pcmodify) { if (item.stabilize) { form.pcmodify.checked = true; Twinkle.protect.formevents.pcmodify({ target: form.pcmodify }); form.pclevel.value = item.stabilize; Twinkle.protect.formevents.pclevel({ target: form.pclevel }); } else { form.pcmodify.checked = false; Twinkle.protect.formevents.pcmodify({ target: form.pcmodify }); } form.pcexpiry.value = item.expiry || '1 month'; } } else { if (item.create) { form.createlevel.value = item.create; Twinkle.protect.formevents.createlevel({ target: form.createlevel }); } form.createexpiry.value = item.expiry || 'infinity'; } const reasonField = actiontype === 'protect' ? form.protectReason : form.reason; if (item.reason) { reasonField.value = item.reason; } else { reasonField.value = ''; } // Add any annotations Twinkle.protect.callback.annotateProtectReason(e); // sort out tagging options, disabled if nonexistent, lua, or TimedText if (mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710) { if (form.category.value === 'unprotect') { form.tagtype.value = 'none'; } else { form.tagtype.value = item.template ? item.template : form.category.value; } Twinkle.protect.formevents.tagtype({ target: form.tagtype }); // Default settings for adding <noinclude> tags to protection templates const isTemplateEditorProtection = form.category.value === 'pp-template'; const isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:Articles for deletion/'); const isNotTemplateNamespace = mw.config.get('wgNamespaceNumber') !== 10; const isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel')); if ((isTemplateEditorProtection || isAFD) && !isCode) { form.noinclude.checked = true; } else if (isCode || isNotTemplateNamespace) { form.noinclude.checked = false; } } } else { // RPP request if (form.category.value === 'unprotect') { form.expiry.value = ''; form.expiry.disabled = true; } else { form.expiry.value = ''; form.expiry.disabled = false; } } }; Twinkle.protect.callback.evaluate = function twinkleprotectCallbackEvaluate(e) { const form = e.target; const input = Morebits.QuickForm.getInputData(form); let tagparams; if (input.actiontype === 'tag' || (input.actiontype === 'protect' && mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */)) { tagparams = { tag: input.tagtype, reason: false, small: input.small, noinclude: input.noinclude }; } switch (input.actiontype) { case 'protect': // protect the page Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = 'Protection complete'; var statusInited = false; var thispage; var allDone = function twinkleprotectCallbackAllDone() { if (thispage) { thispage.getStatusElement().info('done'); } if (tagparams) { Twinkle.protect.callbacks.taggingPageInitial(tagparams); } }; var protectIt = function twinkleprotectCallbackProtectIt(next) { thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Protecting page'); if (mw.config.get('wgArticleId')) { if (input.editmodify) { thispage.setEditProtection(input.editlevel, input.editexpiry); } if (input.movemodify) { // Ensure a level has actually been chosen if (input.movelevel) { thispage.setMoveProtection(input.movelevel, input.moveexpiry); } else { alert('You must chose a move protection level!'); return; } } thispage.setWatchlist(Twinkle.getPref('watchProtectedPages')); } else { thispage.setCreateProtection(input.createlevel, input.createexpiry); thispage.setWatchlist(false); } if (input.protectReason) { thispage.setEditSummary(input.protectReason); } else { alert('You must enter a protect reason, which will be inscribed into the protection log.'); return; } if (input.protectReason_notes_rfppRevid && !/^\d+$/.test(input.protectReason_notes_rfppRevid)) { alert('The provided revision ID is malformed. Please see https://en.wikipedia.org/wiki/Help:Permanent_link for information on how to find the correct ID (also called "oldid").'); return; } if (!statusInited) { Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); statusInited = true; } thispage.setChangeTags(Twinkle.changeTags); thispage.protect(next); }; var stabilizeIt = function twinkleprotectCallbackStabilizeIt() { if (thispage) { thispage.getStatusElement().info('done'); } thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Applying pending changes protection'); thispage.setFlaggedRevs(input.pclevel, input.pcexpiry); if (input.protectReason) { thispage.setEditSummary(input.protectReason + Twinkle.summaryAd); // flaggedrevs tag support: [[phab:T247721]] } else { alert('You must enter a protect reason, which will be inscribed into the protection log.'); return; } if (!statusInited) { Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); statusInited = true; } thispage.setWatchlist(Twinkle.getPref('watchProtectedPages')); thispage.stabilize(allDone, (error) => { if (error.errorCode === 'stabilize_denied') { // [[phab:T234743]] thispage.getStatusElement().error('Failed trying to modify pending changes settings, likely due to a mediawiki bug. Other actions (tagging or regular protection) may have taken place. Please reload the page and try again.'); } }); }; if (input.editmodify || input.movemodify || !mw.config.get('wgArticleId')) { if (input.pcmodify) { protectIt(stabilizeIt); } else { protectIt(allDone); } } else if (input.pcmodify) { stabilizeIt(); } else { alert("Please give Twinkle something to do! \nIf you just want to tag the page, you can choose the 'Tag page with protection template' option at the top."); } break; case 'tag': // apply a protection template Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.followRedirect = false; Morebits.wiki.actionCompleted.notice = 'Tagging complete'; Twinkle.protect.callbacks.taggingPageInitial(tagparams); break; case 'request': // file request at RFPP var typename, typereason; switch (input.category) { case 'pp-dispute': case 'pp-vandalism': case 'pp-usertalk': case 'pp-protected': typename = 'full protection'; break; case 'pp-template': typename = 'template protection'; break; case 'pp-30-500-arb': case 'pp-30-500-vandalism': case 'pp-30-500-disruptive': case 'pp-30-500-blp': case 'pp-30-500-sock': case 'pp-30-500': typename = 'extended confirmed protection'; break; case 'pp-sock-noticeboard': case 'pp-semi-vandalism': case 'pp-semi-disruptive': case 'pp-semi-unsourced': case 'pp-semi-usertalk': case 'pp-semi-sock': case 'pp-semi-blp': case 'pp-semi-protected': typename = 'semi-protection'; break; case 'pp-pc-vandalism': case 'pp-pc-blp': case 'pp-pc-protected': case 'pp-pc-unsourced': case 'pp-pc-disruptive': typename = 'pending changes'; break; case 'pp-move': case 'pp-move-dispute': case 'pp-move-indef': case 'pp-move-vandalism': typename = 'move protection'; break; case 'pp-create-offensive': case 'pp-create-blp': case 'pp-create-salt': typename = 'create protection'; break; case 'unprotect': var admins = $.map(Twinkle.protect.currentProtectionLevels, (pl) => { if (!pl.admin || Twinkle.protect.trustedBots.includes(pl.admin)) { return null; } return 'User:' + pl.admin; }); if (admins.length && !confirm('Have you attempted to contact the protecting admins (' + Morebits.array.uniq(admins).join(', ') + ') first?')) { return false; } // otherwise falls through default: typename = 'unprotection'; break; } switch (input.category) { case 'pp-dispute': typereason = 'Content dispute/edit warring'; break; case 'pp-vandalism': case 'pp-semi-vandalism': case 'pp-pc-vandalism': case 'pp-30-500-vandalism': typereason = 'Persistent [[WP:VAND|vandalism]]'; break; case 'pp-semi-disruptive': case 'pp-pc-disruptive': case 'pp-30-500-disruptive': typereason = 'Persistent [[Wikipedia:Disruptive editing|disruptive editing]]'; break; case 'pp-semi-unsourced': case 'pp-pc-unsourced': typereason = 'Persistent addition of [[WP:INTREF|unsourced or poorly sourced content]]'; break; case 'pp-template': typereason = '[[WP:HIGHRISK|High-risk template]]'; break; case 'pp-30-500-arb': typereason = '[[WP:30/500|Arbitration enforcement]]'; break; case 'pp-usertalk': case 'pp-semi-usertalk': typereason = 'Inappropriate use of user talk page while blocked'; break; case 'pp-sock-noticeboard': case 'pp-semi-sock': case 'pp-30-500-sock': typereason = 'Persistent [[WP:SOCK|sockpuppetry]]'; break; case 'pp-semi-blp': case 'pp-pc-blp': case 'pp-30-500-blp': typereason = '[[WP:BLP|BLP]] policy violations'; break; case 'pp-move-dispute': typereason = 'Page title dispute/move warring'; break; case 'pp-move-vandalism': typereason = 'Page-move vandalism'; break; case 'pp-move-indef': typereason = 'Highly visible page'; break; case 'pp-create-offensive': typereason = 'Offensive name'; break; case 'pp-create-blp': typereason = 'Recently deleted [[WP:BLP|BLP]]'; break; case 'pp-create-salt': typereason = 'Repeatedly recreated'; break; default: typereason = ''; break; } var reason = typereason; if (input.reason !== '') { if (typereason !== '') { reason += '\u00A0\u2013 '; // U+00A0 NO-BREAK SPACE; U+2013 EN RULE } reason += input.reason; } if (reason !== '' && reason.charAt(reason.length - 1) !== '.') { reason += '.'; } var rppparams = { reason: reason, typename: typename, category: input.category, expiry: input.expiry }; Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); var rppName = 'Wikipedia:Requests for page protection/Increase'; // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = 'Wikipedia: Requests for page protection'; Morebits.wiki.actionCompleted.notice = 'Nomination completed, redirecting now to the discussion page'; var rppPage = new Morebits.wiki.Page(rppName, 'Requesting protection of page'); rppPage.setFollowRedirect(true); rppPage.setCallbackParameters(rppparams); rppPage.load(Twinkle.protect.callbacks.fileRequest); break; default: alert('twinkleprotect: unknown kind of action'); break; } }; Twinkle.protect.protectReasonAnnotations = []; Twinkle.protect.callback.annotateProtectReason = function twinkleprotectCallbackAnnotateProtectReason(e) { const form = e.target.form; const protectReason = form.protectReason.value.replace(new RegExp('(?:; )?' + mw.util.escapeRegExp(Twinkle.protect.protectReasonAnnotations.join(': '))), ''); if (this.name === 'protectReason_notes_rfpp') { if (this.checked) { Twinkle.protect.protectReasonAnnotations.push(this.value); $(form.protectReason_notes_rfppRevid).parent().show(); } else { Twinkle.protect.protectReasonAnnotations = []; form.protectReason_notes_rfppRevid.value = ''; $(form.protectReason_notes_rfppRevid).parent().hide(); } } else if (this.name === 'protectReason_notes_rfppRevid') { Twinkle.protect.protectReasonAnnotations = Twinkle.protect.protectReasonAnnotations.filter((el) => !el.includes('[[Special:Permalink')); if (e.target.value.length) { const permalink = '[[Special:Permalink/' + e.target.value + '#' + Morebits.pageNameNorm + ']]'; Twinkle.protect.protectReasonAnnotations.push(permalink); } } if (!Twinkle.protect.protectReasonAnnotations.length) { form.protectReason.value = protectReason; } else { form.protectReason.value = (protectReason ? protectReason + '; ' : '') + Twinkle.protect.protectReasonAnnotations.join(': '); } }; Twinkle.protect.callbacks = { taggingPageInitial: function(tagparams) { if (tagparams.tag === 'noop') { Morebits.Status.info('Applying protection template', 'nothing to do'); return; } const protectedPage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging page'); protectedPage.setCallbackParameters(tagparams); protectedPage.load(Twinkle.protect.callbacks.taggingPage); }, taggingPage: function(protectedPage) { const params = protectedPage.getCallbackParameters(); let text = protectedPage.getPageText(); let tag, summary; const oldtag_re = /(?:\/\*)?\s*(?:<noinclude>)?\s*\{\{\s*(pp-[^{}]*?|protected|(?:t|v|s|p-|usertalk-v|usertalk-s|sb|move)protected(?:2)?|protected template|privacy protection)\s*?\}\}\s*(?:<\/noinclude>)?\s*(?:\*\/)?\s*/gi; const re_result = oldtag_re.exec(text); if (re_result) { if (params.tag === 'none' || confirm('{{' + re_result[1] + '}} was found on the page. \nClick OK to remove it, or click Cancel to leave it there.')) { text = text.replace(oldtag_re, ''); } } if (params.tag === 'none') { summary = 'Removing protection template'; } else { tag = params.tag; if (params.reason) { tag += '|reason=' + params.reason; } if (params.small) { tag += '|small=yes'; } if (/^\s*#redirect/i.test(text)) { // redirect page // Only tag if no {{rcat shell}} is found if (!text.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh)/i)) { text = text.replace(/#REDIRECT ?(\[\[.*?\]\])(.*)/i, '#REDIRECT $1$2\n\n{{' + tag + '}}'); } else { Morebits.Status.info('Redirect category shell present', 'nothing to do'); return; } } else { const needsTagToBeCommentedOut = ['javascript', 'css', 'sanitized-css'].includes(protectedPage.getContentModel()); if (needsTagToBeCommentedOut) { if (params.noinclude) { tag = '/* <noinclude>{{' + tag + '}}</noinclude> */'; } else { tag = '/* {{' + tag + '}} */\n'; } // Prepend tag at very top text = tag + text; } else { if (params.noinclude) { tag = '<noinclude>{{' + tag + '}}</noinclude>'; if (text.startsWith('==')) { tag += '\n'; // a newline is needed to prevent section headings at the very beginning of the page from breaking } } else { tag = '{{' + tag + '}}\n'; } // Insert tag after short description or any hatnotes const wikipage = new Morebits.wikitext.Page(text); text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText(); } } summary = 'Adding {{' + params.tag + '}}'; } protectedPage.setEditSummary(summary); protectedPage.setChangeTags(Twinkle.changeTags); protectedPage.setWatchlist(Twinkle.getPref('watchPPTaggedPages')); protectedPage.setPageText(text); protectedPage.setCreateOption('nocreate'); protectedPage.suppressProtectWarning(); // no need to let admins know they are editing through protection protectedPage.save(); }, fileRequest: function(rppPage) { const rppPage2 = new Morebits.wiki.Page('Wikipedia:Requests for page protection/Decrease', 'Loading requests pages'); rppPage2.load(() => { const params = rppPage.getCallbackParameters(); let text = rppPage.getPageText(); const statusElement = rppPage.getStatusElement(); let text2 = rppPage2.getPageText(); const rppRe = new RegExp('===\\s*(\\[\\[)?\\s*:?\\s*' + Morebits.string.escapeRegExp(Morebits.pageNameNorm) + '\\s*(\\]\\])?\\s*===', 'm'); const tag = rppRe.exec(text) || rppRe.exec(text2); const rppLink = document.createElement('a'); rppLink.setAttribute('href', mw.util.getUrl('Wikipedia:Requests for page protection')); rppLink.appendChild(document.createTextNode('Wikipedia:Requests for page protection')); if (tag) { statusElement.error([ 'There is already a protection request for this page at ', rppLink, ', aborting.' ]); return; } let newtag = '=== [[:' + Morebits.pageNameNorm + ']] ===\n'; if (new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text) || new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text2)) { statusElement.error([ 'There is already a protection request for this page at ', rppLink, ', aborting.' ]); return; } newtag += '* {{pagelinks|1=' + Morebits.pageNameNorm + '}}\n\n'; let words; switch (params.expiry) { case 'temporary': words = 'Temporary '; break; case 'infinity': words = 'Indefinite '; break; default: words = ''; break; } words += params.typename; newtag += "'''" + Morebits.string.toUpperCaseFirstChar(words) + (params.reason !== '' ? ":''' " + Morebits.string.formatReasonText(params.reason) : ".'''") + ' ~~~~'; // If either protection type results in a increased status, then post it under increase // else we post it under decrease let increase = false; const protInfo = Twinkle.protect.protectionPresetsInfo[params.category]; // function to compute protection weights (see comment at Twinkle.protect.protectionWeight) const computeWeight = function(mainLevel, stabilizeLevel) { let result = Twinkle.protect.protectionWeight[mainLevel || 'all']; if (stabilizeLevel) { if (result) { if (stabilizeLevel.level === 'autoconfirmed') { result += 2; } } else { result = Twinkle.protect.protectionWeight['flaggedrevs_' + stabilizeLevel]; } } return result; }; // compare the page's current protection weights with the protection we are requesting const editWeight = computeWeight(Twinkle.protect.currentProtectionLevels.edit && Twinkle.protect.currentProtectionLevels.edit.level, Twinkle.protect.currentProtectionLevels.stabilize && Twinkle.protect.currentProtectionLevels.stabilize.level); if (computeWeight(protInfo.edit, protInfo.stabilize) > editWeight || computeWeight(protInfo.move) > computeWeight(Twinkle.protect.currentProtectionLevels.move && Twinkle.protect.currentProtectionLevels.move.level) || computeWeight(protInfo.create) > computeWeight(Twinkle.protect.currentProtectionLevels.create && Twinkle.protect.currentProtectionLevels.create.level)) { increase = true; } if (increase) { const originalTextLength = text.length; text += '\n' + newtag; if (text.length === originalTextLength) { const linknode = document.createElement('a'); linknode.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing RPP')); linknode.appendChild(document.createTextNode('How to fix RPP')); statusElement.error([ 'Could not find relevant heading on WP:RPP. To fix this problem, please see ', linknode, '.' ]); return; } statusElement.status('Adding new request...'); rppPage.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Requesting ' + params.typename + (params.typename === 'pending changes' ? ' on [[:' : ' of [[:') + Morebits.pageNameNorm + ']].'); rppPage.setChangeTags(Twinkle.changeTags); rppPage.setPageText(text); rppPage.setCreateOption('recreate'); rppPage.save(() => { // Watch the page being requested const watchPref = Twinkle.getPref('watchRequestedPages'); // action=watch has no way to rely on user preferences (T262912), so we do it manually. // The watchdefault pref appears to reliably return '1' (string), // but that's not consistent among prefs so might as well be "correct" const watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10)); if (watch) { const watch_query = { action: 'watch', titles: mw.config.get('wgPageName'), token: mw.user.tokens.get('watchToken') }; // Only add the expiry if page is unwatched or already temporarily watched if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') { watch_query.expiry = watchPref; } new Morebits.wiki.Api('Adding requested page to watchlist', watch_query).post(); } }); } else { const originalTextLength2 = text2.length; text2 += '\n' + newtag; if (text2.length === originalTextLength2) { const linknode2 = document.createElement('a'); linknode2.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing RPP')); linknode2.appendChild(document.createTextNode('How to fix RPP')); statusElement.error([ 'Could not find relevant heading on WP:RPP. To fix this problem, please see ', linknode2, '.' ]); return; } statusElement.status('Adding new request...'); rppPage2.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Requesting ' + params.typename + (params.typename === 'pending changes' ? ' on [[:' : ' of [[:') + Morebits.pageNameNorm + ']].'); rppPage2.setChangeTags(Twinkle.changeTags); rppPage2.setPageText(text2); rppPage2.setCreateOption('recreate'); rppPage2.save(() => { // Watch the page being requested const watchPref = Twinkle.getPref('watchRequestedPages'); // action=watch has no way to rely on user preferences (T262912), so we do it manually. // The watchdefault pref appears to reliably return '1' (string), // but that's not consistent among prefs so might as well be "correct" const watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10)); if (watch) { const watch_query = { action: 'watch', titles: mw.config.get('wgPageName'), token: mw.user.tokens.get('watchToken') }; // Only add the expiry if page is unwatched or already temporarily watched if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') { watch_query.expiry = watchPref; } new Morebits.wiki.Api('Adding requested page to watchlist', watch_query).post(); } }); } }); } }; Twinkle.addInitCallback(Twinkle.protect, 'protect'); }()); // </nowiki> nrjaodyt2ynevgtsmownjid8uoboxwm MediaWiki:Gadget-twinkletag.js 8 24480 268717 2026-04-27T16:49:00Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinkletag.js: Tag module **************************************** * Mode of invocation: Tab ("Tag") * Active on: Existing articles and drafts; file pages with a corresponding file * which is local (not on Commons); all redirects */ Twinkle.tag = function twinkletag() { // redirect tagging (exclude category redirects, which are all soft re...' 268717 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinkletag.js: Tag module **************************************** * Mode of invocation: Tab ("Tag") * Active on: Existing articles and drafts; file pages with a corresponding file * which is local (not on Commons); all redirects */ Twinkle.tag = function twinkletag() { // redirect tagging (exclude category redirects, which are all soft redirects and so shouldn't be tagged with rcats) if (Morebits.isPageRedirect() && mw.config.get('wgNamespaceNumber') !== 14) { Twinkle.tag.mode = 'redirect'; Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Tag redirect'); // file tagging } else if (mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById('mw-sharedupload') && document.getElementById('mw-imagepage-section-filehistory')) { Twinkle.tag.mode = 'file'; Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add maintenance tags to file'); // article/draft article tagging } else if ([0, 118].includes(mw.config.get('wgNamespaceNumber')) && mw.config.get('wgCurRevisionId')) { Twinkle.tag.mode = 'article'; // Can't remove tags when not viewing current version Twinkle.tag.canRemove = (mw.config.get('wgCurRevisionId') === mw.config.get('wgRevisionId')) && // Disabled on latest diff because the diff slider could be used to slide // away from the latest diff without causing the script to reload !mw.config.get('wgDiffNewId'); Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add or remove article maintenance tags'); } }; Twinkle.tag.checkedTags = []; Twinkle.tag.callback = function twinkletagCallback() { const Window = new Morebits.SimpleWindow(630, Twinkle.tag.mode === 'article' ? 500 : 400); Window.setScriptName('Twinkle'); // anyone got a good policy/guideline/info page/instructional page link?? Window.addFooterLink('Tag prefs', 'WP:TW/PREF#tag'); Window.addFooterLink('Twinkle help', 'WP:TW/DOC#tag'); Window.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.tag.callback.evaluate); // if page is unreviewed, add a checkbox to the form so that user can pick whether or not to review it const isPatroller = mw.config.get('wgUserGroups').some((r) => ['patroller', 'sysop'].includes(r)); if (isPatroller) { new mw.Api().get({ action: 'pagetriagelist', format: 'json', page_id: mw.config.get('wgArticleId') }).then((response) => { // Figure out whether the article is marked as reviewed in PageTriage. // Recent articles will have a patrol_status that we can read. // For articles that have been out of the new pages feed for awhile, pages[0] will be undefined. const isReviewed = response.pagetriagelist.pages[0] ? response.pagetriagelist.pages[0].patrol_status > 0 : true; // if article is not marked as reviewed, show the "mark as reviewed" check box if (!isReviewed) { // Quickform is probably already rendered. Instead of using form.append(), we need to make an element and then append it using JQuery. const checkbox = new Morebits.QuickForm.Element({ type: 'checkbox', list: [ { label: 'Mark the page as patrolled/reviewed', value: 'patrol', name: 'patrol', checked: Twinkle.getPref('markTaggedPagesAsPatrolled') } ] }); const html = checkbox.render(); $('.quickform').prepend(html); } }); } form.append({ type: 'input', label: 'Filter tag list:', name: 'quickfilter', size: '30', event: function twinkletagquickfilter() { // flush the DOM of all existing underline spans $allCheckboxDivs.find('.search-hit').each((i, e) => { const labelElement = e.parentElement; // This would convert <label>Hello <span class=search-hit>wo</span>rld</label> // to <label>Hello world</label> labelElement.innerHTML = labelElement.textContent; }); if (this.value) { $allCheckboxDivs.hide(); $allHeaders.hide(); const searchString = this.value; const searchRegex = new RegExp(mw.util.escapeRegExp(searchString), 'i'); $allCheckboxDivs.find('label').each(function () { const labelText = this.textContent; const searchHit = searchRegex.exec(labelText); if (searchHit) { const range = document.createRange(); const textnode = this.childNodes[0]; range.selectNodeContents(textnode); range.setStart(textnode, searchHit.index); range.setEnd(textnode, searchHit.index + searchString.length); const underlineSpan = $('<span>').addClass('search-hit').css('text-decoration', 'underline')[0]; range.surroundContents(underlineSpan); this.parentElement.style.display = 'block'; // show } }); } else { $allCheckboxDivs.show(); $allHeaders.show(); } } }); switch (Twinkle.tag.mode) { case 'article': Window.setTitle('Article maintenance tagging'); // Build sorting and lookup object flatObject, which is always // needed but also used to generate the alphabetical list Twinkle.tag.article.flatObject = {}; Object.values(Twinkle.tag.article.tagList).forEach((group) => { Object.values(group).forEach((subgroup) => { if (Array.isArray(subgroup)) { subgroup.forEach((item) => { Twinkle.tag.article.flatObject[item.tag] = item; }); } else { Twinkle.tag.article.flatObject[subgroup.tag] = subgroup; } }); }); form.append({ type: 'select', name: 'sortorder', label: 'View this list:', tooltip: 'You can change the default view order in your Twinkle preferences (WP:TWPREFS).', event: Twinkle.tag.updateSortOrder, list: [ { type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getPref('tagArticleSortOrder') === 'cat' }, { type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getPref('tagArticleSortOrder') === 'alpha' } ] }); if (!Twinkle.tag.canRemove) { const divElement = document.createElement('div'); divElement.innerHTML = 'For removal of existing tags, please open Tag menu from the current version of article'; form.append({ type: 'div', name: 'untagnotice', label: divElement }); } form.append({ type: 'div', id: 'tagWorkArea', className: 'morebits-scrollbox' }); form.append({ type: 'checkbox', list: [ { label: 'Group inside {{multiple issues}} if possible', value: 'group', name: 'group', tooltip: 'If applying two or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.', checked: Twinkle.getPref('groupByDefault') } ] }); form.append({ type: 'input', label: 'Reason', name: 'reason', tooltip: 'Optional reason to be appended in edit summary. Recommended when removing tags.', size: '60' }); break; case 'file': Window.setTitle('File maintenance tagging'); $.each(Twinkle.tag.fileList, (groupName, group) => { form.append({ type: 'header', label: groupName }); form.append({ type: 'checkbox', name: 'tags', list: group }); }); if (Twinkle.getPref('customFileTagList').length) { form.append({ type: 'header', label: 'Custom tags' }); form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customFileTagList') }); } break; case 'redirect': Window.setTitle('Redirect tagging'); // If a tag has a restriction for this namespace or title, return true, so that we know not to display it in the list of check boxes. var isRestricted = function(item) { if (typeof item.restriction === 'undefined') { return false; } const namespace = mw.config.get('wgNamespaceNumber'); switch (item.restriction) { case 'insideMainspaceOnly': if (namespace !== 0) { return true; } break; case 'outsideUserspaceOnly': if (namespace === 2 || namespace === 3) { return true; } break; case 'insideTalkNamespaceOnly': if (namespace % 2 !== 1 || namespace < 0) { return true; } break; case 'disambiguationPagesOnly': if (!mw.config.get('wgPageName').endsWith('_(disambiguation)')) { return true; } break; default: alert('Twinkle.tag: unknown restriction ' + item.restriction); break; } return false; }; // Generate the HTML form with the list of redirect tags that the user can choose to apply. var i = 1; $.each(Twinkle.tag.redirectList, (groupName, group) => { form.append({ type: 'header', id: 'tagHeader' + i, label: groupName }); const subdiv = form.append({ type: 'div', id: 'tagSubdiv' + i++ }); $.each(group, (subgroupName, subgroup) => { subdiv.append({ type: 'div', label: [ Morebits.htmlNode('b', subgroupName) ] }); subdiv.append({ type: 'checkbox', name: 'tags', list: subgroup .filter((item) => !isRestricted(item)) .map((item) => ({ value: item.tag, label: '{{' + item.tag + '}}: ' + item.description, subgroup: item.subgroup })) }); }); }); if (Twinkle.getPref('customRedirectTagList').length) { form.append({ type: 'header', label: 'Custom tags' }); form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customRedirectTagList') }); } break; default: alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode); break; } form.append({ type: 'submit', className: 'tw-tag-submit' }); const result = form.render(); Window.setContent(result); Window.display(); // for quick filter: $allCheckboxDivs = $(result).find('[name$=tags]').parent(); $allHeaders = $(result).find('h5, .quickformDescription'); result.quickfilter.focus(); // place cursor in the quick filter field as soon as window is opened result.quickfilter.autocomplete = 'off'; // disable browser suggestions result.quickfilter.addEventListener('keypress', (e) => { if (e.keyCode === 13) { // prevent enter key from accidentally submitting the form e.preventDefault(); return false; } }); if (Twinkle.tag.mode === 'article') { Twinkle.tag.alreadyPresentTags = []; if (Twinkle.tag.canRemove) { // Look for existing maintenance tags in the lead section and put them in array // All tags are HTML table elements that are direct children of .mw-parser-output, // except when they are within {{multiple issues}} $('.mw-parser-output').children().each((i, e) => { // break out on encountering the first heading, which means we are no // longer in the lead section if (e.classList.contains('mw-heading')) { return false; } // The ability to remove tags depends on the template's {{ambox}} |name= // parameter bearing the template's correct name (preferably) or a name that at // least redirects to the actual name // All tags have their first class name as "box-" + template name if (e.className.indexOf('box-') === 0) { if (e.classList[0] === 'box-Multiple_issues') { $(e).find('.ambox').each((idx, e) => { if (e.classList[0].indexOf('box-') === 0) { const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' '); Twinkle.tag.alreadyPresentTags.push(tag); } }); return true; // continue } const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' '); Twinkle.tag.alreadyPresentTags.push(tag); } }); // {{Uncategorized}} and {{Improve categories}} are usually placed at the end if ($('.box-Uncategorized').length) { Twinkle.tag.alreadyPresentTags.push('Uncategorized'); } if ($('.box-Improve_categories').length) { Twinkle.tag.alreadyPresentTags.push('Improve categories'); } } // Add status text node after Submit button const statusNode = document.createElement('small'); statusNode.id = 'tw-tag-status'; Twinkle.tag.status = { // initial state; defined like this because these need to be available for reference // in the click event handler numAdded: 0, numRemoved: 0 }; $('button.tw-tag-submit').after(statusNode); // fake a change event on the sort dropdown, to initialize the tag list const evt = document.createEvent('Event'); evt.initEvent('change', true, true); result.sortorder.dispatchEvent(evt); } else { // Redirects and files: Add a link to each template's description page Morebits.QuickForm.getElements(result, 'tags').forEach(generateLinks); } }; // $allCheckboxDivs and $allHeaders are defined globally, rather than in the // quickfilter event function, to avoid having to recompute them on every keydown let $allCheckboxDivs, $allHeaders; Twinkle.tag.updateSortOrder = function(e) { const form = e.target.form; const sortorder = e.target.value; Twinkle.tag.checkedTags = form.getChecked('tags'); const container = new Morebits.QuickForm.Element({ type: 'fragment' }); // function to generate a checkbox, with appropriate subgroup if needed const makeCheckbox = function (item) { const tag = item.tag, description = item.description; const checkbox = { value: tag, label: '{{' + tag + '}}: ' + description }; if (Twinkle.tag.checkedTags.includes(tag)) { checkbox.checked = true; } checkbox.subgroup = item.subgroup; return checkbox; }; const makeCheckboxesForAlreadyPresentTags = function() { container.append({ type: 'header', id: 'tagHeader0', label: 'Tags already present' }); const subdiv = container.append({ type: 'div', id: 'tagSubdiv0' }); const checkboxes = []; const unCheckedTags = e.target.form.getUnchecked('existingTags'); Twinkle.tag.alreadyPresentTags.forEach((tag) => { const checkbox = { value: tag, label: '{{' + tag + '}}' + (Twinkle.tag.article.flatObject[tag] ? ': ' + Twinkle.tag.article.flatObject[tag].description : ''), checked: !unCheckedTags.includes(tag), style: 'font-style: italic' }; checkboxes.push(checkbox); }); subdiv.append({ type: 'checkbox', name: 'existingTags', list: checkboxes }); }; if (sortorder === 'cat') { // categorical sort order // function to iterate through the tags and create a checkbox for each one const doCategoryCheckboxes = function(subdiv, subgroup) { const checkboxes = []; $.each(subgroup, (k, item) => { if (!Twinkle.tag.alreadyPresentTags.includes(item.tag)) { checkboxes.push(makeCheckbox(item)); } }); subdiv.append({ type: 'checkbox', name: 'tags', list: checkboxes }); }; if (Twinkle.tag.alreadyPresentTags.length > 0) { makeCheckboxesForAlreadyPresentTags(); } let i = 1; // go through each category and sub-category and append lists of checkboxes $.each(Twinkle.tag.article.tagList, (groupName, group) => { container.append({ type: 'header', id: 'tagHeader' + i, label: groupName }); const subdiv = container.append({ type: 'div', id: 'tagSubdiv' + i++ }); if (Array.isArray(group)) { doCategoryCheckboxes(subdiv, group); } else { $.each(group, (subgroupName, subgroup) => { subdiv.append({ type: 'div', label: [ Morebits.htmlNode('b', subgroupName) ] }); doCategoryCheckboxes(subdiv, subgroup); }); } }); } else { // alphabetical sort order if (Twinkle.tag.alreadyPresentTags.length > 0) { makeCheckboxesForAlreadyPresentTags(); container.append({ type: 'header', id: 'tagHeader1', label: 'Available tags' }); } // Avoid repeatedly resorting Twinkle.tag.article.alphabeticalList = Twinkle.tag.article.alphabeticalList || Object.keys(Twinkle.tag.article.flatObject).sort(); const checkboxes = []; Twinkle.tag.article.alphabeticalList.forEach((tag) => { if (!Twinkle.tag.alreadyPresentTags.includes(tag)) { checkboxes.push(makeCheckbox(Twinkle.tag.article.flatObject[tag])); } }); container.append({ type: 'checkbox', name: 'tags', list: checkboxes }); } // append any custom tags if (Twinkle.getPref('customTagList').length) { container.append({ type: 'header', label: 'Custom tags' }); container.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customTagList').map((el) => { el.checked = Twinkle.tag.checkedTags.includes(el.value); return el; }) }); } const $workarea = $(form).find('#tagWorkArea'); const rendered = container.render(); $workarea.empty().append(rendered); // for quick filter: $allCheckboxDivs = $workarea.find('[name=tags], [name=existingTags]').parent(); $allHeaders = $workarea.find('h5, .quickformDescription'); form.quickfilter.value = ''; // clear search, because the search results are not preserved over mode change form.quickfilter.focus(); // style adjustments $workarea.find('h5').css({ 'font-size': '110%' }); $workarea.find('h5:not(:first-child)').css({ 'margin-top': '1em' }); $workarea.find('div').filter(':has(span.quickformDescription)').css({ 'margin-top': '0.4em' }); Morebits.QuickForm.getElements(form, 'existingTags').forEach(generateLinks); Morebits.QuickForm.getElements(form, 'tags').forEach(generateLinks); // tally tags added/removed, update statusNode text const statusNode = document.getElementById('tw-tag-status'); $('[name=tags], [name=existingTags]').on('click', function() { if (this.name === 'tags') { Twinkle.tag.status.numAdded += this.checked ? 1 : -1; } else if (this.name === 'existingTags') { Twinkle.tag.status.numRemoved += this.checked ? -1 : 1; } const firstPart = 'Adding ' + Twinkle.tag.status.numAdded + ' tag' + (Twinkle.tag.status.numAdded > 1 ? 's' : ''); const secondPart = 'Removing ' + Twinkle.tag.status.numRemoved + ' tag' + (Twinkle.tag.status.numRemoved > 1 ? 's' : ''); statusNode.textContent = (Twinkle.tag.status.numAdded ? ' ' + firstPart : '') + (Twinkle.tag.status.numRemoved ? (Twinkle.tag.status.numAdded ? '; ' : ' ') + secondPart : ''); }); }; /** * Adds a link to each template's description page * * @param {Morebits.QuickForm.Element} checkbox associated with the template */ var generateLinks = function(checkbox) { const link = Morebits.htmlNode('a', '>'); link.setAttribute('class', 'tag-template-link'); const tagname = checkbox.values; link.setAttribute('href', mw.util.getUrl( (!tagname.includes(':') ? 'Template:' : '') + (!tagname.includes('|') ? tagname : tagname.slice(0, tagname.indexOf('|'))) )); link.setAttribute('target', '_blank'); $(checkbox).parent().append(['\u00A0', link]); }; // Tags for ARTICLES start here Twinkle.tag.article = {}; // Shared across {{Rough translation}} and {{Not English}} const translationSubgroups = [ { name: 'translationLanguage', parameter: '1', type: 'input', label: 'Language of article (if known):', tooltip: 'Consider looking at [[WP:LRC]] for help. If listing the article at PNT, please try to avoid leaving this box blank, unless you are completely unsure.' } ].concat(mw.config.get('wgNamespaceNumber') === 0 ? [ { type: 'checkbox', list: [ { name: 'translationPostAtPNT', label: 'List this article at Wikipedia:Pages needing translation into English (PNT)', checked: true } ] }, { name: 'translationComments', type: 'textarea', label: 'Additional comments to post at PNT', tooltip: 'Optional, and only relevant if "List this article ..." above is checked.' } ] : []); // Tags arranged by category; will be used to generate the alphabetical list, // but tags should be in alphabetical order within the categories // excludeMI: true indicate a tag that *does not* work inside {{multiple issues}} // Add new categories with discretion - the list is long enough as is! Twinkle.tag.article.tagList = { 'Cleanup and maintenance tags': { 'General cleanup': [ { tag: 'Cleanup', description: 'requires cleanup', subgroup: { name: 'cleanup', parameter: 'reason', type: 'input', label: 'Specific reason why cleanup is needed:', tooltip: 'Required.', size: 35, required: true } }, // has a subgroup with text input { tag: 'Cleanup rewrite', description: "needs to be rewritten entirely to comply with Wikipedia's quality standards" }, { tag: 'Copy edit', description: 'requires copy editing for grammar, style, cohesion, tone, or spelling', subgroup: { name: 'copyEdit', parameter: 'for', type: 'input', label: '"This article may require copy editing for..."', tooltip: 'e.g. "consistent spelling". Optional.', size: 35 } } // has a subgroup with text input ], 'Potentially unwanted content': [ { tag: 'Close paraphrasing', description: 'contains close paraphrasing of a non-free copyrighted source', subgroup: { name: 'closeParaphrasing', parameter: 'source', type: 'input', label: 'Source:', tooltip: 'Source that has been closely paraphrased' } }, { tag: 'Copypaste', description: 'appears to have been copied and pasted from another location', excludeMI: true, subgroup: { name: 'copypaste', parameter: 'url', type: 'input', label: 'Source URL:', tooltip: 'If known.', size: 50 } }, // has a subgroup with text input { tag: 'AI-generated', description: 'content appears to be generated by a large language model' }, { tag: 'External links', description: 'external links may not follow content policies or guidelines' }, { tag: 'Non-free', description: 'may contain excessive or improper use of copyrighted materials' } ], 'Structure, formatting, and lead section': [ { tag: 'Cleanup reorganize', description: "needs reorganization to comply with Wikipedia's layout guidelines" }, { tag: 'Lead missing', description: 'no lead section' }, { tag: 'Lead rewrite', description: 'lead section needs to be rewritten to comply with guidelines' }, { tag: 'Lead too long', description: 'lead section is too long for the length of the article' }, { tag: 'Lead too short', description: 'lead section is too short and should be expanded to summarize key points' }, { tag: 'Sections', description: 'needs to be divided into sections by topic' }, { tag: 'Too many sections', description: 'too many section headers dividing up content, should be condensed' }, { tag: 'Very long', description: 'too long to read and navigate comfortably' } ], 'Fiction-related cleanup': [ { tag: 'All plot', description: 'almost entirely a plot summary' }, { tag: 'Fiction', description: 'fails to distinguish between fact and fiction' }, { tag: 'In-universe', description: 'subject is fictional and needs rewriting to provide a non-fictional perspective' }, { tag: 'Long plot', description: 'plot summary is too long or excessively detailed' }, { tag: 'More plot', description: 'plot summary is too short' }, { tag: 'No plot', description: 'needs a plot summary' } ] }, 'General content issues': { 'Importance and notability': [ { tag: 'Notability', description: 'subject may not meet the general notability guideline', subgroup: { name: 'notability', parameter: '1', type: 'select', list: [ { label: "{{notability}}: article's subject may not meet the general notability guideline", value: '' }, { label: '{{notability|Academics}}: notability guideline for academics', value: 'Academics' }, { label: '{{notability|Astro}}: notability guideline for astronomical objects', value: 'Astro' }, { label: '{{notability|Biographies}}: notability guideline for biographies', value: 'Biographies' }, { label: '{{notability|Books}}: notability guideline for books', value: 'Books' }, { label: '{{notability|Companies}}: notability guideline for companies', value: 'Companies' }, { label: '{{notability|Events}}: notability guideline for events', value: 'Events' }, { label: '{{notability|Films}}: notability guideline for films', value: 'Films' }, { label: '{{notability|Geographic}}: notability guideline for geographic features', value: 'Geographic' }, { label: '{{notability|Lists}}: notability guideline for stand-alone lists', value: 'Lists' }, { label: '{{notability|Music}}: notability guideline for music', value: 'Music' }, { label: '{{notability|Neologisms}}: notability guideline for neologisms', value: 'Neologisms' }, { label: '{{notability|Numbers}}: notability guideline for numbers', value: 'Numbers' }, { label: '{{notability|Organizations}}: notability guideline for organizations', value: 'Organizations' }, { label: '{{notability|Products}}: notability guideline for products and services', value: 'Products' }, { label: '{{notability|Sports}}: notability guideline for sports and athletics', value: 'Sports' }, { label: '{{notability|Television}}: notability guideline for television shows', value: 'Television' }, { label: '{{notability|Web}}: notability guideline for web content', value: 'Web' } ] } } ], 'Style of writing': [ { tag: 'Cleanup press release', description: 'reads like a press release or news article', subgroup: { type: 'hidden', name: 'cleanupPR1', parameter: '1', value: 'article' } }, { tag: 'Cleanup tense', description: 'does not follow guidelines on use of different tenses.' }, { tag: 'Essay-like', description: 'written like a personal reflection, personal essay, or argumentative essay' }, { tag: 'Fanpov', description: "written from a fan's point of view" }, { tag: 'Inappropriate person', description: 'uses first-person or second-person inappropiately' }, { tag: 'How-to', description: 'written like a manual or guidebook' }, { tag: 'Over-quotation', description: 'too many or too-lengthy quotations for an encyclopedic entry' }, { tag: 'Promotional', description: 'contains promotional content or is written like an advertisement' }, { tag: 'Prose', description: 'written in a list format but may read better as prose' }, { tag: 'Resume-like', description: 'written like a resume' }, { tag: 'Technical', description: 'too technical for most readers to understand' }, { tag: 'Tone', description: 'tone or style may not reflect the encyclopedic tone used on Wikipedia' } ], 'Sense (or lack thereof)': [ { tag: 'Confusing', description: 'confusing or unclear' }, { tag: 'Unfocused', description: 'lacks focus or is about more than one topic' } ], 'Information and detail': [ { tag: 'Context', description: 'insufficient context for those unfamiliar with the subject' }, { tag: 'Excessive examples', description: 'may contain indiscriminate, excessive, or irrelevant examples' }, { tag: 'Expert needed', description: 'needs attention from an expert on the subject', subgroup: [ { name: 'expertNeeded', parameter: '1', type: 'input', label: 'Name of relevant WikiProject:', tooltip: 'Optionally, enter the name of a WikiProject which might be able to help recruit an expert. Don\'t include the "WikiProject" prefix.' }, { name: 'expertNeededReason', parameter: 'reason', type: 'input', label: 'Reason:', tooltip: 'Short explanation describing the issue. Either Reason or Talk link is required.' }, { name: 'expertNeededTalk', parameter: 'talk', type: 'input', label: 'Talk discussion:', tooltip: 'Name of the section of this article\'s talk page where the issue is being discussed. Do not give a link, just the name of the section. Either Reason or Talk link is required.' } ] }, { tag: 'Overly detailed', description: 'excessive amount of intricate detail' }, { tag: 'Undue weight', description: 'lends undue weight to certain ideas, incidents, or controversies' } ], Timeliness: [ { tag: 'Current', description: 'documents a current event', excludeMI: true }, // Works but not intended for use in MI { tag: 'Current related', description: 'documents a topic affected by a current event', excludeMI: true }, // Works but not intended for use in MI { tag: 'Update', description: 'needs additional up-to-date information added', subgroup: [ { name: 'updatePart', parameter: 'part', type: 'input', label: 'What part of the article:', tooltip: 'Part that needs updating', size: '45' }, { name: 'updateReason', parameter: 'reason', type: 'input', label: 'Reason:', tooltip: 'Explanation why the article is out of date', size: '55' } ] } ], 'Neutrality, bias, and factual accuracy': [ { tag: 'Autobiography', description: 'autobiography and may not be written neutrally' }, { tag: 'COI', description: 'creator or major contributor may have a conflict of interest', subgroup: mw.config.get('wgNamespaceNumber') === 0 ? { name: 'coiReason', type: 'textarea', label: 'Explanation for COI tag (will be posted on this article\'s talk page):', tooltip: 'Optional, but strongly recommended. Leave blank if not wanted.' } : [] }, { tag: 'Disputed', description: 'questionable factual accuracy' }, { tag: 'Fringe theories', description: 'presents fringe theories as mainstream views' }, { tag: 'Globalize', description: 'may not represent a worldwide view of the subject', subgroup: [ { type: 'hidden', name: 'globalize1', parameter: '1', value: 'article' }, { name: 'globalizeRegion', parameter: '2', type: 'input', label: 'Over-represented country or region' } ] }, { tag: 'Hoax', description: 'may partially or completely be a hoax' }, { tag: 'Paid contributions', description: 'contains paid contributions, and may therefore require cleanup' }, { tag: 'Peacock', description: 'contains wording that promotes the subject in a subjective manner without adding information' }, { tag: 'POV', description: 'does not maintain a neutral point of view' }, { tag: 'Recentism', description: 'slanted towards recent events' }, { tag: 'Too few opinions', description: 'may not include all significant viewpoints' }, { tag: 'Undisclosed paid', description: 'may have been created or edited in return for undisclosed payments' }, { tag: 'Weasel', description: 'neutrality or verifiability is compromised by the use of weasel words' } ], 'Verifiability and sources': [ { tag: 'BLP no footnotes', description: 'BLP that lacks inline citations'}, { tag: 'BLP one source', description: 'BLP that relies largely or entirely on a single source' }, { tag: 'BLP sources', description: 'BLP that needs additional references or sources for verification' }, { tag: 'BLP unreferenced', description: 'BLP does not cite any sources at all (use BLP PROD instead for new articles)' }, { tag: 'More citations needed', description: 'needs additional references or sources for verification' }, { tag: 'No footnotes', description: 'has references, but lacks inline citations' }, { tag: 'No significant coverage', description: 'does not cite any sources containing significant coverage' }, { tag: 'No significant coverage (sports)', description: 'sports biography that does not cite any sources containing significant coverage' }, { tag: 'One source', description: 'relies largely or entirely on a single source' }, { tag: 'Only primary sources', description: 'relies only on references to primary sources, and needs secondary sources' }, { tag: 'Original research', description: 'contains original research' }, { tag: 'Primary sources', description: 'relies too much on references to primary sources, and needs secondary sources' }, { tag: 'Self-published', description: 'contains excessive or inappropriate references to self-published sources' }, { tag: 'Sources exist', description: 'notable topic, sources are available that could be added to article' }, { tag: 'Third-party', description: 'relies too heavily on sources too closely associated with the subject' }, { tag: 'Unreferenced', description: 'does not cite any sources at all' }, { tag: 'Unreliable sources', description: 'some references may not be reliable' }, { tag: 'User-generated', description: 'contains many references to user-generated (self-published) content'} ] }, 'Specific content issues': { Accessibility: [ { tag: 'Cleanup colors', description: 'uses color as only way to convey information' }, { tag: 'Overcoloured', description: 'overuses color' }, { tag: 'Dark mode problems', description: 'has problems when viewed in dark mode' } ], Language: [ { tag: 'Not English', description: 'written in a language other than English and needs translation', excludeMI: true, subgroup: translationSubgroups.slice(0, 1).concat([{ type: 'checkbox', list: [ { name: 'translationNotify', label: 'Notify article creator', checked: true, tooltip: "Places {{uw-notenglish}} on the creator's talk page." } ] }]).concat(translationSubgroups.slice(1)) }, { tag: 'Rough translation', description: 'poor translation from another language', excludeMI: true, subgroup: translationSubgroups }, { tag: 'Expand language', description: 'should be expanded with text translated from a foreign-language article', excludeMI: true, subgroup: [{ type: 'hidden', name: 'expandLangTopic', parameter: 'topic', value: '', required: true // force empty topic param in output }, { name: 'expandLanguageLangCode', parameter: 'langcode', type: 'input', label: 'Language code:', tooltip: 'Language code of the language from which article is to be expanded from', required: true }, { name: 'expandLanguageArticle', parameter: 'otherarticle', type: 'input', label: 'Name of article:', tooltip: 'Name of article to be expanded from, without the interwiki prefix' }] } ], Links: [ { tag: 'Dead end', description: 'article has no links to other articles' }, { tag: 'Orphan', description: 'linked to from no other articles' }, { tag: 'Overlinked', description: 'too many duplicate and/or irrelevant links to other articles' }, { tag: 'Underlinked', description: 'needs more wikilinks to other articles' } ], 'Referencing technique': [ { tag: 'Citation style', description: 'unclear or inconsistent citation style' }, { tag: 'Cleanup bare URLs', description: 'uses bare URLs for references, which are prone to link rot' }, { tag: 'More footnotes needed', description: 'has some references, but insufficient inline citations' }, { tag: 'Parenthetical referencing', description: 'uses parenthetical referencing, which is deprecated on Wikipedia' } ], Categories: [ { tag: 'Improve categories', description: 'needs additional or more specific categories', excludeMI: true }, { tag: 'Uncategorized', description: 'not added to any categories', excludeMI: true } ] }, Merging: [ { tag: 'History merge', description: 'another page should be history merged into this one', excludeMI: true, subgroup: [ { name: 'histmergeOriginalPage', parameter: 'originalpage', type: 'input', label: 'Other article:', tooltip: 'Name of the page that should be merged into this one (required).', required: true }, { name: 'histmergeReason', parameter: 'reason', type: 'input', label: 'Reason:', tooltip: 'Short explanation describing the reason a history merge is needed. Should probably begin with "because" and end with a period.' }, { name: 'histmergeSysopDetails', parameter: 'details', type: 'input', label: 'Extra details:', tooltip: 'For complex cases, provide extra instructions for the reviewing administrator.' } ] } ], Splitting: [ { tag: 'Split', description: 'should be split into multiple pages' }, { tag: 'Split dab', description: 'disambiguation page should be split into multiple pages' } ], Informational: [ { tag: 'GOCEinuse', description: 'currently undergoing a major copy edit by the Guild of Copy Editors', excludeMI: true }, { tag: 'In use', description: 'undergoing a major edit for a short while', excludeMI: true }, { tag: 'Under construction', description: 'in the process of an expansion or major restructuring', excludeMI: true } ] }; // Tags for REDIRECTS start here // Not by policy, but the list roughly approximates items with >500 // transclusions from Template:R template index Twinkle.tag.redirectList = { 'Grammar, punctuation, and spelling': { Abbreviation: [ { tag: 'R from acronym', description: 'redirect from an acronym (e.g. POTUS) to its expanded form', restriction: 'insideMainspaceOnly' }, { tag: 'R from airport code', description: 'redirect from an airport\'s IATA or ICAO code to that airport\'s article', restriction: 'insideMainspaceOnly' }, { tag: 'R from airline code', description: 'redirect from an airline\'s IATA or ICAO code to that airline\'s article', restriction: 'insideMainspaceOnly' }, { tag: 'R from initialism', description: 'redirect from an initialism (e.g. AGF) to its expanded form', restriction: 'insideMainspaceOnly' }, { tag: 'R from MathSciNet abbreviation', description: 'redirect from MathSciNet publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' }, { tag: 'R from NLM abbreviation', description: 'redirect from a NLM publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' } ], Capitalisation: [ { tag: 'R from CamelCase', description: 'redirect from a CamelCase title' }, { tag: 'R from other capitalisation', description: 'redirect from a title with another method of capitalisation', restriction: 'insideMainspaceOnly' }, { tag: 'R from miscapitalisation', description: 'redirect from a capitalisation error' } ], 'Grammar & punctuation': [ { tag: 'R from modification', description: 'redirect from a modification of the target\'s title, such as with words rearranged' }, { tag: 'R from plural', description: 'redirect from a plural word to the singular equivalent', restriction: 'insideMainspaceOnly' }, { tag: 'R to plural', description: 'redirect from a singular noun to its plural form', restriction: 'insideMainspaceOnly' } ], 'Parts of speech': [ { tag: 'R from verb', description: 'redirect from an English-language verb or verb phrase', restriction: 'insideMainspaceOnly' }, { tag: 'R from adjective', description: 'redirect from an adjective (word or phrase that describes a noun)', restriction: 'insideMainspaceOnly' } ], Spelling: [ { tag: 'R from alternative spelling', description: 'redirect from a title with a different spelling' }, { tag: 'R from alternative transliteration', description: 'redirect from an alternative English transliteration to a more common variation' }, { tag: 'R from ASCII-only', description: 'redirect from a title in only basic ASCII to the formal title, with differences that are not diacritical marks or ligatures' }, { tag: 'R to ASCII-only', description: 'redirect to a title in only basic ASCII from the formal title, with differences that are not diacritical marks or ligatures' }, { tag: 'R from diacritic', description: 'redirect from a page name that has diacritical marks (accents, umlauts, etc.)' }, { tag: 'R to diacritic', description: 'redirect to the article title with diacritical marks (accents, umlauts, etc.)' }, { tag: 'R from misspelling', description: 'redirect from a misspelling or typographical error' } ] }, 'Alternative names': { General: [ { tag: 'R from alternative language', description: 'redirect from or to a title in another language', subgroup: [ { name: 'altLangFrom', type: 'input', label: 'From language (two-letter code):', tooltip: 'Enter the two-letter code of the language the redirect name is in; such as en for English, de for German' }, { name: 'altLangTo', type: 'input', label: 'To language (two-letter code):', tooltip: 'Enter the two-letter code of the language the target name is in; such as en for English, de for German' }, { name: 'altLangInfo', type: 'div', label: $.parseHTML('<p>For a list of language codes, see <a href="/wiki/Wp:Template_messages/Redirect_language_codes">Wikipedia:Template messages/Redirect language codes</a></p>') } ] }, { tag: 'R from alternative name', description: 'redirect from a title that is another name, a pseudonym, a nickname, or a synonym' }, { tag: 'R from ambiguous sort name', description: 'redirect from an ambiguous sort name to a page or list that disambiguates it' }, { tag: 'R from former name', description: 'redirect from a former or historic name or a working title', restriction: 'insideMainspaceOnly' }, { tag: 'R from incomplete name', description: 'R from incomplete name' }, { tag: 'R from incorrect name', description: 'redirect from an erroneus name that is unsuitable as a title' }, { tag: 'R from less specific name', description: 'redirect from a less specific title to a more specific, less general one' }, { tag: 'R from long name', description: 'redirect from a more complete title' }, { tag: 'R from more specific name', description: 'redirect from a more specific title to a less specific, more general one' }, { tag: 'R from non-neutral name', description: 'redirect from a title that contains a non-neutral, pejorative, controversial, or offensive word, phrase, or name' }, { tag: 'R from short name', description: 'redirect from a title that is a shortened form of a person\'s full name, a book title, or other more complete title' }, { tag: 'R from sort name', description: 'redirect from the target\'s sort name, such as beginning with their surname rather than given name', restriction: 'insideMainspaceOnly' }, { tag: 'R from synonym', description: 'redirect from a semantic synonym of the target page title' } ], People: [ { tag: 'R from birth name', description: 'redirect from a person\'s birth name to a more common name', restriction: 'insideMainspaceOnly' }, { tag: 'R from given name', description: 'redirect from a person\'s given name', restriction: 'insideMainspaceOnly' }, { tag: 'R from married name', description: 'redirect from a person\'s married name to a more common name', restriction: 'insideMainspaceOnly' }, { tag: 'R from name with title', description: 'redirect from a person\'s name preceded or followed by a title to the name with no title or with the title in parentheses', restriction: 'insideMainspaceOnly' }, { tag: 'R from person', description: 'redirect from a person or persons to a related article', restriction: 'insideMainspaceOnly' }, { tag: 'R from personal name', description: 'redirect from an individual\'s personal name to an article titled with their professional or other better known moniker', restriction: 'insideMainspaceOnly' }, { tag: 'R from pseudonym', description: 'redirect from a pseudonym', restriction: 'insideMainspaceOnly' }, { tag: 'R from surname', description: 'redirect from a title that is a surname', restriction: 'insideMainspaceOnly' } ], Technical: [ { tag: 'R from drug trade name', description: 'redirect from (or to) the trade name of a drug to (or from) the international nonproprietary name (INN)' }, { tag: 'R from filename', description: 'redirect from a title that is a filename of the target', restriction: 'insideMainspaceOnly' }, { tag: 'R from molecular formula', description: 'redirect from a molecular/chemical formula to its technical or trivial name' }, { tag: 'R from gene symbol', description: 'redirect from a Human Genome Organisation (HUGO) symbol for a gene to an article about the gene', restriction: 'insideMainspaceOnly' } ], Organisms: [ { tag: 'R to scientific name', description: 'redirect from the common name to the scientific name', restriction: 'insideMainspaceOnly' }, { tag: 'R from scientific name', description: 'redirect from the scientific name to the common name', restriction: 'insideMainspaceOnly' }, { tag: 'R from alternative scientific name', description: 'redirect from an alternative scientific name to the accepted scientific name', restriction: 'insideMainspaceOnly' }, { tag: 'R from scientific abbreviation', description: 'redirect from a scientific abbreviation', restriction: 'insideMainspaceOnly' }, { tag: 'R to monotypic taxon', description: 'redirect from the only lower-ranking member of a monotypic taxon to its monotypic taxon', restriction: 'insideMainspaceOnly' }, { tag: 'R from monotypic taxon', description: 'redirect from a monotypic taxon to its only lower-ranking member', restriction: 'insideMainspaceOnly' }, { tag: 'R taxon with possibilities', description: 'redirect from a title related to a living organism that potentially could be expanded into an article', restriction: 'insideMainspaceOnly' } ], Geography: [ { tag: 'R from name and country', description: 'redirect from the specific name to the briefer name', restriction: 'insideMainspaceOnly' }, { tag: 'R from more specific geographic name', description: 'redirect from a geographic location that includes extraneous identifiers such as the county or region of a city', restriction: 'insideMainspaceOnly' } ] }, 'Navigation aids': { Navigation: [ { tag: 'R to anchor', description: 'redirect from a topic that does not have its own page to an anchored part of a page on the subject' }, { tag: 'R avoided double redirect', description: 'redirect from an alternative title for another redirect', subgroup: { name: 'doubleRedirectTarget', type: 'input', label: 'Redirect target name', tooltip: 'Enter the page this redirect would target if the page wasn\'t also a redirect' } }, { tag: 'R from file metadata link', description: 'redirect of a wikilink created from EXIF, XMP, or other information (i.e. the "metadata" section on some image description pages)', restriction: 'insideMainspaceOnly' }, { tag: 'R to list entry', description: 'redirect to a list which contains brief descriptions of subjects not notable enough to have separate articles', restriction: 'insideMainspaceOnly' }, { tag: 'R mentioned in hatnote', description: 'redirect from a title that is mentioned in a hatnote at the redirect target' }, { tag: 'R to section', description: 'similar to {{R to list entry}}, but when list is organized in sections, such as list of characters in a fictional universe' }, { tag: 'R from shortcut', description: 'redirect from a Wikipedia shortcut' }, { tag: 'R to subpage', description: 'redirect to a subpage' } ], Disambiguation: [ { tag: 'R from ambiguous term', description: 'redirect from an ambiguous page name to a page that disambiguates it. This template should never appear on a page that has "(disambiguation)" in its title, use R to disambiguation page instead' }, { tag: 'R to disambiguation page', description: 'redirect to a disambiguation page', restriction: 'disambiguationPagesOnly' }, { tag: 'R from incomplete disambiguation', description: 'redirect from a page name that is too ambiguous to be the title of an article and should redirect to an appropriate disambiguation page' }, { tag: 'R from incorrect disambiguation', description: 'redirect from a page name with incorrect disambiguation due to an error or previous editorial misconception' }, { tag: 'R from other disambiguation', description: 'redirect from a page name with an alternative disambiguation qualifier' }, { tag: 'R from unnecessary disambiguation', description: 'redirect from a page name that has an unneeded disambiguation qualifier' } ], 'Merge, duplicate & move': [ { tag: 'R from duplicated article', description: 'redirect to a similar article in order to preserve its edit history' }, { tag: 'R with history', description: 'redirect from a page containing substantive page history, kept to preserve content and attributions' }, { tag: 'R from move', description: 'redirect from a page that has been moved/renamed' }, { tag: 'R from merge', description: 'redirect from a merged page in order to preserve its edit history' } ], Namespace: [ { tag: 'R from remote talk page', description: 'redirect from a talk page in any talk namespace to a corresponding page that is more heavily watched', restriction: 'insideTalkNamespaceOnly' }, { tag: 'R to category namespace', description: 'redirect from a page outside the category namespace to a category page' }, { tag: 'R to help namespace', description: 'redirect from any page inside or outside of help namespace to a page in that namespace' }, { tag: 'R to main namespace', description: 'redirect from a page outside the main-article namespace to an article in mainspace' }, { tag: 'R to portal namespace', description: 'redirect from any page inside or outside of portal space to a page in that namespace' }, { tag: 'R to project namespace', description: 'redirect from any page inside or outside of project (Wikipedia: or WP:) space to any page in the project namespace' }, { tag: 'R to user namespace', description: 'redirect from a page outside the user namespace to a user page (not to a user talk page)', restriction: 'outsideUserspaceOnly' } ] }, Media: { General: [ { tag: 'R from album', description: 'redirect from an album to a related topic such as the recording artist or a list of albums', restriction: 'insideMainspaceOnly' }, { tag: 'R from band name', description: 'redirect from a musical band or musical group name that redirects an article on a single person, i.e. the band or group leader' }, { tag: 'R from book', description: 'redirect from a book title to a more general, relevant article', restriction: 'insideMainspaceOnly' }, { tag: 'R from cover song', description: 'redirect from a cover version of a song to the article about the original song this version covers' }, { tag: 'R from film', description: 'redirect from a film title that is a subtopic of the redirect target or a title in an alternative language that has been produced in that language', restriction: 'insideMainspaceOnly' }, { tag: 'R from journal', description: 'redirect from a trade or professional journal article a more general, relevant Wikipedia article, such as the author or publisher of the article or to the title in an alternative language' }, { tag: 'R from lyric', description: 'redirect from a lyric to a song or other source that describes the lyric' }, { tag: 'R from meme', description: 'redirect from a name of an internet meme or other pop culture phenomenon that is a subtopic of the redirect target' }, { tag: 'R from song', description: 'redirect from a song title to a more general, relevant article' }, { tag: 'R from television episode', description: 'redirect from a television episode title to a related work or lists of episodes', restriction: 'insideMainspaceOnly' }, { tag: 'R from television program', description: 'redirect from a title of television program, television series or web series that is a subtopic of the redirect target' }, { tag: 'R from upcoming film', description: 'redirect from a title that potentially could be expanded into a new article or other type of associated page such as a new template.' }, { tag: 'R from work', description: 'redirect from a creative work a related topic such as the author/artist, publisher, or a subject related to the work' } ], Fiction: [ { tag: 'R from fictional character', description: 'redirect from a fictional character to a related fictional work or list of characters', restriction: 'insideMainspaceOnly' }, { tag: 'R from fictional element', description: 'redirect from a fictional element (such as an object or concept) to a related fictional work or list of similar elements', restriction: 'insideMainspaceOnly' }, { tag: 'R from fictional location', description: 'redirect from a fictional location or setting to a related fictional work or list of places', restriction: 'insideMainspaceOnly' } ] }, Miscellaneous: { 'Related information': [ { tag: 'R to article without mention', description: 'redirect to an article without any mention of the redirected word or phrase', restriction: 'insideMainspaceOnly' }, { tag: 'R from company name', description: 'redirect from a company name to a related article', restriction: 'insideMainspaceOnly' }, { tag: 'R to decade', description: 'redirect from a year to the decade article', restriction: 'insideMainspaceOnly' }, { tag: 'R from domain name', description: 'redirect from a domain name to an article about a website', restriction: 'insideMainspaceOnly' }, { tag: 'R from emoji', description: 'redirect from an emoji to an article describing the depicted concept or the emoji itself' }, { tag: 'R from phrase', description: 'redirect from a phrase to a more general relevant article covering the topic' }, { tag: 'R from list topic', description: 'redirect from the topic of a list to the equivalent list' }, { tag: 'R from member', description: 'redirect from a member of a group to a related topic such as the group or organization' }, { tag: 'R to related topic', description: 'redirect to an article about a similar topic', restriction: 'insideMainspaceOnly' }, { tag: 'R from related word', description: 'redirect from a related word' }, { tag: 'R from school', description: 'redirect from a school article that had very little information', restriction: 'insideMainspaceOnly' }, { tag: 'R from subtopic', description: 'redirect from a title that is a subtopic of the target article', restriction: 'insideMainspaceOnly' }, { tag: 'R to subtopic', description: 'redirect to a subtopic of the redirect\'s title', restriction: 'insideMainspaceOnly' }, { tag: 'R from Unicode character', description: 'redirect from a single Unicode character to an article or Wikipedia project page that infers meaning for the symbol', restriction: 'insideMainspaceOnly' }, { tag: 'R from Unicode code', description: 'redirect from a Unicode code point to an article about the character it represents', restriction: 'insideMainspaceOnly' } ], 'With possibilities': [ { tag: 'R with possibilities', description: 'redirect from a specific title to a more general, less detailed article (something which can and should be expanded)' } ], 'ISO codes': [ { tag: 'R from ISO 4 abbreviation', description: 'redirect from an ISO 4 publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' }, { tag: 'R from ISO 639 code', description: 'redirect from a title that is an ISO 639 language code to an article about the language', restriction: 'insideMainspaceOnly' } ], Printworthiness: [ { tag: 'R printworthy', description: 'redirect from a title that would be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' }, { tag: 'R unprintworthy', description: 'redirect from a title that would NOT be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' } ] } }; // maintenance tags for FILES start here Twinkle.tag.fileList = { 'License and sourcing problem tags': [ { label: '{{Better source requested}}: source info consists of bare image URL/generic base URL only', value: 'Better source requested' }, { label: '{{Maybe free media}}: currently tagged under non-free license, but free license may be available ', value: 'Maybe free media' }, { label: '{{Non-free reduce}}: non-low-resolution fair use image (or too-long audio clip, etc)', value: 'Non-free reduce' }, { label: '{{Orphaned non-free revisions}}: fair use media with old revisions that need to be deleted', value: 'Orphaned non-free revisions' } ], 'Wikimedia Commons-related tags': [ { label: '{{Copy to Commons}}: free media that should be copied to Commons', value: 'Copy to Commons' }, { label: '{{Deleted on Commons}}: file has previously been deleted from Commons', value: 'Deleted on Commons', subgroup: { type: 'input', name: 'deletedOnCommonsName', label: 'Name on Commons:', tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix' } }, { label: '{{Do not move to Commons}}: file not suitable for moving to Commons', value: 'Do not move to Commons', subgroup: [ { type: 'input', name: 'DoNotMoveToCommons_reason', label: 'Reason:', tooltip: 'Enter the reason why this image should not be moved to Commons (required). If the file is PD in the US but not in country of origin, enter "US only"', required: true }, { type: 'number', name: 'DoNotMoveToCommons_expiry', label: 'Expiration year:', min: new Morebits.Date().getFullYear(), tooltip: 'If this file can be moved to Commons beginning in a certain year, you can enter it here (optional).' } ] }, { label: '{{Keep local}}: request to keep local copy of a Commons file', value: 'Keep local', subgroup: { type: 'input', name: 'keeplocalName', label: 'Commons image name if different:', tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:' } }, { label: '{{Nominated for deletion on Commons}}: file is nominated for deletion on Commons', value: 'Nominated for deletion on Commons', subgroup: { type: 'input', name: 'nominatedOnCommonsName', label: 'Name on Commons:', tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:' } } ], 'Cleanup tags': [ { label: '{{Artifacts}}: PNG contains residual compression artifacts', value: 'Artifacts' }, { label: '{{Bad font}}: SVG uses fonts not available on the thumbnail server', value: 'Bad font' }, { label: '{{Bad format}}: PDF/DOC/... file should be converted to a more useful format', value: 'Bad format' }, { label: '{{Bad GIF}}: GIF that should be PNG, JPEG, or SVG', value: 'Bad GIF' }, { label: '{{Bad JPEG}}: JPEG that should be PNG or SVG', value: 'Bad JPEG' }, { label: '{{Bad SVG}}: SVG with a mix of raster and vector graphics', value: 'Bad SVG' }, { label: '{{Bad trace}}: auto-traced SVG requiring cleanup', value: 'Bad trace' }, { label: '{{Cleanup image}}: general cleanup', value: 'Cleanup image', subgroup: { type: 'input', name: 'cleanupimageReason', label: 'Reason:', tooltip: 'Enter the reason for cleanup (required)', required: true } }, { label: '{{ClearType}}: image (not screenshot) with ClearType anti-aliasing', value: 'ClearType' }, { label: '{{Fake SVG}}: SVG solely containing raster graphics without true vector content', value: 'Fake SVG' }, { label: '{{Imagewatermark}}: image contains visible or invisible watermarking', value: 'Imagewatermark' }, { label: '{{NoCoins}}: image using coins to indicate scale', value: 'NoCoins' }, { label: '{{Overcompressed JPEG}}: JPEG with high levels of artifacts', value: 'Overcompressed JPEG' }, { label: '{{Opaque}}: opaque background should be transparent', value: 'Opaque' }, { label: '{{Remove border}}: unneeded border, white space, etc.', value: 'Remove border' }, { label: '{{Rename media}}: file should be renamed according to the criteria at [[WP:FMV]]', value: 'Rename media', subgroup: [ { type: 'input', name: 'renamemediaNewname', label: 'New name:', tooltip: 'Enter the new name for the image (optional)' }, { type: 'input', name: 'renamemediaReason', label: 'Reason:', tooltip: 'Enter the reason for the rename (optional)' } ] }, { label: '{{Should be PNG}}: GIF or JPEG should be lossless', value: 'Should be PNG' }, { label: '{{Should be SVG}}: PNG, GIF or JPEG should be vector graphics', value: 'Should be SVG', subgroup: { name: 'svgCategory', type: 'select', list: [ { label: '{{Should be SVG|other}}', value: 'other' }, { label: '{{Should be SVG|alphabet}}: character images, font examples, etc.', value: 'alphabet' }, { label: '{{Should be SVG|chemical}}: chemical diagrams, etc.', value: 'chemical' }, { label: '{{Should be SVG|circuit}}: electronic circuit diagrams, etc.', value: 'circuit' }, { label: '{{Should be SVG|coat of arms}}: coats of arms', value: 'coat of arms' }, { label: '{{Should be SVG|diagram}}: diagrams that do not fit any other subcategory', value: 'diagram' }, { label: '{{Should be SVG|emblem}}: emblems, free/libre logos, insignias, etc.', value: 'emblem' }, { label: '{{Should be SVG|fair use}}: fair-use images, fair-use logos', value: 'fair use' }, { label: '{{Should be SVG|flag}}: flags', value: 'flag' }, { label: '{{Should be SVG|graph}}: visual plots of data', value: 'graph' }, { label: '{{Should be SVG|logo}}: logos', value: 'logo' }, { label: '{{Should be SVG|map}}: maps', value: 'map' }, { label: '{{Should be SVG|music}}: musical scales, notes, etc.', value: 'music' }, { label: '{{Should be SVG|physical}}: "realistic" images of physical objects, people, etc.', value: 'physical' }, { label: '{{Should be SVG|symbol}}: miscellaneous symbols, icons, etc.', value: 'symbol' } ] } }, { label: '{{Should be text}}: image should be represented as text, tables, or math markup', value: 'Should be text' } ], 'Image quality tags': [ { label: '{{Image hoax}}: Image may be manipulated or constitute a hoax', value: 'Image hoax' }, { label: '{{Image-blownout}}', value: 'Image-blownout' }, { label: '{{Image-out-of-focus}}', value: 'Image-out-of-focus' }, { label: '{{Image-Poor-Quality}}', value: 'Image-Poor-Quality', subgroup: { type: 'input', name: 'ImagePoorQualityReason', label: 'Reason:', tooltip: 'Enter the reason why this image is so bad (required)', required: true } }, { label: '{{Image-underexposure}}', value: 'Image-underexposure' }, { label: '{{Low quality chem}}: disputed chemical structures', value: 'Low quality chem', subgroup: { type: 'input', name: 'lowQualityChemReason', label: 'Reason:', tooltip: 'Enter the reason why the diagram is disputed (required)', required: true } } ], 'Replacement tags': [ { label: '{{Obsolete}}: improved version available', value: 'Obsolete' }, { label: '{{PNG version available}}', value: 'PNG version available' }, { label: '{{Vector version available}}', value: 'Vector version available' } ] }; Twinkle.tag.fileList['Replacement tags'].forEach((el) => { el.subgroup = { type: 'input', label: 'Replacement file:', tooltip: 'Enter the name of the file which replaces this one (required)', name: el.value.replace(/ /g, '_') + 'File', required: true }; }); Twinkle.tag.callbacks = { article: function articleCallback(pageobj) { // Remove tags that become superfluous with this action let pageText = pageobj.getPageText().replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, ''); const params = pageobj.getCallbackParameters(); /** * Saves the page following the removal of tags if any. The last step. * Called from removeTags() */ const postRemoval = function() { if (params.tagsToRemove.length) { // Remove empty {{multiple issues}} if found pageText = pageText.replace(/\{\{(multiple ?issues|article ?issues|mi)\s*\|\s*\}\}\n?/im, ''); // Remove single-element {{multiple issues}} if found pageText = pageText.replace(/\{\{(?:multiple ?issues|article ?issues|mi)\s*\|\s*(\{\{[^}]+\}\})\s*\}\}/im, '$1'); } // Build edit summary const makeSentence = function(array) { if (array.length < 3) { return array.join(' and '); } const last = array.pop(); return array.join(', ') + ', and ' + last; }; const makeTemplateLink = function(tag) { let text = '{{[['; // if it is a custom tag with a parameter if (tag.includes('|')) { tag = tag.slice(0, tag.indexOf('|')); } text += tag.includes(':') ? tag : 'Template:' + tag + '|' + tag; return text + ']]}}'; }; let summaryText; const addedTags = params.tags.map(makeTemplateLink); const removedTags = params.tagsToRemove.map(makeTemplateLink); if (addedTags.length) { summaryText = 'Added ' + makeSentence(addedTags); summaryText += removedTags.length ? '; and removed ' + makeSentence(removedTags) : ''; } else { summaryText = 'Removed ' + makeSentence(removedTags); } summaryText += ' tag' + (addedTags.length + removedTags.length > 1 ? 's' : ''); if (params.reason) { summaryText += ': ' + params.reason; } // avoid truncated summaries if (summaryText.length > 499) { summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1'); } pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); if ((mw.config.get('wgNamespaceNumber') === 0 && Twinkle.getPref('watchTaggedVenues').includes('articles')) || (mw.config.get('wgNamespaceNumber') === 118 && Twinkle.getPref('watchTaggedVenues').includes('drafts'))) { pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages')); } pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(() => { // COI: Start the discussion on the talk page (mainspace only) if (params.coiReason) { const coiTalkPage = new Morebits.wiki.Page('Talk:' + Morebits.pageNameNorm, 'Starting discussion on talk page'); coiTalkPage.setNewSectionText(params.coiReason + ' ~~~~'); coiTalkPage.setNewSectionTitle('COI tag (' + new Morebits.Date(pageobj.getLoadTime()).format('MMMM Y', 'utc') + ')'); coiTalkPage.setChangeTags(Twinkle.changeTags); coiTalkPage.setCreateOption('recreate'); coiTalkPage.newSection(); } // Special functions for {{not English}} and {{rough translation}} // Post at WP:PNT (mainspace only) if (params.translationPostAtPNT) { const pntPage = new Morebits.wiki.Page('Wikipedia:Pages needing translation into English', 'Listing article at Wikipedia:Pages needing translation into English'); pntPage.setFollowRedirect(true); pntPage.load((pageobj) => { const oldText = pageobj.getPageText(); const lang = params.translationLanguage; const reason = params.translationComments; let templateText; let text, summary; if (params.tags.includes('Rough translation')) { templateText = '{{subst:Dual fluency request|pg=' + Morebits.pageNameNorm + '|Language=' + (lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~'; // Place in section == Translated pages that could still use some cleanup == text = oldText + '\n\n' + templateText; summary = 'Translation cleanup requested on '; } else if (params.tags.includes('Not English')) { templateText = '{{subst:Translation request|pg=' + Morebits.pageNameNorm + '|Language=' + (lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~'; // Place in section == Pages for consideration == text = oldText.replace(/\n+(==\s?Translated pages that could still use some cleanup\s?==)/, '\n\n' + templateText + '\n\n$1'); summary = 'Translation' + (lang ? ' from ' + lang : '') + ' requested on '; } if (text === oldText) { pageobj.getStatusElement().error('failed to find target spot for the discussion'); return; } pageobj.setPageText(text); pageobj.setEditSummary(summary + ' [[:' + Morebits.pageNameNorm + ']]'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setCreateOption('recreate'); pageobj.save(); }); } // Notify the user ({{Not English}} only) if (params.translationNotify) { new Morebits.wiki.Page(Morebits.pageNameNorm).lookupCreation((innerPageobj) => { const initialContrib = innerPageobj.getCreator(); // Disallow warning yourself if (initialContrib === mw.config.get('wgUserName')) { innerPageobj.getStatusElement().warn('You (' + initialContrib + ') created this page; skipping user notification'); return; } const userTalkPage = new Morebits.wiki.Page('User talk:' + initialContrib, 'Notifying initial contributor (' + initialContrib + ')'); userTalkPage.setNewSectionTitle('Your article [[' + Morebits.pageNameNorm + ']]'); userTalkPage.setNewSectionText('{{subst:uw-notenglish|1=' + Morebits.pageNameNorm + (params.translationPostAtPNT ? '' : '|nopnt=yes') + '}} ~~~~'); userTalkPage.setEditSummary('Notice: Please use English when contributing to the English Wikipedia.'); userTalkPage.setChangeTags(Twinkle.changeTags); userTalkPage.setCreateOption('recreate'); userTalkPage.setFollowRedirect(true, false); userTalkPage.newSection(); }); } }); if (params.patrol) { pageobj.triage(); } }; /** * Removes the existing tags that were deselected (if any) * Calls postRemoval() when done */ const removeTags = function removeTags() { if (params.tagsToRemove.length === 0) { postRemoval(); return; } Morebits.Status.info('Info', 'Removing deselected tags that were already present'); const getRedirectsFor = []; // Remove the tags from the page text, if found in its proper name, // otherwise moves it to `getRedirectsFor` array earmarking it for // later removal params.tagsToRemove.forEach((tag) => { const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?'); if (tagRegex.test(pageText)) { pageText = pageText.replace(tagRegex, ''); } else { getRedirectsFor.push('Template:' + tag); } }); if (!getRedirectsFor.length) { postRemoval(); return; } // Remove tags which appear in page text as redirects const api = new Morebits.wiki.Api('Getting template redirects', { action: 'query', prop: 'linkshere', titles: getRedirectsFor.join('|'), redirects: 1, // follow redirect if the class name turns out to be a redirect page lhnamespace: '10', // template namespace only lhshow: 'redirect', lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops format: 'json' }, ((apiobj) => { const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere); pages.forEach((page) => { let removed = false; page.linkshere.concat({title: page.title}).forEach((el) => { const tag = el.title.slice(9); const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?'); if (tagRegex.test(pageText)) { pageText = pageText.replace(tagRegex, ''); removed = true; return false; // break out of $.each } }); if (!removed) { Morebits.Status.warn('Info', 'Failed to find {{' + page.title.slice(9) + '}} on the page... excluding'); } }); postRemoval(); })); api.post(); }; if (!params.tags.length) { removeTags(); return; } let tagRe, tagText = '', tags = []; const groupableTags = [], groupableExistingTags = []; // Executes first: addition of selected tags /** * Updates `tagText` with the syntax of `tagName` template with its parameters * * @param {number} tagIndex * @param {string} tagName */ const addTag = function articleAddTag(tagIndex, tagName) { let currentTag = ''; if (tagName === 'Uncategorized' || tagName === 'Improve categories') { pageText += '\n\n{{' + tagName + '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}'; } else { currentTag += '{{' + tagName; // fill in other parameters, based on the tag const subgroupObj = Twinkle.tag.article.flatObject[tagName] && Twinkle.tag.article.flatObject[tagName].subgroup; if (subgroupObj) { const subgroups = Array.isArray(subgroupObj) ? subgroupObj : [ subgroupObj ]; subgroups.forEach((gr) => { if (gr.parameter && (params[gr.name] || gr.required)) { currentTag += '|' + gr.parameter + '=' + (params[gr.name] || ''); } }); } switch (tagName) { case 'Not English': case 'Rough translation': if (params.translationPostAtPNT) { currentTag += '|listed=yes'; } break; default: break; } currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n'; tagText += currentTag; } }; /** * Adds the tags which go outside {{multiple issues}}, either because * these tags aren't supported in {{multiple issues}} or because * {{multiple issues}} is not being added to the page at all */ const addUngroupedTags = function() { $.each(tags, addTag); // Insert tag after short description or any hatnotes, // as well as deletion/protection-related templates const wikipage = new Morebits.wikitext.Page(pageText); const templatesAfter = Twinkle.hatnoteRegex + // Protection templates 'pp|pp-.*?|' + // CSD 'db|delete|db-.*?|speedy deletion-.*?|' + // PROD '(?:proposed deletion|prod blp)\\/dated(?:\\s*\\|(?:concern|user|timestamp|help).*)+|' + // not a hatnote, but sometimes under a CSD or AfD 'salt|proposed deletion endorsed'; // AfD is special, as the tag includes html comments before and after the actual template // trailing whitespace/newline needed since this subst's a newline const afdRegex = '(?:<!--.*AfD.*\\n\\{\\{(?:Article for deletion\\/dated|AfDM).*\\}\\}\\n<!--.*(?:\\n<!--.*)?AfD.*(?:\\s*\\n))?'; pageText = wikipage.insertAfterTemplates(tagText, templatesAfter, null, afdRegex).getText(); removeTags(); }; // Separate tags into groupable ones (`groupableTags`) and non-groupable ones (`tags`) params.tags.forEach((tag) => { tagRe = new RegExp('\\{\\{' + tag + '(\\||\\}\\})', 'im'); // regex check for preexistence of tag can be skipped if in canRemove mode if (Twinkle.tag.canRemove || !tagRe.exec(pageText)) { // condition Twinkle.tag.article.tags[tag] to ensure that its not a custom tag // Custom tags are assumed non-groupable, since we don't know whether MI template supports them if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) { groupableTags.push(tag); } else { tags.push(tag); } } else { if (tag === 'History merge') { tags.push(tag); } else { Morebits.Status.warn('Info', 'Found {{' + tag + '}} on the article already...excluding'); } } }); // To-be-retained existing tags that are groupable params.tagsToRemain.forEach((tag) => { // If the tag is unknown to us, we consider it non-groupable if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) { groupableExistingTags.push(tag); } }); const miTest = /\{\{(multiple ?issues|article ?issues|mi)(?!\s*\|\s*section\s*=)[^}]+\{/im.exec(pageText); if (miTest && groupableTags.length > 0) { Morebits.Status.info('Info', 'Adding supported tags inside existing {{multiple issues}} tag'); tagText = ''; $.each(groupableTags, addTag); const miRegex = new RegExp('(\\{\\{\\s*' + miTest[1] + '\\s*(?:\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?)\\}\\}\\s*', 'im'); pageText = pageText.replace(miRegex, '$1' + tagText + '}}\n'); tagText = ''; addUngroupedTags(); } else if (params.group && !miTest && (groupableExistingTags.length + groupableTags.length) >= 2) { Morebits.Status.info('Info', 'Grouping supported tags inside {{multiple issues}}'); tagText += '{{Multiple issues|\n'; /** * Adds newly added tags to MI */ const addNewTagsToMI = function() { $.each(groupableTags, addTag); tagText += '}}\n'; addUngroupedTags(); }; const getRedirectsFor = []; // Reposition the tags on the page into {{multiple issues}}, if found with its // proper name, else moves it to `getRedirectsFor` array to be handled later groupableExistingTags.forEach((tag) => { const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?)'); if (tagRegex.test(pageText)) { tagText += tagRegex.exec(pageText)[1]; pageText = pageText.replace(tagRegex, ''); } else { getRedirectsFor.push('Template:' + tag); } }); if (!getRedirectsFor.length) { addNewTagsToMI(); return; } const api = new Morebits.wiki.Api('Getting template redirects', { action: 'query', prop: 'linkshere', titles: getRedirectsFor.join('|'), redirects: 1, lhnamespace: '10', // template namespace only lhshow: 'redirect', lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops format: 'json' }, ((apiobj) => { const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere); pages.forEach((page) => { let found = false; page.linkshere.forEach((el) => { const tag = el.title.slice(9); const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?)'); if (tagRegex.test(pageText)) { tagText += tagRegex.exec(pageText)[1]; pageText = pageText.replace(tagRegex, ''); found = true; return false; // break out of $.each } }); if (!found) { Morebits.Status.warn('Info', 'Failed to find the existing {{' + page.title.slice(9) + '}} on the page... skip repositioning'); } }); addNewTagsToMI(); })); api.post(); } else { tags = tags.concat(groupableTags); addUngroupedTags(); } }, redirect: function redirect(pageobj) { const params = pageobj.getCallbackParameters(), tags = []; let pageText = pageobj.getPageText(), tagRe, tagText = '', summaryText = 'Added', i; for (i = 0; i < params.tags.length; i++) { tagRe = new RegExp('(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im'); if (!tagRe.exec(pageText)) { tags.push(params.tags[i]); } else { Morebits.Status.warn('Info', 'Found {{' + params.tags[i] + '}} on the redirect already...excluding'); } } const addTag = function redirectAddTag(tagIndex, tagName) { tagText += '\n{{' + tagName; if (tagName === 'R from alternative language') { if (params.altLangFrom) { tagText += '|from=' + params.altLangFrom; } if (params.altLangTo) { tagText += '|to=' + params.altLangTo; } } else if (tagName === 'R avoided double redirect' && params.doubleRedirectTarget) { tagText += '|1=' + params.doubleRedirectTarget; } tagText += '}}'; if (tagIndex > 0) { if (tagIndex === (tags.length - 1)) { summaryText += ' and'; } else if (tagIndex < (tags.length - 1)) { summaryText += ','; } } summaryText += ' {{[[:' + (tagName.includes(':') ? tagName : 'Template:' + tagName + '|' + tagName) + ']]}}'; }; if (!tags.length) { Morebits.Status.warn('Info', 'No tags remaining to apply'); } tags.sort(); $.each(tags, addTag); // Check for all Rcat shell redirects (from #433) if (pageText.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh|RCS)/i)) { // Regex inspired by [[User:Kephir/gadgets/sagittarius.js]] ([[Special:PermaLink/831402893]]) const oldTags = pageText.match(/(\s*{{[A-Za-z\s]+\|(?:\s*1=)?)((?:[^|{}]|{{[^}]+}})+)(}})\s*/i); pageText = pageText.replace(oldTags[0], oldTags[1] + tagText + oldTags[2] + oldTags[3]); } else { // Fold any pre-existing Rcats into taglist and under Rcatshell const pageTags = pageText.match(/\s*{{R(?:edirect)? .*?}}/img); let oldPageTags = ''; if (pageTags) { pageTags.forEach((pageTag) => { const pageRe = new RegExp(Morebits.string.escapeRegExp(pageTag), 'img'); pageText = pageText.replace(pageRe, ''); pageTag = pageTag.trim(); oldPageTags += '\n' + pageTag; }); } pageText = pageText.trim() + '\n\n{{Redirect category shell|' + tagText + oldPageTags + '\n}}'; } summaryText += (tags.length > 0 ? ' tag' + (tags.length > 1 ? 's' : ' ') : ' {{[[Template:Redirect category shell|Redirect category shell]]}}') + ' to redirect'; // avoid truncated summaries if (summaryText.length > 499) { summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1'); } pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); if (Twinkle.getPref('watchTaggedVenues').includes('redirects')) { pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages')); } pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if (params.patrol) { pageobj.triage(); } }, file: function twinkletagCallbacksFile(pageobj) { let text = pageobj.getPageText(); const params = pageobj.getCallbackParameters(); let summary = 'Adding '; // Add maintenance tags if (params.tags.length) { let tagtext = '', currentTag; $.each(params.tags, (k, tag) => { // when other commons-related tags are placed, remove "move to Commons" tag if (['Keep local', 'Do not move to Commons'].includes(tag)) { text = Twinkle.removeMoveToCommonsTagsFromWikicode( text ); } currentTag = tag; switch (tag) { case 'Keep local': if (params.keeplocalName !== '') { currentTag += '|1=' + params.keeplocalName; } break; case 'Rename media': if (params.renamemediaNewname !== '') { currentTag += '|1=' + params.renamemediaNewname; } if (params.renamemediaReason !== '') { currentTag += '|2=' + params.renamemediaReason; } break; case 'Cleanup image': currentTag += '|1=' + params.cleanupimageReason; break; case 'Image-Poor-Quality': currentTag += '|1=' + params.ImagePoorQualityReason; break; case 'Image hoax': currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}'; break; case 'Low quality chem': currentTag += '|1=' + params.lowQualityChemReason; break; case 'Vector version available': text = text.replace(/\{\{((convert to |convertto|should be |shouldbe|to)?svg|badpng|vectorize)[^}]*\}\}/gi, ''); /* falls through */ case 'PNG version available': /* falls through */ case 'Obsolete': currentTag += '|1=' + params[tag.replace(/ /g, '_') + 'File']; break; case 'Do not move to Commons': currentTag += '|reason=' + params.DoNotMoveToCommons_reason; if (params.DoNotMoveToCommons_expiry) { currentTag += '|expiry=' + params.DoNotMoveToCommons_expiry; } break; case 'Orphaned non-free revisions': currentTag = 'subst:' + currentTag; // subst // remove {{non-free reduce}} and redirects text = text.replace(/\{\{\s*(Template\s*:\s*)?(Non-free reduce|FairUseReduce|Fairusereduce|Fair Use Reduce|Fair use reduce|Reduce size|Reduce|Fair-use reduce|Image-toobig|Comic-ovrsize-img|Non-free-reduce|Nfr|Smaller image|Nonfree reduce)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, ''); currentTag += '|date={{subst:date}}'; break; case 'Copy to Commons': currentTag += '|human=' + mw.config.get('wgUserName'); break; case 'Should be SVG': currentTag += '|' + params.svgCategory; break; case 'Nominated for deletion on Commons': if (params.nominatedOnCommonsName !== '') { currentTag += '|1=' + params.nominatedOnCommonsName; } break; case 'Deleted on Commons': if (params.deletedOnCommonsName !== '') { currentTag += '|1=' + params.deletedOnCommonsName; } break; default: break; // don't care } currentTag = '{{' + currentTag + '}}\n'; tagtext += currentTag; summary += '{{' + tag + '}}, '; }); if (!tagtext) { pageobj.getStatusElement().warn('User canceled operation; nothing to do'); return; } text = tagtext + text; } pageobj.setPageText(text); pageobj.setEditSummary(summary.substring(0, summary.length - 2)); pageobj.setChangeTags(Twinkle.changeTags); if (Twinkle.getPref('watchTaggedVenues').includes('files')) { pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages')); } pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if (params.patrol) { pageobj.triage(); } } }; /** * Given an array of incompatible tags, check if we have two or more selected * * @param {Array} incompatibleTags * @param {Array} tagsToCheck * @param {string} [extraMessage] * @return {true|undefined} */ Twinkle.tag.checkIncompatible = function(incompatibleTags, tagsToCheck, extraMessage = null) { const count = incompatibleTags.filter((tag) => tagsToCheck.includes(tag)).length; if (count > 1) { const incompatibleTagsString = '{{' + incompatibleTags.join('}}, {{') + '}}'; let message = 'Please select only one of: ' + incompatibleTagsString + '.'; message += extraMessage ? ' ' + extraMessage : ''; alert(message); return true; } }; Twinkle.tag.callback.evaluate = function twinkletagCallbackEvaluate(e) { const form = e.target; const params = Morebits.QuickForm.getInputData(form); // Validation // We could theoretically put them all checkIncompatible calls in a // forEach loop, but it's probably clearer not to have [[array one], // [array two]] devoid of context. switch (Twinkle.tag.mode) { case 'article': params.tagsToRemove = form.getUnchecked('existingTags'); // not in `input` params.tagsToRemain = params.existingTags || []; // container not created if none present if (Twinkle.tag.checkIncompatible(['Not English', 'Rough translation'], params.tags)) { return; } break; case 'file': if (Twinkle.tag.checkIncompatible(['Bad GIF', 'Bad JPEG', 'Bad SVG', 'Bad format'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible(['Should be PNG', 'Should be SVG', 'Should be text'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible(['Bad SVG', 'Vector version available'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible(['Bad JPEG', 'Overcompressed JPEG'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible(['PNG version available', 'Vector version available'], params.tags)) { return; } // Get extension from either mime-type or title, if not present (e.g., SVGs) var extension = ((extension = $('.mime-type').text()) && extension.split(/\//)[1]) || mw.Title.newFromText(Morebits.pageNameNorm).getExtension(); if (extension) { const extensionUpper = extension.toUpperCase(); // What self-respecting file format has *two* extensions?! if (extensionUpper === 'JPG') { extension = 'JPEG'; } // Check that selected templates make sense given the file's extension. // {{Bad GIF|JPEG|SVG}}, {{Fake SVG}} if (extensionUpper !== 'GIF' && params.tags.includes('Bad GIF')) { alert('This appears to be a ' + extension + ' file, so {{Bad GIF}} is inappropriate.'); return; } else if (extensionUpper !== 'JPEG' && params.tags.includes('Bad JPEG')) { alert('This appears to be a ' + extension + ' file, so {{Bad JPEG}} is inappropriate.'); return; } else if (extensionUpper !== 'SVG' && params.tags.includes('Bad SVG')) { alert('This appears to be a ' + extension + ' file, so {{Bad SVG}} is inappropriate.'); return; } else if (extensionUpper !== 'SVG' && params.tags.includes('Fake SVG')) { alert('This appears to be a ' + extension + ' file, so {{Fake SVG}} is inappropriate.'); return; } // {{Should be PNG|SVG}} if (params.tags.includes('Should be ' + extensionUpper)) { alert('This is already a ' + extension + ' file, so {{Should be ' + extensionUpper + '}} is inappropriate.'); return; } // {{Overcompressed JPEG}} if (params.tags.includes('Overcompressed JPEG') && extensionUpper !== 'JPEG') { alert('This appears to be a ' + extension + ' file, so {{Overcompressed JPEG}} probably doesn\'t apply.'); return; } // {{Bad trace}} and {{Bad font}} if (extensionUpper !== 'SVG') { if (params.tags.includes('Bad trace')) { alert('This appears to be a ' + extension + ' file, so {{Bad trace}} probably doesn\'t apply.'); return; } else if (params.tags.includes('Bad font')) { alert('This appears to be a ' + extension + ' file, so {{Bad font}} probably doesn\'t apply.'); return; } } } // {{Do not move to Commons}} if ( params.tags.includes('Do not move to Commons') && params.DoNotMoveToCommons_expiry && ( !/^2\d{3}$/.test(params.DoNotMoveToCommons_expiry) || parseInt(params.DoNotMoveToCommons_expiry, 10) <= new Date().getFullYear() ) ) { alert('Must be a valid future year.'); return; } break; case 'redirect': if (Twinkle.tag.checkIncompatible(['R printworthy', 'R unprintworthy'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible(['R from subtopic', 'R to subtopic'], params.tags)) { return; } if (Twinkle.tag.checkIncompatible([ 'R to category namespace', 'R to help namespace', 'R to main namespace', 'R to portal namespace', 'R to project namespace', 'R to user namespace' ], params.tags)) { return; } break; default: alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode); break; } // File/redirect: return if no tags selected // Article: return if no tag is selected and no already present tag is deselected if (params.tags.length === 0 && (Twinkle.tag.mode !== 'article' || params.tagsToRemove.length === 0)) { alert('You must select at least one tag!'); return; } Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(form); Morebits.wiki.actionCompleted.redirect = Morebits.pageNameNorm; Morebits.wiki.actionCompleted.notice = 'Tagging complete, reloading article in a few seconds'; if (Twinkle.tag.mode === 'redirect') { Morebits.wiki.actionCompleted.followRedirect = false; } const wikipediaPage = new Morebits.wiki.Page(Morebits.pageNameNorm, 'Tagging ' + Twinkle.tag.mode); wikipediaPage.setCallbackParameters(params); wikipediaPage.setChangeTags(Twinkle.changeTags); // Here to apply to triage wikipediaPage.load(Twinkle.tag.callbacks[Twinkle.tag.mode]); }; Twinkle.addInitCallback(Twinkle.tag, 'tag'); }()); // </nowiki> l8mdv0hq2ogqwlr76q72a9jjpn7hzxd MediaWiki:Gadget-twinklewarn.js 8 24481 268718 2026-04-27T16:50:19Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinklewarn.js: Warn module **************************************** * Mode of invocation: Tab ("Warn") * Active on: Any page with relevant user name (userspace, contribs, * etc.) (not IP ranges), as well as the rollback success page */ Twinkle.warn = function twinklewarn() { // Users and IPs but not IP ranges if (mw.config.exists('wgRel...' 268718 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinklewarn.js: Warn module **************************************** * Mode of invocation: Tab ("Warn") * Active on: Any page with relevant user name (userspace, contribs, * etc.) (not IP ranges), as well as the rollback success page */ Twinkle.warn = function twinklewarn() { // Users and IPs but not IP ranges if (mw.config.exists('wgRelevantUserName') && !Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) { Twinkle.addPortletLink(Twinkle.warn.callback, 'Warn', 'tw-warn', 'Warn/notify user'); if (Twinkle.getPref('autoMenuAfterRollback') && mw.config.get('wgNamespaceNumber') === 3 && Twinkle.getPrefill('vanarticle') && !Twinkle.getPrefill('twinklewelcome') && !Twinkle.getPrefill('noautowarn')) { Twinkle.warn.callback(); } } // Modify URL of talk page on rollback success pages, makes use of a // custom message box in [[MediaWiki:Rollback-success]] if (mw.config.get('wgAction') === 'rollback') { const $vandalTalkLink = $('#mw-rollback-success').find('.mw-usertoollinks a').first(); if ($vandalTalkLink.length) { $vandalTalkLink.css('font-weight', 'bold'); $vandalTalkLink.wrapInner($('<span>').attr('title', 'If appropriate, you can use Twinkle to warn the user about their edits to this page.')); // Can't provide vanarticlerevid as only wgCurRevisionId is provided const extraParam = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm); const href = $vandalTalkLink.attr('href'); if (!href.includes('?')) { $vandalTalkLink.attr('href', href + '?' + extraParam); } else { $vandalTalkLink.attr('href', href + '&' + extraParam); } } } }; // Used to close window when switching to ARV in autolevel Twinkle.warn.dialog = null; Twinkle.warn.callback = function twinklewarnCallback() { if (mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') && !confirm('You are about to warn yourself! Are you sure you want to proceed?')) { return; } Twinkle.warn.dialog = new Morebits.SimpleWindow(600, 440); const dialog = Twinkle.warn.dialog; dialog.setTitle('Warn/notify user'); dialog.setScriptName('Twinkle'); dialog.addFooterLink('Choosing a warning level', 'WP:UWUL#Levels'); dialog.addFooterLink('Warn prefs', 'WP:TW/PREF#warn'); dialog.addFooterLink('Twinkle help', 'WP:TW/DOC#warn'); dialog.addFooterLink('Give feedback', 'WT:TW'); const form = new Morebits.QuickForm(Twinkle.warn.callback.evaluate); const main_select = form.append({ type: 'field', label: 'Choose type of warning/notice to issue', tooltip: 'First choose a main warning group, then the specific warning to issue.' }); const main_group = main_select.append({ type: 'select', name: 'main_group', tooltip: 'You can customize the default selection in your Twinkle preferences', event: Twinkle.warn.callback.change_category }); const defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10); main_group.append({ type: 'option', label: 'Auto-select level (1-4)', value: 'autolevel', selected: defaultGroup === 11 }); main_group.append({ type: 'option', label: '1: General note', value: 'level1', selected: defaultGroup === 1 }); main_group.append({ type: 'option', label: '2: Caution', value: 'level2', selected: defaultGroup === 2 }); main_group.append({ type: 'option', label: '3: Warning', value: 'level3', selected: defaultGroup === 3 }); main_group.append({ type: 'option', label: '4: Final warning', value: 'level4', selected: defaultGroup === 4 }); main_group.append({ type: 'option', label: '4im: Only warning', value: 'level4im', selected: defaultGroup === 5 }); if (Twinkle.getPref('combinedSingletMenus')) { main_group.append({ type: 'option', label: 'Single-issue messages', value: 'singlecombined', selected: defaultGroup === 6 || defaultGroup === 7 }); } else { main_group.append({ type: 'option', label: 'Single-issue notices', value: 'singlenotice', selected: defaultGroup === 6 }); main_group.append({ type: 'option', label: 'Single-issue warnings', value: 'singlewarn', selected: defaultGroup === 7 }); } if (Twinkle.getPref('customWarningList').length) { main_group.append({ type: 'option', label: 'Custom warnings', value: 'custom', selected: defaultGroup === 9 }); } main_group.append({ type: 'option', label: 'All warning templates', value: 'kitchensink', selected: defaultGroup === 10 }); main_select.append({ type: 'select', name: 'sub_group', event: Twinkle.warn.callback.change_subcategory }); // Will be empty to begin with. form.append({ type: 'input', name: 'article', label: 'Linked page', value: Twinkle.getPrefill('vanarticle') || '', tooltip: 'A page can be linked within the notice, perhaps because it was a revert to said page that dispatched this notice. Leave empty for no page to be linked.' }); form.append({ type: 'div', label: '', style: 'color: red', id: 'twinkle-warn-warning-messages' }); const more = form.append({ type: 'field', name: 'reasonGroup', label: 'Warning information' }); more.append({ type: 'textarea', label: 'Optional message:', name: 'reason', tooltip: 'Perhaps a reason, or that a more detailed notice must be appended' }); const previewlink = document.createElement('a'); $(previewlink).on('click', () => { Twinkle.warn.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = 'pointer'; previewlink.textContent = 'Preview'; more.append({ type: 'div', id: 'warningpreview', label: [ previewlink ] }); more.append({ type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' }); more.append({ type: 'submit', label: 'Submit' }); var result = form.render(); dialog.setContent(result); dialog.display(); result.main_group.root = result; result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklewarn-previewbox').last()[0]); // Potential notices for staleness and missed reverts const vanrevid = Twinkle.getPrefill('vanarticlerevid'); if (vanrevid) { let message = ''; let query = {}; // If you tried reverting, check if *you* actually reverted if (!Twinkle.getPrefill('noautowarn') && Twinkle.getPrefill('vanarticle')) { // Via rollback link query = { action: 'query', titles: Twinkle.getPrefill('vanarticle'), prop: 'revisions', rvstartid: vanrevid, rvlimit: 2, rvdir: 'newer', rvprop: 'user', format: 'json' }; new Morebits.wiki.Api('Checking if you successfully reverted the page', query, ((apiobj) => { const rev = apiobj.getResponse().query.pages[0].revisions; const revertUser = rev && rev[1].user; if (revertUser && revertUser !== mw.config.get('wgUserName')) { message += ' Someone else reverted the page and may have already warned the user.'; $('#twinkle-warn-warning-messages').text('Note:' + message); } })).post(); } // Confirm edit wasn't too old for a warning const checkStale = function(vantimestamp) { const revDate = new Morebits.Date(vantimestamp); if (vantimestamp && revDate.isValid()) { if (revDate.add(24, 'hours').isBefore(new Date())) { message += ' This edit was made more than 24 hours ago so a warning may be stale.'; $('#twinkle-warn-warning-messages').text('Note:' + message); } } }; let vantimestamp = Twinkle.getPrefill('vantimestamp'); // If from a rollback module-based revert, no API lookup necessary if (vantimestamp) { checkStale(vantimestamp); } else { query = { action: 'query', prop: 'revisions', rvprop: 'timestamp', revids: vanrevid, format: 'json' }; new Morebits.wiki.Api('Grabbing the revision timestamps', query, ((apiobj) => { const rev = apiobj.getResponse().query.pages[0].revisions; vantimestamp = rev && rev[0].timestamp; checkStale(vantimestamp); })).post(); } } // We must init the first choice (General Note); const evt = document.createEvent('Event'); evt.initEvent('change', true, true); result.main_group.dispatchEvent(evt); }; // This is all the messages that might be dispatched by the code // Each of the individual templates require the following information: // label (required): A short description displayed in the dialog // summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with "." // suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such. // hideLinkedPage (optional): Set to true to hide the "Linked page" text box. Some warning templates do not have a linked article parameter. // hideReason (optional): Set to true to hide the "Optional message" text box. Some warning templates do not have a reason parameter. Twinkle.warn.messages = { levels: { 'Common warnings': { 'uw-vandalism': { level1: { label: 'Vandalism', summary: 'General note: Unconstructive editing' }, level2: { label: 'Vandalism', summary: 'Caution: Unconstructive editing' }, level3: { label: 'Vandalism', summary: 'Warning: Vandalism' }, level4: { label: 'Vandalism', summary: 'Final warning: Vandalism' }, level4im: { label: 'Vandalism', summary: 'Only warning: Vandalism' } }, 'uw-disruptive': { level1: { label: 'Disruptive editing', summary: 'General note: Unconstructive editing' }, level2: { label: 'Disruptive editing', summary: 'Caution: Unconstructive editing' }, level3: { label: 'Disruptive editing', summary: 'Warning: Disruptive editing' } }, 'uw-test': { level1: { label: 'Editing tests', summary: 'General note: Editing tests' }, level2: { label: 'Editing tests', summary: 'Caution: Editing tests' }, level3: { label: 'Editing tests', summary: 'Warning: Editing tests' } }, 'uw-delete': { level1: { label: 'Removal of content, blanking', summary: 'General note: Removal of content, blanking' }, level2: { label: 'Removal of content, blanking', summary: 'Caution: Removal of content, blanking' }, level3: { label: 'Removal of content, blanking', summary: 'Warning: Removal of content, blanking' }, level4: { label: 'Removal of content, blanking', summary: 'Final warning: Removal of content, blanking' }, level4im: { label: 'Removal of content, blanking', summary: 'Only warning: Removal of content, blanking' } }, 'uw-generic': { level4: { label: 'Generic warning (for template series missing level 4)', summary: 'Final warning notice' } } }, 'Behavior in articles': { 'uw-ai': { level1: { label: 'Using a large language model', summary: 'General note: Using a large language model' }, level2: { label: 'Using a large language model', summary: 'Caution: Using a large language model' }, level3: { label: 'Using a large language model', summary: 'Warning: Using a large language model' }, level4: { label: 'Using a large language model', summary: 'Final warning: Using a large language model' } }, 'uw-biog': { level1: { label: 'Adding unreferenced controversial information about living persons', summary: 'General note: Adding unreferenced controversial information about living persons' }, level2: { label: 'Adding unreferenced controversial information about living persons', summary: 'Caution: Adding unreferenced controversial information about living persons' }, level3: { label: 'Adding unreferenced controversial/defamatory information about living persons', summary: 'Warning: Adding unreferenced controversial information about living persons' }, level4: { label: 'Adding unreferenced defamatory information about living persons', summary: 'Final warning: Adding unreferenced controversial information about living persons' }, level4im: { label: 'Adding unreferenced defamatory information about living persons', summary: 'Only warning: Adding unreferenced controversial information about living persons' } }, 'uw-defamatory': { level1: { label: 'Addition of defamatory content', summary: 'General note: Addition of defamatory content' }, level2: { label: 'Addition of defamatory content', summary: 'Caution: Addition of defamatory content' }, level3: { label: 'Addition of defamatory content', summary: 'Warning: Addition of defamatory content' }, level4: { label: 'Addition of defamatory content', summary: 'Final warning: Addition of defamatory content' }, level4im: { label: 'Addition of defamatory content', summary: 'Only warning: Addition of defamatory content' } }, 'uw-error': { level1: { label: 'Introducing deliberate factual errors', summary: 'General note: Introducing factual errors' }, level2: { label: 'Introducing deliberate factual errors', summary: 'Caution: Introducing factual errors' }, level3: { label: 'Introducing deliberate factual errors', summary: 'Warning: Introducing deliberate factual errors' }, level4: { label: 'Introducing deliberate factual errors', summary: 'Final warning: Introducing deliberate factual errors' } }, 'uw-fringe': { level1: { label: 'Introducing fringe theories', summary: 'General note: Introducing fringe theories' }, level2: { label: 'Introducing fringe theories', summary: 'Caution: Introducing fringe theories' }, level3: { label: 'Introducing fringe theories', summary: 'Warning: Introducing fringe theories' } }, 'uw-genre': { level1: { label: 'Frequent or mass changes to genres without consensus or references', summary: 'General note: Frequent or mass changes to genres without consensus or references' }, level2: { label: 'Frequent or mass changes to genres without consensus or references', summary: 'Caution: Frequent or mass changes to genres without consensus or references' }, level3: { label: 'Frequent or mass changes to genres without consensus or reference', summary: 'Warning: Frequent or mass changes to genres without consensus or reference' }, level4: { label: 'Frequent or mass changes to genres without consensus or reference', summary: 'Final warning: Frequent or mass changes to genres without consensus or reference' } }, 'uw-image': { level1: { label: 'Image-related vandalism in articles', summary: 'General note: Image-related vandalism in articles' }, level2: { label: 'Image-related vandalism in articles', summary: 'Caution: Image-related vandalism in articles' }, level3: { label: 'Image-related vandalism in articles', summary: 'Warning: Image-related vandalism in articles' }, level4: { label: 'Image-related vandalism in articles', summary: 'Final warning: Image-related vandalism in articles' }, level4im: { label: 'Image-related vandalism', summary: 'Only warning: Image-related vandalism' } }, 'uw-joke': { level1: { label: 'Using improper humor in articles', summary: 'General note: Using improper humor in articles' }, level2: { label: 'Using improper humor in articles', summary: 'Caution: Using improper humor in articles' }, level3: { label: 'Using improper humor in articles', summary: 'Warning: Using improper humor in articles' }, level4: { label: 'Using improper humor in articles', summary: 'Final warning: Using improper humor in articles' }, level4im: { label: 'Using improper humor', summary: 'Only warning: Using improper humor' } }, 'uw-nor': { level1: { label: 'Adding original research', summary: 'General note: Adding original research' }, level2: { label: 'Adding original research', summary: 'Caution: Adding original research' }, level3: { label: 'Adding original research', summary: 'Warning: Adding original research' }, level4: { label: 'Adding original research', summary: 'Final warning: Adding original research' } }, 'uw-notcensored': { level1: { label: 'Censorship of material', summary: 'General note: Censorship of material' }, level2: { label: 'Censorship of material', summary: 'Caution: Censorship of material' }, level3: { label: 'Censorship of material', summary: 'Warning: Censorship of material' } }, 'uw-own': { level1: { label: 'Ownership of articles', summary: 'General note: Ownership of articles' }, level2: { label: 'Ownership of articles', summary: 'Caution: Ownership of articles' }, level3: { label: 'Ownership of articles', summary: 'Warning: Ownership of articles' }, level4: { label: 'Ownership of articles', summary: 'Final warning: Ownership of articles' }, level4im: { label: 'Ownership of articles', summary: 'Only warning: Ownership of articles' } }, 'uw-pronouns': { level1: { label: 'Introducing incorrect pronouns', summary: 'General note: Introducing incorrect pronouns' }, level2: { label: 'Introducing incorrect pronouns', summary: 'Caution: Introducing incorrect pronouns' }, level3: { label: 'Introducing incorrect pronouns', summary: 'Warning: Introducing incorrect pronouns' } }, 'uw-subtle': { level1: { label: 'Subtle vandalism', summary: 'General note: Possible unconstructive editing' }, level2: { label: 'Subtle vandalism', summary: 'Caution: Likely unconstructive editing' }, level3: { label: 'Subtle vandalism', summary: 'Warning: Subtle vandalism' }, level4: { label: 'Subtle vandalism', summary: 'Final warning: Subtle vandalism' } }, 'uw-talkinarticle': { level1: { label: 'Adding commentary to an article', summary: 'General note: Adding commentary to an article' }, level2: { label: 'Adding commentary to an article', summary: 'Caution: Adding commentary to an article' }, level3: { label: 'Adding commentary to an article', summary: 'Warning: Adding commentary to an article' } }, 'uw-tdel': { level1: { label: 'Removal of maintenance templates', summary: 'General note: Removal of maintenance templates' }, level2: { label: 'Removal of maintenance templates', summary: 'Caution: Removal of maintenance templates' }, level3: { label: 'Removal of maintenance templates', summary: 'Warning: Removal of maintenance templates' }, level4: { label: 'Removal of maintenance templates', summary: 'Final warning: Removal of maintenance templates' } }, 'uw-unsourced': { level1: { label: 'Addition of unsourced or improperly cited material', summary: 'General note: Addition of unsourced or improperly cited material' }, level2: { label: 'Addition of unsourced or improperly cited material', summary: 'Caution: Addition of unsourced or improperly cited material' }, level3: { label: 'Addition of unsourced or improperly cited material', summary: 'Warning: Addition of unsourced or improperly cited material' }, level4: { label: 'Addition of unsourced or improperly cited material', summary: 'Final warning: Addition of unsourced or improperly cited material' } } }, 'Promotions and spam': { 'uw-advert': { level1: { label: 'Using Wikipedia for advertising or promotion', summary: 'General note: Using Wikipedia for advertising or promotion' }, level2: { label: 'Using Wikipedia for advertising or promotion', summary: 'Caution: Using Wikipedia for advertising or promotion' }, level3: { label: 'Using Wikipedia for advertising or promotion', summary: 'Warning: Using Wikipedia for advertising or promotion' }, level4: { label: 'Using Wikipedia for advertising or promotion', summary: 'Final warning: Using Wikipedia for advertising or promotion' }, level4im: { label: 'Using Wikipedia for advertising or promotion', summary: 'Only warning: Using Wikipedia for advertising or promotion' } }, 'uw-npov': { level1: { label: 'Not adhering to neutral point of view', summary: 'General note: Not adhering to neutral point of view' }, level2: { label: 'Not adhering to neutral point of view', summary: 'Caution: Not adhering to neutral point of view' }, level3: { label: 'Not adhering to neutral point of view', summary: 'Warning: Not adhering to neutral point of view' }, level4: { label: 'Not adhering to neutral point of view', summary: 'Final warning: Not adhering to neutral point of view' } }, 'uw-paid': { level1: { label: 'Paid editing without disclosure under the Wikimedia Terms of Use', summary: 'General note: Disclosure requirements for paid editing under the Wikimedia Terms of Use' }, level2: { label: 'Paid editing without disclosure under the Wikimedia Terms of Use', summary: 'Caution: Disclosure requirements for paid editing under the Wikimedia Terms of Use' }, level3: { label: 'Paid editing without disclosure under the Wikimedia Terms of Use', summary: 'Warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use' }, level4: { label: 'Paid editing without disclosure under the Wikimedia Terms of Use', summary: 'Final warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use' } }, 'uw-spam': { level1: { label: 'Adding inappropriate external links', summary: 'General note: Adding inappropriate external links' }, level2: { label: 'Adding spam links', summary: 'Caution: Adding spam links' }, level3: { label: 'Adding spam links', summary: 'Warning: Adding spam links' }, level4: { label: 'Adding spam links', summary: 'Final warning: Adding spam links' }, level4im: { label: 'Adding spam links', summary: 'Only warning: Adding spam links' } } }, 'Behavior towards other editors': { 'uw-agf': { level1: { label: 'Not assuming good faith', summary: 'General note: Not assuming good faith' }, level2: { label: 'Not assuming good faith', summary: 'Caution: Not assuming good faith' }, level3: { label: 'Not assuming good faith', summary: 'Warning: Not assuming good faith' } }, 'uw-aitalk': { level1: { label: 'Posting LLM-generated comments', summary: 'General note: Posting LLM-generated comments' }, level2: { label: 'Posting LLM-generated comments', summary: 'Caution: Posting LLM-generated comments' }, level3: { label: 'Posting LLM-generated comments', summary: 'Warning: Posting LLM-generated comments' }, level4: { label: 'Posting LLM-generated comments', summary: 'Final warning: Posting LLM-generated comments' } }, 'uw-harass': { level1: { label: 'Harassment of other users', summary: 'General note: Harassment of other users' }, level2: { label: 'Harassment of other users', summary: 'Caution: Harassment of other users' }, level3: { label: 'Harassment of other users', summary: 'Warning: Harassment of other users' }, level4: { label: 'Harassment of other users', summary: 'Final warning: Harassment of other users' }, level4im: { label: 'Harassment of other users', summary: 'Only warning: Harassment of other users' } }, 'uw-npa': { level1: { label: 'Personal attack directed at a specific editor', summary: 'General note: Personal attack directed at a specific editor' }, level2: { label: 'Personal attack directed at a specific editor', summary: 'Caution: Personal attack directed at a specific editor' }, level3: { label: 'Personal attack directed at a specific editor', summary: 'Warning: Personal attack directed at a specific editor' }, level4: { label: 'Personal attack directed at a specific editor', summary: 'Final warning: Personal attack directed at a specific editor' }, level4im: { label: 'Personal attack directed at a specific editor', summary: 'Only warning: Personal attack directed at a specific editor' } }, 'uw-tempabuse': { level1: { label: 'Improper use of warning or blocking template', summary: 'General note: Improper use of warning or blocking template' }, level2: { label: 'Improper use of warning or blocking template', summary: 'Caution: Improper use of warning or blocking template' } } }, 'Removal of deletion tags': { 'uw-afd': { level1: { label: 'Removing {{afd}} templates', summary: 'General note: Removing {{afd}} templates' }, level2: { label: 'Removing {{afd}} templates', summary: 'Caution: Removing {{afd}} templates' }, level3: { label: 'Removing {{afd}} templates', summary: 'Warning: Removing {{afd}} templates' }, level4: { label: 'Removing {{afd}} templates', summary: 'Final warning: Removing {{afd}} templates' } }, 'uw-blpprod': { level1: { label: 'Removing {{blp prod}} templates', summary: 'General note: Removing {{blp prod}} templates' }, level2: { label: 'Removing {{blp prod}} templates', summary: 'Caution: Removing {{blp prod}} templates' }, level3: { label: 'Removing {{blp prod}} templates', summary: 'Warning: Removing {{blp prod}} templates' }, level4: { label: 'Removing {{blp prod}} templates', summary: 'Final warning: Removing {{blp prod}} templates' } }, 'uw-idt': { level1: { label: 'Removing file deletion tags', summary: 'General note: Removing file deletion tags' }, level2: { label: 'Removing file deletion tags', summary: 'Caution: Removing file deletion tags' }, level3: { label: 'Removing file deletion tags', summary: 'Warning: Removing file deletion tags' }, level4: { label: 'Removing file deletion tags', summary: 'Final warning: Removing file deletion tags' } }, 'uw-rfd': { level1: { label: 'Removing redirects for discussion tags', summary: 'General note: Removing redirects for discussion tags' }, level2: { label: 'Removing redirects for discussion tags', summary: 'Caution: Removing redirects for discussion tags' }, level3: { label: 'Removing redirects for discussion tags', summary: 'Warning: Removing redirects for discussion tags' }, level4: { label: 'Removing redirects for discussion tags', summary: 'Final warning: Removing redirects for discussion tags' } }, 'uw-speedy': { level1: { label: 'Removing speedy deletion tags', summary: 'General note: Removing speedy deletion tags' }, level2: { label: 'Removing speedy deletion tags', summary: 'Caution: Removing speedy deletion tags' }, level3: { label: 'Removing speedy deletion tags', summary: 'Warning: Removing speedy deletion tags' }, level4: { label: 'Removing speedy deletion tags', summary: 'Final warning: Removing speedy deletion tags' } }, 'uw-tfd': { level1: { label: 'Removing {{tfd}} templates', summary: 'General note: Removing {{tfd}} templates' }, level2: { label: 'Removing {{tfd}} templates', summary: 'Caution: Removing {{tfd}} templates' }, level3: { label: 'Removing {{tfd}} templates', summary: 'Warning: Removing {{tfd}} templates' }, level4: { label: 'Removing {{tfd}} templates', summary: 'Final warning: Removing {{tfd}} templates' } } }, Other: { 'uw-attempt': { level1: { label: 'Triggering the edit filter', summary: 'General note: Triggering the edit filter' }, level2: { label: 'Triggering the edit filter', summary: 'Caution: Triggering the edit filter' }, level3: { label: 'Triggering the edit filter', summary: 'Warning: Triggering the edit filter' }, level4: { label: 'Triggering the edit filter', summary: 'Final warning: Triggering the edit filter' }, level4im: { label: 'Triggering the edit filter', summary: 'Only warning: Triggering the edit filter' } }, 'uw-chat': { level1: { label: 'Using talk page as forum', summary: 'General note: Using talk page as forum' }, level2: { label: 'Using talk page as forum', summary: 'Caution: Using talk page as forum' }, level3: { label: 'Using talk page as forum', summary: 'Warning: Using talk page as forum' }, level4: { label: 'Using talk page as forum', summary: 'Final warning: Using talk page as forum' } }, 'uw-create': { level1: { label: 'Creating inappropriate pages', summary: 'General note: Creating inappropriate pages' }, level2: { label: 'Creating inappropriate pages', summary: 'Caution: Creating inappropriate pages' }, level3: { label: 'Creating inappropriate pages', summary: 'Warning: Creating inappropriate pages' }, level4: { label: 'Creating inappropriate pages', summary: 'Final warning: Creating inappropriate pages' }, level4im: { label: 'Creating inappropriate pages', summary: 'Only warning: Creating inappropriate pages' } }, 'uw-fv': { level1: { label: 'Added statement had source, but it did not verify content', summary: 'General note: Added statement had source, but it did not verify content' } }, 'uw-mislead': { level1: { label: 'Using misleading edit summaries', summary: 'General note: Using misleading edit summaries' }, level2: { label: 'Using misleading edit summaries', summary: 'Caution: Using misleading edit summaries' }, level3: { label: 'Using misleading edit summaries', summary: 'Warning: Using misleading edit summaries' } }, 'uw-mos': { level1: { label: 'Manual of style', summary: 'General note: Formatting, date, language, etc (Manual of style)' }, level2: { label: 'Manual of style', summary: 'Caution: Formatting, date, language, etc (Manual of style)' }, level3: { label: 'Manual of style', summary: 'Warning: Formatting, date, language, etc (Manual of style)' }, level4: { label: 'Manual of style', summary: 'Final warning: Formatting, date, language, etc (Manual of style)' } }, 'uw-move': { level1: { label: 'Page moves against naming conventions or consensus', summary: 'General note: Page moves against naming conventions or consensus' }, level2: { label: 'Page moves against naming conventions or consensus', summary: 'Caution: Page moves against naming conventions or consensus' }, level3: { label: 'Page moves against naming conventions or consensus', summary: 'Warning: Page moves against naming conventions or consensus' }, level4: { label: 'Page moves against naming conventions or consensus', summary: 'Final warning: Page moves against naming conventions or consensus' }, level4im: { label: 'Page moves against naming conventions or consensus', summary: 'Only warning: Page moves against naming conventions or consensus' } }, 'uw-redirect': { level1: { label: 'Creating inappropriate redirects', summary: 'General note: Creating inappropriate redirects' }, level2: { label: 'Creating inappropriate redirects', summary: 'Caution: Creating inappropriate redirects' }, level3: { label: 'Creating inappropriate redirects', summary: 'Warning: Creating inappropriate redirects' }, level4: { label: 'Creating inappropriate redirects', summary: 'Final warning: Creating inappropriate redirects' }, level4im: { label: 'Creating inappropriate redirects', summary: 'Only warning: Creating inappropriate redirects' } }, 'uw-tpv': { level1: { label: "Refactoring others' talk page comments", summary: "General note: Refactoring others' talk page comments" }, level2: { label: "Refactoring others' talk page comments", summary: "Caution: Refactoring others' talk page comments" }, level3: { label: "Refactoring others' talk page comments", summary: "Warning: Refactoring others' talk page comments" }, level4: { label: "Refactoring others' talk page comments", summary: "Final warning: Refactoring others' talk page comments" }, level4im: { label: "Refactoring others' talk page comments", summary: "Only warning: Refactoring others' talk page comments" } }, 'uw-upload': { level1: { label: 'Uploading unencyclopedic images', summary: 'General note: Uploading unencyclopedic images' }, level2: { label: 'Uploading unencyclopedic images', summary: 'Caution: Uploading unencyclopedic images' }, level3: { label: 'Uploading unencyclopedic images', summary: 'Warning: Uploading unencyclopedic images' }, level4: { label: 'Uploading unencyclopedic images', summary: 'Final warning: Uploading unencyclopedic images' }, level4im: { label: 'Uploading unencyclopedic images', summary: 'Only warning: Uploading unencyclopedic images' } } } }, singlenotice: { 'uw-addalink': { label: 'Mistakes with the Add a Link newcomer task', summary: 'Notice: Mistakes with the Add a Link newcomer task' }, 'uw-agf-sock': { label: 'Use of multiple accounts (assuming good faith)', summary: 'Notice: Using multiple accounts' }, 'uw-aiv': { label: 'Bad AIV report', summary: 'Notice: Bad AIV report' }, 'uw-articletodraft': { label: 'Article moved to draftspace', summary: 'Notice: Article moved to draftspace', hideReason: true }, 'uw-autobiography': { label: 'Creating autobiographies', summary: 'Notice: Creating autobiographies' }, 'uw-badcat': { label: 'Adding incorrect categories', summary: 'Notice: Adding incorrect categories' }, 'uw-badlistentry': { label: 'Adding inappropriate entries to lists', summary: 'Notice: Adding inappropriate entries to lists' }, 'uw-bareurl': { label: 'Adding a bare URL', summary: 'Notice: Adding a bare URL' }, 'uw-bite': { label: '"Biting" newcomers', summary: 'Notice: "Biting" newcomers', suppressArticleInSummary: true // non-standard (user name, not article), and not necessary }, 'uw-blar': { label: 'Article blanked and redirected', summary: 'Notice: Article blanked and redirected', hideReason: true }, 'uw-circular': { label: 'Using circular sources', summary: 'Notice: Using circular sources' }, 'uw-coi': { label: 'Conflict of interest', summary: 'Notice: Conflict of interest', heading: 'Managing a conflict of interest' }, 'uw-copying': { label: 'Copying text to another page', summary: 'Notice: Copying text to another page' }, 'uw-crystal': { label: 'Adding speculative or unconfirmed information', summary: 'Notice: Adding speculative or unconfirmed information' }, 'uw-c&pmove': { label: 'Cut and paste moves', summary: 'Notice: Cut and paste moves' }, 'uw-dab': { label: 'Incorrect edit to a disambiguation page', summary: 'Notice: Incorrect edit to a disambiguation page' }, 'uw-date': { label: 'Unnecessarily changing date formats', summary: 'Notice: Unnecessarily changing date formats' }, 'uw-deadlink': { label: 'Removing proper sources containing dead links', summary: 'Notice: Removing proper sources containing dead links' }, 'uw-displaytitle': { label: 'Incorrect use of DISPLAYTITLE', summary: 'Notice: Incorrect use of DISPLAYTITLE' }, 'uw-draftfirst': { label: 'User should draft in userspace without the risk of speedy deletion', summary: 'Notice: Consider drafting your article in [[Help:Userspace draft|userspace]]' }, 'uw-editsummary': { label: 'New user not using edit summary', summary: 'Notice: Not using edit summary' }, 'uw-editsummary2': { label: 'Experienced user not using edit summary', summary: 'Notice: Not using edit summary', hideLinkedPage: true, hideReason: true }, 'uw-elinbody': { label: 'Adding external links to the body of an article', summary: 'Notice: Keep external links to External links sections at the bottom of an article' }, 'uw-english': { label: 'Not communicating in English', summary: 'Notice: Not communicating in English' }, 'uw-hasty': { label: 'Hasty addition of speedy deletion tags', summary: 'Notice: Allow creators time to improve their articles before tagging them for deletion' }, 'uw-islamhon': { label: 'Use of Islamic honorifics', summary: 'Notice: Use of Islamic honorifics' }, 'uw-italicize': { label: 'Italicize books, films, albums, magazines, TV series, etc within articles', summary: 'Notice: Italicize books, films, albums, magazines, TV series, etc within articles' }, 'uw-lang': { label: 'Unnecessarily changing between British and American English', summary: 'Notice: Unnecessarily changing between British and American English', heading: 'National varieties of English' }, 'uw-linking': { label: 'Excessive addition of redlinks or repeated blue links', summary: 'Notice: Excessive addition of redlinks or repeated blue links' }, 'uw-longsd': { label: 'Insertion of long short description', summary: 'Notice: Insertion of long short description' }, 'uw-minor': { label: 'Incorrect use of minor edits check box', summary: 'Notice: Incorrect use of minor edits check box' }, 'uw-mostm': { label: 'Formatting of trademarks', summary: 'Notice: Formatting of trademarks' }, 'uw-multiple-accts': { label: 'Inappropriate use of alternative accounts', summary: 'Notice: Inappropriate use of alternative accounts' }, 'uw-notenglish': { label: 'Creating non-English articles', summary: 'Notice: Creating non-English articles' }, 'uw-notenglishedit': { label: 'Adding non-English content to articles', summary: 'Notice: Adding non-English content to articles' }, 'uw-notvote': { label: 'We use consensus, not voting', summary: 'Notice: We use consensus, not voting' }, 'uw-orphantalk': { label: 'Talk page created with no article', summary: 'Notice: Talk page created with no article' }, 'uw-plagiarism': { label: 'Copying from public domain sources without attribution', summary: 'Notice: Copying from public domain sources without attribution' }, 'uw-preview': { label: 'Use preview button to avoid mistakes', summary: 'Notice: Use preview button to avoid mistakes' }, 'uw-redlink': { label: 'Indiscriminate removal of redlinks', summary: 'Notice: Be careful when removing redlinks' }, 'uw-refspam': { label: 'Adding citations to research published by a small group of researchers', summary: 'Notice: Adding citations to research published by a small group of researchers', hideLinkedPage: true, hideReason: true }, 'uw-selfrevert': { label: 'Self-reverted editing tests', summary: 'Notice: Self-reverted editing tests' }, 'uw-socialnetwork': { label: 'Wikipedia is not a social network', summary: 'Notice: Wikipedia is not a social network' }, 'uw-sofixit': { label: 'Be bold and fix things yourself', summary: 'Notice: You can be bold and fix things yourself' }, 'uw-spoiler': { label: 'Adding spoiler alerts or removing spoilers from appropriate sections', summary: "Notice: Don't delete or flag potential 'spoilers' in Wikipedia articles" }, 'uw-talkinarticle': { label: 'Talk in article', summary: 'Notice: Talk in article' }, 'uw-tilde': { label: 'Not signing posts', summary: 'Notice: Not signing posts' }, 'uw-toppost': { label: 'Posting at the top of talk pages', summary: 'Notice: Posting at the top of talk pages' }, 'uw-translation': { label: 'Adding translations without proper attribution', summary: 'Notice: Attribution required when translating articles' }, 'uw-unattribcc': { label: 'Copying from compatibly-licensed sources without attribution', summary: 'Notice: Copying from compatibly-licensed sources without attribution' }, 'uw-userspace draft finish': { label: 'Stale userspace draft', summary: 'Notice: Stale userspace draft' }, 'uw-usertalk': { label: 'Misuse of user talk page', summary: 'Notice: Misuse of user talk page', hideLinkedPage: true }, 'uw-vgscope': { label: 'Adding video game walkthroughs, cheats or instructions', summary: 'Notice: Adding video game walkthroughs, cheats or instructions' }, 'uw-warn': { label: 'Place user warning templates when reverting vandalism', summary: 'Notice: You can use user warning templates when reverting vandalism' }, 'uw-wrongsummary': { label: 'Using inaccurate or inappropriate edit summaries', summary: 'Notice: Using inaccurate or inappropriate edit summaries' } }, singlewarn: { 'uw-3rr': { label: 'Potential three-revert rule violation; see also uw-ew', summary: 'Warning: Three-revert rule' }, 'uw-affiliate': { label: 'Affiliate marketing', summary: 'Warning: Affiliate marketing' }, 'uw-attack': { label: 'Creating attack pages', summary: 'Warning: Creating attack pages', suppressArticleInSummary: true }, 'uw-botun': { label: 'Bot username', summary: 'Warning: Bot username' }, 'uw-canvass': { label: 'Canvassing', summary: 'Warning: Canvassing' }, 'uw-copyright': { label: 'Copyright violation', summary: 'Warning: Copyright violation' }, 'uw-copyright-link': { label: 'Linking to copyrighted works violation', summary: 'Warning: Linking to copyrighted works violation' }, 'uw-copyright-new': { label: 'Copyright violation (with explanation for new users)', summary: 'Notice: Avoiding copyright problems', heading: 'Wikipedia and copyright' }, 'uw-copyright-remove': { label: 'Removing {{copyvio}} template from articles', summary: 'Warning: Removing {{copyvio}} templates' }, 'uw-derogatory': { label: 'Addition of derogatory/hateful content', summary: 'Warning: Addition of derogatory content' }, 'uw-efsummary': { label: 'Edit summary triggering the edit filter', summary: 'Warning: Edit summary triggering the edit filter' }, 'uw-ew': { label: 'Edit warring (stronger wording)', summary: 'Warning: Edit warring' }, 'uw-ewsoft': { label: 'Edit warring (softer wording for newcomers)', summary: 'Warning: Edit warring' }, 'uw-hijacking': { label: 'Hijacking articles', summary: 'Warning: Hijacking articles' }, 'uw-hoax': { label: 'Creating hoaxes', summary: 'Warning: Creating hoaxes' }, 'uw-legal': { label: 'Making legal threats', summary: 'Warning: Making legal threats' }, 'uw-login': { label: 'Editing while logged out', summary: 'Warning: Editing while logged out' }, 'uw-multipleTAs': { label: 'Usage of multiple temporary accounts', summary: 'Warning: Vandalism using multiple temporary accounts' }, 'uw-paraphrase': { label: 'Close paraphrasing', summary: 'Warning: Close paraphrasing' }, 'uw-pinfo': { label: 'Personal info (outing)', summary: 'Warning: Personal info' }, 'uw-salt': { label: 'Recreating salted articles under a different title', summary: 'Notice: Recreating creation-protected articles under a different title' }, 'uw-socksuspect': { label: 'Sockpuppetry', summary: 'Warning: You are a suspected [[WP:SOCK|sockpuppet]]' // of User:... }, 'uw-upv': { label: 'Userpage vandalism', summary: 'Warning: Userpage vandalism' }, 'uw-username': { label: 'Username is against policy', summary: 'Warning: Your username might be against policy', suppressArticleInSummary: true // not relevant for this template }, 'uw-coi-username': { label: 'Username is against policy, and conflict of interest', summary: 'Warning: Username and conflict of interest', heading: 'Your username' }, 'uw-userpage': { label: 'Userpage or subpage is against policy', summary: 'Warning: Userpage or subpage is against policy' } } }; /** * Reads Twinkle.warn.messages and returns a specified template's property (such as label, summary, * suppressArticleInSummary, hideLinkedPage, or hideReason) */ Twinkle.warn.getTemplateProperty = function(templates, templateName, propertyName) { let result; const isNumberedTemplate = templateName.match(/(1|2|3|4|4im)$/); if (isNumberedTemplate) { const unNumberedTemplateName = templateName.replace(/(?:1|2|3|4|4im)$/, ''); const level = isNumberedTemplate[0]; const numberedWarnings = {}; $.each(templates.levels, (key, val) => { $.extend(numberedWarnings, val); }); $.each(numberedWarnings, (key) => { if (key === unNumberedTemplateName) { result = numberedWarnings[key]['level' + level][propertyName]; } }); } // Non-level templates can also end in a number. So check this for all templates. const otherWarnings = {}; $.each(templates, (key, val) => { if (key !== 'levels') { $.extend(otherWarnings, val); } }); $.each(otherWarnings, (key) => { if (key === templateName) { result = otherWarnings[key][propertyName]; } }); return result; }; // Used repeatedly below across menu rebuilds Twinkle.warn.prev_article = null; Twinkle.warn.prev_reason = null; Twinkle.warn.talkpageObj = null; Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) { const value = e.target.value; const sub_group = e.target.root.sub_group; sub_group.main_group = value; let old_subvalue = sub_group.value; let old_subvalue_re; if (old_subvalue) { if (value === 'kitchensink') { // Exact match possible in kitchensink menu old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue)); } else { old_subvalue = old_subvalue.replace(/\d*(im)?$/, ''); old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue) + '(\\d*(?:im)?)$'); } } while (sub_group.hasChildNodes()) { sub_group.removeChild(sub_group.firstChild); } let selected = false; // worker function to create the combo box entries const createEntries = function(contents, container, wrapInOptgroup, val = value) { // level2->2, singlewarn->''; also used to distinguish the // scaled levels from singlenotice, singlewarn, and custom const level = val.replace(/^\D+/g, ''); // due to an apparent iOS bug, we have to add an option-group to prevent truncation of text // (search WT:TW archives for "Problem selecting warnings on an iPhone") if (wrapInOptgroup && $.client.profile().platform === 'iphone') { let wrapperOptgroup = new Morebits.QuickForm.Element({ type: 'optgroup', label: 'Available templates' }); wrapperOptgroup = wrapperOptgroup.render(); container.appendChild(wrapperOptgroup); container = wrapperOptgroup; } $.each(contents, (itemKey, itemProperties) => { // Skip if the current template doesn't have a version for the current level if (!!level && !itemProperties[val]) { return; } const key = typeof itemKey === 'string' ? itemKey : itemProperties.value; const template = key + level; const elem = new Morebits.QuickForm.Element({ type: 'option', label: '{{' + template + '}}: ' + (level ? itemProperties[val].label : itemProperties.label), value: template }); // Select item best corresponding to previous selection if (!selected && old_subvalue && old_subvalue_re.test(template)) { elem.data.selected = selected = true; } const elemRendered = container.appendChild(elem.render()); $(elemRendered).data('messageData', itemProperties); }); }; const createGroup = function(warnGroup, label, wrapInOptgroup, val) { wrapInOptgroup = typeof wrapInOptgroup !== 'undefined' ? wrapInOptgroup : true; let optgroup = new Morebits.QuickForm.Element({ type: 'optgroup', label: label }); optgroup = optgroup.render(); sub_group.appendChild(optgroup); createEntries(warnGroup, optgroup, wrapInOptgroup, val); }; switch (value) { case 'singlenotice': case 'singlewarn': createEntries(Twinkle.warn.messages[value], sub_group, true); break; case 'singlecombined': var unSortedSinglets = $.extend({}, Twinkle.warn.messages.singlenotice, Twinkle.warn.messages.singlewarn); var sortedSingletMessages = {}; Object.keys(unSortedSinglets).sort().forEach((key) => { sortedSingletMessages[key] = unSortedSinglets[key]; }); createEntries(sortedSingletMessages, sub_group, true); break; case 'custom': createEntries(Twinkle.getPref('customWarningList'), sub_group, true); break; case 'kitchensink': ['level1', 'level2', 'level3', 'level4', 'level4im'].forEach((lvl) => { $.each(Twinkle.warn.messages.levels, (levelGroupLabel, levelGroup) => { createGroup(levelGroup, 'Level ' + lvl.slice(5) + ': ' + levelGroupLabel, true, lvl); }); }); createGroup(Twinkle.warn.messages.singlenotice, 'Single-issue notices'); createGroup(Twinkle.warn.messages.singlewarn, 'Single-issue warnings'); createGroup(Twinkle.getPref('customWarningList'), 'Custom warnings'); break; case 'level1': case 'level2': case 'level3': case 'level4': case 'level4im': // Creates subgroup regardless of whether there is anything to place in it; // leaves "Removal of deletion tags" empty for 4im $.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => { createGroup(groupContents, groupLabel, false); }); break; case 'autolevel': // Check user page to determine appropriate level var autolevelProc = function() { const wikitext = Twinkle.warn.talkpageObj.getPageText(); // history not needed for autolevel const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0]; // Pseudo-params with only what's needed to parse the level i.e. no messageData const params = { sub_group: old_subvalue, article: e.target.root.article.value }; const lvl = 'level' + Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[1]; // Identical to level1, etc. above but explicitly provides the level $.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => { createGroup(groupContents, groupLabel, false, lvl); }); // Trigger subcategory change, add select menu, etc. Twinkle.warn.callback.postCategoryCleanup(e); }; if (Twinkle.warn.talkpageObj) { autolevelProc(); } else { const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings'); usertalk_page.setFollowRedirect(true, false); usertalk_page.load((pageobj) => { Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj autolevelProc(); }, () => { // Catch and warn if the talkpage can't load, // most likely because it's a cross-namespace redirect // Supersedes the typical $autolevelMessage added in autolevelParseWikitext const $noTalkPageNode = $('<strong>') .text('Unable to load user talk page; it might be a cross-namespace redirect. Autolevel detection will not work.') .attr('id', 'twinkle-warn-autolevel-message') .css('color', 'red'); $noTalkPageNode.insertBefore($('#twinkle-warn-warning-messages')); // If a preview was opened while in a different mode, close it // Should nullify the need to catch the error in preview callback e.target.root.previewer.closePreview(); }); } break; default: alert('Unknown warning group in twinklewarn'); break; } // Trigger subcategory change, add select menu, etc. // Here because of the async load for autolevel if (value !== 'autolevel') { // reset any autolevel-specific messages while we're here $('#twinkle-warn-autolevel-message').remove(); Twinkle.warn.callback.postCategoryCleanup(e); } }; Twinkle.warn.callback.postCategoryCleanup = function twinklewarnCallbackPostCategoryCleanup(e) { // clear overridden label on article textbox Morebits.QuickForm.setElementTooltipVisibility(e.target.root.article, true); Morebits.QuickForm.resetElementLabel(e.target.root.article); // Trigger custom label/change on main category change Twinkle.warn.callback.change_subcategory(e); // Use select2 to make the select menu searchable if (!Twinkle.getPref('oldSelect')) { $('select[name=sub_group]') .select2({ theme: 'default select2-morebits', width: '100%', matcher: Morebits.select2.matchers.optgroupFull, templateResult: Morebits.select2.highlightSearchMatches, language: { searching: Morebits.select2.queryInterceptor } }) .change(Twinkle.warn.callback.change_subcategory); $('.select2-selection').on('keydown', Morebits.select2.autoStart).trigger('focus'); mw.util.addCSS( // Increase height '.select2-container .select2-dropdown .select2-results > .select2-results__options { max-height: 350px; }' + // Reduce padding '.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' + '.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' + // Adjust font size '.select2-container .select2-dropdown .select2-results { font-size: 13px; }' + '.select2-container .selection .select2-selection__rendered { font-size: 13px; }' ); } }; Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) { const selected_main_group = e.target.form.main_group.value; const selected_template = e.target.form.sub_group.value; // If template shouldn't have a linked article, hide the linked article label and text box const hideLinkedPage = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideLinkedPage'); if (hideLinkedPage) { e.target.form.article.value = ''; Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, false); } else { Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, true); } // If template shouldn't have an optional message, hide the optional message label and text box const hideReason = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideReason'); if (hideReason) { e.target.form.reason.value = ''; Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, false); } else { Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, true); } // Tags that don't take a linked article, but something else (often a username). // The value of each tag is the label next to the input field const notLinkedArticle = { 'uw-agf-sock': 'Optional username of other account (without User:) ', 'uw-bite': "Username of 'bitten' user (without User:) ", 'uw-socksuspect': 'Username of sock master, if known (without User:) ', 'uw-username': 'Username violates policy because... ', 'uw-aiv': 'Optional username that was reported (without User:) ' }; const hasLevel = ['singlenotice', 'singlewarn', 'singlecombined', 'kitchensink'].includes(selected_main_group); if (hasLevel) { if (notLinkedArticle[selected_template]) { if (Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.form.article.value; } e.target.form.article.notArticle = true; e.target.form.article.value = ''; // change form labels according to the warning selected Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, false); Morebits.QuickForm.overrideElementLabel(e.target.form.article, notLinkedArticle[selected_template]); } else if (e.target.form.article.notArticle) { if (Twinkle.warn.prev_article !== null) { e.target.form.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.form.article.notArticle = false; Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, true); Morebits.QuickForm.resetElementLabel(e.target.form.article); } } // add big red notice, warning users about how to use {{uw-[coi-]username}} appropriately $('#tw-warn-red-notice').remove(); let $redWarning; if (selected_template === 'uw-username') { $redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " + "Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " + '{{uw-username}} should only be used in edge cases in order to engage in discussion with the user.</div>'); $redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup)); } else if (selected_template === 'uw-coi-username') { $redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-coi-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " + "Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " + '{{uw-coi-username}} should only be used in edge cases in order to engage in discussion with the user.</div>'); $redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup)); } }; Twinkle.warn.callbacks = { getWarningWikitext: function(templateName, article, reason, isCustom) { let text = '{{subst:' + templateName; // add linked article for user warnings if (article) { // c&pmove has the source as the first parameter if (templateName === 'uw-c&pmove') { text += '|to=' + article; } else { text += '|1=' + article; } } if (reason && !isCustom) { // add extra message if (templateName === 'uw-userpage') { text += "|3=''" + Morebits.string.formatReasonText(reason) + "''"; } else { text += "|2=''" + Morebits.string.formatReasonText(reason) + "''"; } } text += '}}'; if (reason && isCustom) { // we assume that custom warnings lack a {{{2}}} parameter text += " ''" + reason + "''"; } return text + ' ~~~~'; }, showPreview: function(form, templatename) { const input = Morebits.QuickForm.getInputData(form); // Provided on autolevel, not otherwise templatename = templatename || input.sub_group; const linkedarticle = input.article; const templatetext = Twinkle.warn.callbacks.getWarningWikitext(templatename, linkedarticle, input.reason, input.main_group === 'custom'); form.previewer.beginRender(templatetext, 'User_talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username }, // Just a pass-through unless the autolevel option was selected preview: function(form) { if (form.main_group.value === 'autolevel') { // Always get a new, updated talkpage for autolevel processing const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings'); usertalk_page.setFollowRedirect(true, false); // Will fail silently if the talk page is a cross-ns redirect, // removal of the preview box handled when loading the menu usertalk_page.load((pageobj) => { Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj const wikitext = pageobj.getPageText(); // history not needed for autolevel const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0]; const params = { sub_group: form.sub_group.value, article: form.article.value, messageData: $(form.sub_group).find('option[value="' + $(form.sub_group).val() + '"]').data('messageData') }; const template = Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[0]; Twinkle.warn.callbacks.showPreview(form, template); // If the templates have diverged, fake a change event // to reload the menu with the updated pageobj if (form.sub_group.value !== template) { const evt = document.createEvent('Event'); evt.initEvent('change', true, true); form.main_group.dispatchEvent(evt); } }); } else { Twinkle.warn.callbacks.showPreview(form); } }, /** * Used in the main and autolevel loops to determine when to warn * about excessively recent, stale, or identical warnings. * * @param {string} wikitext The text of a user's talk page, from getPageText() * @return {Object[]} - Array of objects: latest contains most recent * warning and date; history lists all prior warnings */ dateProcessing: function(wikitext) { const history_re = /<!--\s?Template:([uU]w-.*?)\s?-->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4} \(UTC\))/g; const history = {}; const latest = { date: new Morebits.Date(0), type: '' }; let current; while ((current = history_re.exec(wikitext)) !== null) { const template = current[1], current_date = new Morebits.Date(current[2]); if (!(template in history) || history[template].isBefore(current_date)) { history[template] = current_date; } if (!latest.date.isAfter(current_date)) { latest.date = current_date; latest.type = template; } } return [latest, history]; }, // False positive // eslint-disable-next-line jsdoc/require-returns-check /** * Main loop for deciding what the level should increment to. Most of * this is really just error catching and updating the subsequent data. * May produce up to two notices in a twinkle-warn-autolevel-messages div * * @param {string} wikitext The text of a user's talk page, from getPageText() (required) * @param {Object} params Params object: sub_group is the template (required); * article is the user-provided article (form.article) used to link ARV on recent level4 warnings; * messageData is only necessary if getting the full template, as it's * used to ensure a valid template of that level exists * @param {Object} latest First element of the array returned from * dateProcessing. Provided here rather than processed within to avoid * repeated call to dateProcessing * @param {(Date|Morebits.Date)} date Date from which staleness is determined * @param {Morebits.Status} statelem Status element, only used for handling error in final execution * * @return {Array} - Array that contains the full template and just the warning level */ autolevelParseWikitext: function(wikitext, params, latest, date, statelem) { let level; // undefined rather than '' means the isNaN below will return true if (/\d(?:im)?$/.test(latest.type)) { // level1-4im level = parseInt(latest.type.replace(/.*(\d)(?:im)?$/, '$1'), 10); } else if (latest.type) { // Non-numbered warning // Try to leverage existing categorization of // warnings, all but one are universally lowercased const loweredType = /uw-multipleTAs/i.test(latest.type) ? 'uw-multipleTAs' : latest.type.toLowerCase(); // It would be nice to account for blocks, but in most // cases the hidden message is terminal, not the sig if (Twinkle.warn.messages.singlewarn[loweredType]) { level = 3; } else { level = 1; // singlenotice or not found } } const $autolevelMessage = $('<div>') .attr('id', 'twinkle-warn-autolevel-message'); if (isNaN(level)) { // No prior warnings found, this is the first level = 1; } else if (level > 4 || level < 1) { // Shouldn't happen const message = 'Unable to parse previous warning level, please manually select a warning level.'; if (statelem) { statelem.error(message); } else { alert(message); } return; } else { date = date || new Date(); const autoTimeout = new Morebits.Date(latest.date.getTime()).add(parseInt(Twinkle.getPref('autolevelStaleDays'), 10), 'days'); if (autoTimeout.isAfter(date)) { if (level === 4) { level = 4; // Basically indicates whether we're in the final Main evaluation or not, // and thus whether we can continue or need to display the warning and link if (!statelem) { const $link = $('<a>') .attr('href', '#') .text('click here to open the ARV tool.') .css('fontWeight', 'bold') .on('click', () => { Morebits.wiki.actionCompleted.redirect = null; Twinkle.warn.dialog.close(); Twinkle.arv.callback(mw.config.get('wgRelevantUserName')); $('input[name=page]').val(params.article); // Target page $('input[value=final]').prop('checked', true); // Vandalism after final }); const $statusNode = $('<div>') .text(mw.config.get('wgRelevantUserName') + ' recently received a level 4 warning (' + latest.type + ') so it might be better to report them instead; ') .css('color', 'red'); $statusNode.append($link[0]); $autolevelMessage.append($statusNode); } } else { // Automatically increase severity level += 1; } } else { // Reset warning level if most-recent warning is too old level = 1; } } $autolevelMessage.prepend($('<div>Will issue a <span style="font-weight: bold;">level ' + level + '</span> template.</div>')); // Place after the stale and other-user-reverted (text-only) messages $('#twinkle-warn-autolevel-message').remove(); // clean slate $autolevelMessage.insertAfter($('#twinkle-warn-warning-messages')); let template = params.sub_group.replace(/(.*)\d$/, '$1'); // Validate warning level, falling back to the uw-generic series. // Only a few items are missing a level, and in all but a handful // of cases, the uw-generic series is explicitly used elsewhere per WP:UTM. if (params.messageData && !params.messageData['level' + level]) { template = 'uw-generic'; } template += level; return [template, level]; }, main: function(pageobj) { const text = pageobj.getPageText(); const statelem = pageobj.getStatusElement(); const params = pageobj.getCallbackParameters(); let messageData = params.messageData; const [latest, history] = Twinkle.warn.callbacks.dateProcessing(text); const now = new Morebits.Date(pageobj.getLoadTime()); Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj, just in case if (params.main_group === 'autolevel') { // [template, level] const templateAndLevel = Twinkle.warn.callbacks.autolevelParseWikitext(text, params, latest, now, statelem); // Only if there's a change from the prior display/load if (params.sub_group !== templateAndLevel[0] && !confirm('Will issue a {{' + templateAndLevel[0] + '}} template to the user, okay?')) { statelem.error('aborted per user request'); return; } // Update params now that we've selected a warning params.sub_group = templateAndLevel[0]; messageData = params.messageData['level' + templateAndLevel[1]]; } else if (params.sub_group in history) { if (new Morebits.Date(history[params.sub_group]).add(1, 'day').isAfter(now)) { if (!confirm('An identical ' + params.sub_group + ' has been issued in the last 24 hours. \nWould you still like to add this warning/notice?')) { statelem.error('aborted per user request'); return; } } } latest.date.add(1, 'minute'); // after long debate, one minute is max if (latest.date.isAfter(now)) { if (!confirm('A ' + latest.type + ' has been issued in the last minute. \nWould you still like to add this warning/notice?')) { statelem.error('aborted per user request'); return; } } // build the edit summary // Function to handle generation of summary prefix for custom templates const customProcess = function(template) { template = template.split('|')[0]; let prefix; switch (template.slice(-1)) { case '1': prefix = 'General note'; break; case '2': prefix = 'Caution'; break; case '3': prefix = 'Warning'; break; case '4': prefix = 'Final warning'; break; case 'm': if (template.slice(-3) === '4im') { prefix = 'Only warning'; break; } // falls through default: prefix = 'Notice'; break; } return prefix + ': ' + Morebits.string.toUpperCaseFirstChar(messageData.label); }; let summary; if (params.main_group === 'custom') { summary = customProcess(params.sub_group); } else { // Normalize kitchensink to the 1-4im style if (params.main_group === 'kitchensink' && !/^D+$/.test(params.sub_group)) { let sub = params.sub_group.slice(-1); if (sub === 'm') { sub = params.sub_group.slice(-3); } // Don't overwrite uw-3rr, technically unnecessary if (/\d/.test(sub)) { params.main_group = 'level' + sub; } } // singlet || level1-4im, no need to /^\D+$/.test(params.main_group) summary = messageData.summary || (messageData[params.main_group] && messageData[params.main_group].summary); // Not in Twinkle.warn.messages, assume custom template if (!summary) { summary = customProcess(params.sub_group); } if (messageData.suppressArticleInSummary !== true && params.article) { if (params.sub_group === 'uw-agf-sock' || params.sub_group === 'uw-socksuspect' || params.sub_group === 'uw-aiv') { // these templates require a username summary += ' of [[:User:' + params.article + ']]'; } else { summary += ' on [[:' + params.article + ']]'; } } } pageobj.setEditSummary(summary + '.'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setWatchlist(Twinkle.getPref('watchWarnings')); // Get actual warning text const warningText = Twinkle.warn.callbacks.getWarningWikitext(params.sub_group, params.article, params.reason, params.main_group === 'custom'); let sectionExists = false, sectionNumber = 0; // Only check sections if there are sections or there's a chance we won't create our own if (!messageData.heading && text.length) { // Get all sections const sections = text.match(/^(==*).+\1/gm); if (sections && sections.length !== 0) { // Find the index of the section header in question const dateHeaderRegex = now.monthHeaderRegex(); sectionNumber = 0; // Find this month's section among L2 sections, preferring the bottom-most sectionExists = sections.reverse().some((sec, idx) => /^(==)[^=].+\1/m.test(sec) && dateHeaderRegex.test(sec) && typeof (sectionNumber = sections.length - 1 - idx) === 'number'); } } if (sectionExists) { // append to existing section pageobj.setPageSection(sectionNumber + 1); pageobj.setAppendText('\n\n' + warningText); pageobj.append(); } else { if (messageData.heading) { // create new section pageobj.setNewSectionTitle(messageData.heading); } else { Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found'); pageobj.setNewSectionTitle(now.monthHeader(0)); } pageobj.setNewSectionText(warningText); pageobj.newSection(); } } }; Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) { const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName'); // reason, main_group, sub_group, article const params = Morebits.QuickForm.getInputData(e.target); // Check that a reason was filled in if uw-username was selected if (params.sub_group === 'uw-username' && !params.article) { alert('You must supply a reason for the {{uw-username}} template.'); return; } // The autolevel option will already know by now if a user talk page // is a cross-namespace redirect (via !!Twinkle.warn.talkpageObj), so // technically we could alert an error here, but the user will have // already ignored the bold red error above. Moreover, they probably // *don't* want to actually issue a warning, so the error handling // after the form is submitted is probably preferable // Find the selected <option> element so we can fetch the data structure const $selectedEl = $(e.target.sub_group).find('option[value="' + $(e.target.sub_group).val() + '"]'); params.messageData = $selectedEl.data('messageData'); Morebits.SimpleWindow.setButtonsEnabled(false); Morebits.Status.init(e.target); Morebits.wiki.actionCompleted.redirect = userTalkPage; Morebits.wiki.actionCompleted.notice = 'Warning complete, reloading talk page in a few seconds'; const wikipedia_page = new Morebits.wiki.Page(userTalkPage, 'User talk page modification'); wikipedia_page.setCallbackParameters(params); wikipedia_page.setFollowRedirect(true, false); wikipedia_page.load(Twinkle.warn.callbacks.main); }; Twinkle.addInitCallback(Twinkle.warn, 'warn'); }()); // </nowiki> c7y1q740n2c7o108x7ltn2xgoyciji3 MediaWiki:Gadget-twinkleconfig.js 8 24482 268719 2026-04-27T16:53:08Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// <nowiki> (function() { /* **************************************** *** twinkleconfig.js: Preferences module **************************************** * Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences, and adds an ad box to the top of user subpages belonging to the currently logged-in user which end in '.js' * Active on: What I just said. Yeah. */ Twinkl...' 268719 javascript text/javascript // <nowiki> (function() { /* **************************************** *** twinkleconfig.js: Preferences module **************************************** * Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences, and adds an ad box to the top of user subpages belonging to the currently logged-in user which end in '.js' * Active on: What I just said. Yeah. */ Twinkle.config = {}; Twinkle.config.watchlistEnums = { yes: 'Add to watchlist (indefinitely)', no: "Don't add to watchlist", default: 'Follow your site preferences', '1 week': 'Watch for 1 week', '1 month': 'Watch for 1 month', '3 months': 'Watch for 3 months', '6 months': 'Watch for 6 months' }; Twinkle.config.commonSets = { csdCriteria: { db: 'Custom rationale ({{db}})', a1: 'A1', a2: 'A2', a3: 'A3', a7: 'A7', a9: 'A9', a10: 'A10', a11: 'A11', c1: 'C1', c4: 'C4', f1: 'F1', f2: 'F2', f3: 'F3', f7: 'F7', f8: 'F8', f9: 'F9', g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g7: 'G7', g8: 'G8', g10: 'G10', g11: 'G11', g12: 'G12', g13: 'G13', g14: 'G14', g15: 'G15', r2: 'R2', r3: 'R3', r4: 'R4', t5: 'T5', u1: 'U1', u2: 'U2', u6: 'U6', u7: 'U7' }, csdCriteriaNotification: { db: 'Custom rationale ({{db}})', a1: 'A1', a2: 'A2', a3: 'A3', a7: 'A7', a9: 'A9', a10: 'A10', a11: 'A11', c1: 'C1', f1: 'F1', f2: 'F2', f3: 'F3', f7: 'F7', f9: 'F9', g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5 ("general sanction violation" only)', g6: 'G6 ("copy-paste move" only)', g10: 'G10', g11: 'G11', g12: 'G12', g13: 'G13', g14: 'G14', g15: 'G15', r2: 'R2', r3: 'R3', r4: 'R4', u6: 'U6', u7: 'U7' }, csdAndImageDeletionCriteria: { db: 'Custom rationale ({{db}})', a1: 'A1', a2: 'A2', a3: 'A3', a7: 'A7', a9: 'A9', a10: 'A10', a11: 'A11', c1: 'C1', c4: 'C4', f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9', f11: 'F11', g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g7: 'G7', g8: 'G8', g10: 'G10', g11: 'G11', g12: 'G12', g13: 'G13', g14: 'G14', g15: 'G15', r2: 'R2', r3: 'R3', r4: 'R4', t5: 'T5', u1: 'U1', u2: 'U2', u6: 'U6', u7: 'U7' }, namespacesNoSpecial: { 0: 'Article', 1: 'Talk (article)', 2: 'User', 3: 'User talk', 4: 'Wikipedia', 5: 'Wikipedia talk', 6: 'File', 7: 'File talk', 8: 'MediaWiki', 9: 'MediaWiki talk', 10: 'Template', 11: 'Template talk', 12: 'Help', 13: 'Help talk', 14: 'Category', 15: 'Category talk', 100: 'Portal', 101: 'Portal talk', 118: 'Draft', 119: 'Draft talk', 710: 'TimedText', 711: 'TimedText talk', 828: 'Module', 829: 'Module talk' } }; Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys( Twinkle.config.commonSets.csdCriteria ); Twinkle.config.commonSets.csdCriteriaNotificationDisplayOrder = Object.keys( Twinkle.config.commonSets.csdCriteriaNotification ); Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys( Twinkle.config.commonSets.csdAndImageDeletionCriteria ); /** * Section entry format: * * { * title: <human-readable section title>, * module: <name of the associated module, used to link to sections>, * adminOnly: <true for admin-only sections>, * hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>, * preferences: [ * { * name: <TwinkleConfig property name>, * label: <human-readable short description - used as a form label>, * helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.> * adminOnly: <true for admin-only preferences>, * type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }), * enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>, * setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>, * setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>, * customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>, * customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor> * }, * . . . * ] * }, * . . . * */ Twinkle.config.sections = [ { title: 'General', module: 'general', preferences: [ // TwinkleConfig.userTalkPageMode may take arguments: // 'window': open a new window, remember the opened window // 'tab': opens in a new tab, if possible. // 'blank': force open in a new window, even if such a window exists { name: 'userTalkPageMode', label: 'When opening a user talk page, open it', type: 'enum', enumValues: { window: 'In a window, replacing other user talks', tab: 'In a new tab', blank: 'In a totally new window' } }, // TwinkleConfig.dialogLargeFont (boolean) { name: 'dialogLargeFont', label: 'Use larger text in Twinkle dialogs', type: 'boolean' }, // Twinkle.config.disabledModules (array) { name: 'disabledModules', label: 'Turn off the selected Twinkle modules', helptip: 'Anything you select here will NOT be available for use, so act with care. Uncheck to reactivate.', type: 'set', setValues: { arv: 'ARV', warn: 'Warn', welcome: 'Welcome', talkback: 'Talkback', speedy: 'CSD', prod: 'PROD', xfd: 'XfD', image: 'Image (DI)', protect: 'Protect (RPP)', tag: 'Tag', diff: 'Diff', unlink: 'Unlink', rollback: 'Revert and rollback' } }, // Twinkle.config.disabledSysopModules (array) { name: 'disabledSysopModules', label: 'Turn off the selected admin-only modules', helptip: 'Anything you select here will NOT be available for use, so act with care. Uncheck to reactivate.', adminOnly: true, type: 'set', setValues: { block: 'Block', deprod: 'DePROD', batchdelete: 'D-batch', batchprotect: 'P-batch', batchundelete: 'Und-batch' } } ] }, { title: 'ARV', module: 'arv', preferences: [ { name: 'spiWatchReport', label: 'Add sockpuppet report pages to watchlist', type: 'enum', enumValues: Twinkle.config.watchlistEnums } ] }, { title: 'Block user', module: 'block', adminOnly: true, preferences: [ // TwinkleConfig.defaultToBlock64 (boolean) // Whether to default to just blocking the /64 on or off { name: 'defaultToBlock64', label: 'For IPv6 addresses, select the option to block the /64 range by default', type: 'boolean' }, // TwinkleConfig.defaultToPartialBlocks (boolean) // Whether to default partial blocks on or off { name: 'defaultToPartialBlocks', label: 'Select partial blocks by default when opening the block menu', helptip: 'If the user is already blocked, this will be overridden in favor of defaulting to the current block type', type: 'boolean' }, // TwinkleConfig.blankTalkpageOnIndefBlock (boolean) // if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]]) { name: 'blankTalkpageOnIndefBlock', label: 'Blank the talk page when indefinitely blocking users', helptip: 'See <a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a> for more information.', type: 'boolean' } ] }, { title: 'Image deletion (DI)', module: 'image', preferences: [ // TwinkleConfig.notifyUserOnDeli (boolean) // If the user should be notified after placing a file deletion tag { name: 'notifyUserOnDeli', label: 'Check the "notify initial uploader" box by default', type: 'boolean' }, // TwinkleConfig.deliWatchPage (string) // The watchlist setting of the page tagged for deletion. { name: 'deliWatchPage', label: 'Add image page to watchlist when tagging', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.deliWatchUser (string) // The watchlist setting of the user talk page if a notification is placed. { name: 'deliWatchUser', label: 'Add user talk page of initial uploader to watchlist when notifying', type: 'enum', enumValues: Twinkle.config.watchlistEnums } ] }, { title: 'Page protection ' + (Morebits.userIsSysop ? '(PP)' : '(RPP)'), module: 'protect', preferences: [ { name: 'watchRequestedPages', label: 'Add page to watchlist when requesting protection', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'watchPPTaggedPages', label: 'Add page to watchlist when tagging with protection template', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'watchProtectedPages', label: 'Add page to watchlist when protecting', helptip: 'If also tagging the page after protection, that preference will be favored.', adminOnly: true, type: 'enum', enumValues: Twinkle.config.watchlistEnums } ] }, { title: 'Proposed deletion (PROD)', module: 'prod', preferences: [ // TwinkleConfig.watchProdPages (string) // Watchlist setting when applying prod template to page { name: 'watchProdPages', label: 'Add article to watchlist when tagging', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.markProdPagesAsPatrolled (boolean) // If, when applying prod template to page, to mark the page as curated/patrolled (if the page was reached from NewPages) { name: 'markProdPagesAsPatrolled', label: 'Mark page as patrolled/reviewed when tagging (if possible)', helptip: 'This should probably not be checked as doing so is against best practice consensus', type: 'boolean' }, // TwinkleConfig.prodReasonDefault (string) // The prefilled PROD reason. { name: 'prodReasonDefault', label: 'Prefilled PROD reason', type: 'string' }, { name: 'logProdPages', label: 'Keep a log in userspace of all pages you tag for PROD', helptip: 'Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you tag for PROD using Twinkle.', type: 'boolean' }, { name: 'prodLogPageName', label: 'Keep the PROD userspace log at this user subpage', helptip: 'Enter a subpage name in this box. You will find your PROD log at User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the PROD userspace log.', type: 'string' } ] }, { title: 'Revert and rollback', module: 'rollback', preferences: [ // TwinkleConfig.autoMenuAfterRollback (bool) // Option to automatically open the warning menu if the user talk page is opened post-reversion { name: 'autoMenuAfterRollback', label: 'Automatically open the Twinkle warn menu on a user talk page after Twinkle rollback', helptip: 'Only operates if the relevant box is checked below.', type: 'boolean' }, // TwinkleConfig.openTalkPage (array) // What types of actions that should result in opening of talk page { name: 'openTalkPage', label: 'Open user talk page after these types of reversions', type: 'set', setValues: { agf: 'AGF rollback', norm: 'Normal rollback', vand: 'Vandalism rollback' } }, // TwinkleConfig.openTalkPageOnAutoRevert (bool) // Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened. { name: 'openTalkPageOnAutoRevert', label: 'Open user talk page when invoking rollback from user contributions or recent changes', helptip: 'When this is on, the desired options must be enabled in the previous setting for this to work.', type: 'boolean' }, // TwinkleConfig.rollbackInPlace (bool) // { name: 'rollbackInPlace', label: "Don't reload the page when rolling back from contributions or recent changes", helptip: "When this is on, Twinkle won't reload the contributions or recent changes feed after reverting, allowing you to revert more than one edit at a time.", type: 'boolean' }, // TwinkleConfig.markRevertedPagesAsMinor (array) // What types of actions that should result in marking edit as minor { name: 'markRevertedPagesAsMinor', label: 'Mark as minor edit for these types of reversions', type: 'set', setValues: { agf: 'AGF rollback', norm: 'Normal rollback', vand: 'Vandalism rollback', torev: '"Restore this version"' } }, // TwinkleConfig.watchRevertedPages (array) // What types of actions that should result in forced addition to watchlist { name: 'watchRevertedPages', label: 'Add pages to watchlist for these types of reversions', type: 'set', setValues: { agf: 'AGF rollback', norm: 'Normal rollback', vand: 'Vandalism rollback', torev: '"Restore this version"' } }, // TwinkleConfig.watchRevertedExpiry // If any of the above items are selected, whether to expire the watch { name: 'watchRevertedExpiry', label: 'When reverting a page, how long to watch it for', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.offerReasonOnNormalRevert (boolean) // If to offer a prompt for extra summary reason for normal reverts, default to true { name: 'offerReasonOnNormalRevert', label: 'Prompt for reason for normal rollbacks', helptip: '"Normal" rollbacks are the ones that are invoked from the middle [rollback] link.', type: 'boolean' }, { name: 'confirmOnRollback', label: 'Require confirmation before reverting (all devices)', helptip: 'For users of pen or touch devices, and chronically indecisive people.', type: 'boolean' }, { name: 'confirmOnMobileRollback', label: 'Require confirmation before reverting (mobile devices only)', helptip: 'Avoid accidental reversions when on mobile devices.', type: 'boolean' }, // TwinkleConfig.showRollbackLinks (array) // Where Twinkle should show rollback links: // diff, others, mine, contribs, history, recent // Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres { name: 'showRollbackLinks', label: 'Show rollback links on these pages', type: 'set', setValues: { diff: 'Diff pages', others: 'Contributions pages of other users', mine: 'My contributions page', recent: 'Recent changes and related changes special pages', history: 'History pages' } } ] }, { title: 'Speedy deletion (CSD)', module: 'speedy', preferences: [ { name: 'speedySelectionStyle', label: 'When to go ahead and tag/delete the page', type: 'enum', enumValues: { buttonClick: 'When I click "Submit"', radioClick: 'As soon as I click an option' } }, // TwinkleConfig.watchSpeedyPages (array) // Whether to add speedy tagged or deleted pages to watchlist { name: 'watchSpeedyPages', label: 'Add page to watchlist when using these criteria', type: 'set', setValues: Twinkle.config.commonSets.csdCriteria, setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder }, // TwinkleConfig.watchSpeedyExpiry // If any of the above items are selected, whether to expire the watch { name: 'watchSpeedyExpiry', label: 'When tagging a page, how long to watch it for', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.markSpeedyPagesAsPatrolled (boolean) // If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages) { name: 'markSpeedyPagesAsPatrolled', label: 'Mark page as patrolled/reviewed when tagging (if possible)', helptip: 'This should probably not be checked as doing so is against best practice consensus', type: 'boolean' }, // TwinkleConfig.watchSpeedyUser (string) // The watchlist setting of the user talk page if they receive a notification. { name: 'watchSpeedyUser', label: 'Add user talk page of initial contributor to watchlist (when notifying)', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.welcomeUserOnSpeedyDeletionNotification (array of strings) // On what types of speedy deletion notifications shall the user be welcomed // with a "firstarticle" notice if their talk page has not yet been created. { name: 'welcomeUserOnSpeedyDeletionNotification', label: 'Welcome page creator when notifying with these criteria', helptip: 'The welcome is issued only if the user is notified about the deletion, and only if their talk page does not already exist. The template used is {{firstarticle}}.', type: 'set', setValues: Twinkle.config.commonSets.csdCriteriaNotification, setDisplayOrder: Twinkle.config.commonSets.csdCriteriaNotificationDisplayOrder }, // TwinkleConfig.notifyUserOnSpeedyDeletionNomination (array) // What types of actions should result in the author of the page being notified of nomination { name: 'notifyUserOnSpeedyDeletionNomination', label: 'Notify page creator when tagging with these criteria', helptip: 'Even if you choose to notify from the CSD screen, the notification will only take place for those criteria selected here.', type: 'set', setValues: Twinkle.config.commonSets.csdCriteriaNotification, setDisplayOrder: Twinkle.config.commonSets.csdCriteriaNotificationDisplayOrder }, // TwinkleConfig.warnUserOnSpeedyDelete (array) // What types of actions should result in the author of the page being notified of speedy deletion (admin only) { name: 'warnUserOnSpeedyDelete', label: 'Notify page creator when deleting under these criteria', helptip: 'Even if you choose to notify from the CSD screen, the notification will only take place for those criteria selected here.', adminOnly: true, type: 'set', setValues: Twinkle.config.commonSets.csdCriteriaNotification, setDisplayOrder: Twinkle.config.commonSets.csdCriteriaNotificationDisplayOrder }, // TwinkleConfig.promptForSpeedyDeletionSummary (array of strings) { name: 'promptForSpeedyDeletionSummary', label: 'Allow editing of deletion summary when deleting under these criteria', adminOnly: true, type: 'set', setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria, setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder }, // TwinkleConfig.deleteTalkPageOnDelete (boolean) // If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only) { name: 'deleteTalkPageOnDelete', label: 'Check the "also delete talk page" box by default', adminOnly: true, type: 'boolean' }, { name: 'deleteRedirectsOnDelete', label: 'Check the "also delete redirects" box by default', adminOnly: true, type: 'boolean' }, // TwinkleConfig.deleteSysopDefaultToDelete (boolean) // Make the CSD screen default to "delete" instead of "tag" (admin only) { name: 'deleteSysopDefaultToDelete', label: 'Default to outright deletion instead of speedy tagging', helptip: 'If there is a CSD tag already present, Twinkle will always default to "delete" mode', adminOnly: true, type: 'boolean' }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: 'speedyWindowWidth', label: 'Width of speedy deletion window (pixels)', type: 'integer' }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: 'speedyWindowHeight', label: 'Height of speedy deletion window (pixels)', helptip: 'If you have a big monitor, you might like to increase this.', type: 'integer' }, { name: 'logSpeedyNominations', label: 'Keep a log in userspace of all CSD nominations', helptip: 'Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you nominate for CSD using Twinkle. Files tagged using DI are also added to this log.', type: 'boolean' }, { name: 'speedyLogPageName', label: 'Keep the CSD userspace log at this user subpage', helptip: 'Enter a subpage name in this box. You will find your CSD log at User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the CSD userspace log.', type: 'string' }, { name: 'noLogOnSpeedyNomination', label: 'Do not create a userspace log entry when tagging with these criteria', type: 'set', setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria, setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder } ] }, { title: 'Tag', module: 'tag', preferences: [ { name: 'watchTaggedVenues', label: 'Add page to watchlist when tagging these type of pages', type: 'set', setValues: { articles: 'Articles', drafts: 'Drafts', redirects: 'Redirects', files: 'Files' } }, { name: 'watchTaggedPages', label: 'When tagging a page, how long to watch it for', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'watchMergeDiscussions', label: 'Add talk pages to watchlist when starting merge discussions', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'markTaggedPagesAsMinor', label: 'Mark addition of tags as a minor edit', type: 'boolean' }, { name: 'markTaggedPagesAsPatrolled', label: 'Check the "mark page as patrolled/reviewed" box by default', type: 'boolean' }, { name: 'groupByDefault', label: 'Check the "group into {{multiple issues}}" box by default', type: 'boolean' }, { name: 'tagArticleSortOrder', label: 'Default view order for article tags', type: 'enum', enumValues: { cat: 'By categories', alpha: 'In alphabetical order' } }, { name: 'customTagList', label: 'Custom article/draft maintenance tags to display', helptip: "These appear as additional options at the bottom of the list of tags. For example, you could add new maintenance tags which have not yet been added to Twinkle's defaults.", type: 'customList', customListValueTitle: 'Template name (no curly brackets)', customListLabelTitle: 'Text to show in Tag dialog' }, { name: 'customFileTagList', label: 'Custom file maintenance tags to display', helptip: 'Additional tags that you wish to add for files.', type: 'customList', customListValueTitle: 'Template name (no curly brackets)', customListLabelTitle: 'Text to show in Tag dialog' }, { name: 'customRedirectTagList', label: 'Custom redirect category tags to display', helptip: 'Additional tags that you wish to add for redirects.', type: 'customList', customListValueTitle: 'Template name (no curly brackets)', customListLabelTitle: 'Text to show in Tag dialog' } ] }, { title: 'Talkback', module: 'talkback', preferences: [ { name: 'markTalkbackAsMinor', label: 'Mark talkbacks as minor edits', type: 'boolean' }, { name: 'insertTalkbackSignature', label: 'Insert signature within talkbacks', type: 'boolean' }, { name: 'talkbackHeading', label: 'Section heading to use for talkback and please see', tooltip: 'Should NOT include the equals signs ("==") used for wikitext formatting', type: 'string' }, { name: 'mailHeading', label: "Section heading to use for \"you've got mail\" notices", tooltip: 'Should NOT include the equals signs ("==") used for wikitext formatting', type: 'string' } ] }, { title: 'Unlink', module: 'unlink', preferences: [ // TwinkleConfig.unlinkNamespaces (array) // In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft) { name: 'unlinkNamespaces', label: 'Remove links from pages in these namespaces', helptip: 'Avoid selecting any talk namespaces, as Twinkle might end up unlinking on talk archives (a big no-no).', type: 'set', setValues: Twinkle.config.commonSets.namespacesNoSpecial } ] }, { title: 'Warn user', module: 'warn', preferences: [ // TwinkleConfig.defaultWarningGroup (int) // Which level warning should be the default selected group, default is 1 { name: 'defaultWarningGroup', label: 'Default warning level', type: 'enum', enumValues: { 1: 'Level 1', 2: 'Level 2', 3: 'Level 3', 4: 'Level 4', 5: 'Level 4im', 6: 'Single-issue notices', 7: 'Single-issue warnings', // 8 was used for block templates before #260 9: 'Custom warnings', 10: 'All warning templates', 11: 'Auto-select level (1-4)' } }, // TwinkleConfig.combinedSingletMenus (boolean) // if true, show one menu with both single-issue notices and warnings instead of two separately { name: 'combinedSingletMenus', label: 'Replace the two separate single-issue menus into one combined menu', helptip: 'Selecting either single-issue notices or single-issue warnings as your default will make this your default if enabled.', type: 'boolean' }, // TwinkleConfig.watchWarnings (string) // Watchlist setting for the page which has been dispatched an warning or notice { name: 'watchWarnings', label: 'Add user talk page to watchlist when notifying', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.oldSelect (boolean) // if true, use the native select menu rather the select2-based one { name: 'oldSelect', label: 'Use the non-searchable classic select menu', type: 'boolean' }, { name: 'customWarningList', label: 'Custom warning templates to display', helptip: 'You can add individual templates or user subpages. Custom warnings appear in the "Custom warnings" category within the warning dialog box.', type: 'customList', customListValueTitle: 'Template name (no curly brackets)', customListLabelTitle: 'Text to show in warning list (also used as edit summary)' } ] }, { title: 'Welcome user', module: 'welcome', preferences: [ { name: 'topWelcomes', label: 'Place welcomes above existing content on user talk pages', type: 'boolean' }, { name: 'watchWelcomes', label: 'Add user talk pages to watchlist when welcoming', helptip: 'Doing so adds to the personal element of welcoming a user - you will be able to see how they are coping as a newbie, and possibly help them.', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'insertUsername', label: 'Add your username to the template (where applicable)', helptip: "Some welcome templates have an opening sentence like \"Hi, I'm &lt;username&gt;. Welcome\" etc. If you turn off this option, these templates will not display your username in that way.", type: 'boolean' }, { name: 'quickWelcomeMode', label: 'Clicking the "welcome" link on a diff page (which only appears if the editor\'s user talk page has not been created yet) will', helptip: 'If you choose to welcome automatically, the template you specify below will be used.', type: 'enum', enumValues: { auto: 'immediately post the welcome template specified below', norm: 'prompt you to select a template' } }, { name: 'quickWelcomeTemplate', label: 'Template to use when welcoming automatically', helptip: 'Enter the name of a welcome template, without the curly brackets. A link to the given article will be added.', type: 'string' }, { name: 'customWelcomeList', label: 'Custom welcome templates to display', helptip: "You can add other welcome templates, or user subpages that are welcome templates (prefixed with \"User:\"). Don't forget that these templates are substituted onto user talk pages.", type: 'customList', customListValueTitle: 'Template name (no curly brackets)', customListLabelTitle: 'Text to show in Welcome dialog' }, { name: 'customWelcomeSignature', label: 'Automatically sign custom welcome templates', helptip: 'If your custom welcome templates contain a built-in signature within the template, turn off this option.', type: 'boolean' } ] }, { title: 'XFD (deletion discussions)', module: 'xfd', preferences: [ { name: 'logXfdNominations', label: 'Keep a log in userspace of all pages you nominate for a deletion discussion (XfD)', helptip: 'The userspace log offers a good way to keep track of all pages you nominate for XfD using Twinkle.', type: 'boolean' }, { name: 'xfdLogPageName', label: 'Keep the deletion discussion userspace log at this user subpage', helptip: 'Enter a subpage name in this box. You will find your XfD log at User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the XfD userspace log.', type: 'string' }, { name: 'noLogOnXfdNomination', label: 'Do not create a userspace log entry when nominating at this venue', type: 'set', setValues: { afd: 'AfD', tfd: 'TfD', ffd: 'FfD', cfd: 'CfD', cfds: 'CfD/S', mfd: 'MfD', rfd: 'RfD', rm: 'RM' } }, // TwinkleConfig.xfdWatchPage (string) // The watchlist setting of the page being nominated for XfD. { name: 'xfdWatchPage', label: 'Add the nominated page to watchlist', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.xfdWatchDiscussion (string) // The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination), // or the list page for the other processes. { name: 'xfdWatchDiscussion', label: 'Add the deletion discussion page to watchlist', helptip: 'This refers to the discussion subpage (for AfD and MfD) or the daily log page (for TfD, CfD, RfD and FfD)', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.xfdWatchList (string) // The watchlist setting of the XfD list page, *if* the discussion is on a separate page. { name: 'xfdWatchList', label: 'Add the daily log/list page to the watchlist (AfD and MfD)', helptip: 'This only applies for AfD and MfD, where the discussions are transcluded onto a daily log page (for AfD) or the main MfD page (for MfD).', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.xfdWatchUser (string) // The watchlist setting of the user talk page if they receive a notification. { name: 'xfdWatchUser', label: 'Add user talk page of initial contributor to watchlist (when notifying)', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, // TwinkleConfig.xfdWatchRelated (string) // The watchlist setting of the target of a redirect being nominated for RfD. { name: 'xfdWatchRelated', label: "Add the redirect's target page to watchlist (when notifying)", helptip: 'This only applies for RfD, when leaving a notification on the talk page of the target of the redirect', type: 'enum', enumValues: Twinkle.config.watchlistEnums }, { name: 'markXfdPagesAsPatrolled', label: 'Mark page as patrolled/reviewed when nominating for AFD (if possible)', type: 'boolean' } ] }, { title: 'Hidden', hidden: true, preferences: [ // twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50 { name: 'revertMaxRevisions', type: 'integer' }, // twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale // Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two: // https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62 { name: 'autolevelStaleDays', type: 'integer' }, // How many pages should be queried by deprod and batchdelete/protect/undelete { name: 'batchMax', type: 'integer', adminOnly: true }, // How many pages should be processed at a time by deprod and batchdelete/protect/undelete { name: 'batchChunks', type: 'integer', adminOnly: true } ] } ]; // end of Twinkle.config.sections Twinkle.config.init = function twinkleconfigInit() { // create the config page at Wikipedia:Twinkle/Preferences if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').project && mw.config.get('wgTitle') === 'Twinkle/Preferences') && mw.config.get('wgAction') === 'view') { if (!document.getElementById('twinkle-config')) { return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless } // set style to nothing to prevent conflict with external css document.getElementById('twinkle-config').removeAttribute('style'); document.getElementById('twinkle-config-titlebar').removeAttribute('style'); const contentdiv = document.getElementById('twinkle-config-content'); contentdiv.textContent = ''; // clear children // let user know about possible conflict with skin js/common.js file // (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings) if (window.TwinkleConfig || window.FriendlyConfig) { const contentnotice = document.createElement('div'); contentnotice.className = 'plainlinks twinkle-ombox'; contentnotice.innerHTML = '<div>' + '<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' + '</div>' + '<div>' + '<p><big><b>Before modifying your settings here,</b> you must remove your old Twinkle and Friendly settings from your personal skin JavaScript.</big></p>' + '<p>To do this, you can <a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') + '.js', { action: 'edit' }) + '" target="_blank"><b>edit your personal skin javascript file</b></a> or <a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>your common.js file</b></a>, removing all lines of code that refer to <code>TwinkleConfig</code> and <code>FriendlyConfig</code>.</p>' + '</div>'; contentdiv.appendChild(contentnotice); } // start a table of contents const toctable = document.createElement('div'); toctable.className = 'toc'; toctable.style.marginLeft = '0.4em'; // create TOC title const toctitle = document.createElement('div'); toctitle.id = 'toctitle'; const toch2 = document.createElement('h2'); toch2.textContent = 'Contents '; toctitle.appendChild(toch2); // add TOC show/hide link const toctoggle = document.createElement('span'); toctoggle.className = 'toctoggle'; toctoggle.appendChild(document.createTextNode('[')); const toctogglelink = document.createElement('a'); toctogglelink.className = 'internal'; toctogglelink.setAttribute('href', '#tw-tocshowhide'); toctogglelink.textContent = 'hide'; toctoggle.appendChild(toctogglelink); toctoggle.appendChild(document.createTextNode(']')); toctitle.appendChild(toctoggle); toctable.appendChild(toctitle); // create item container: this is what we add stuff to const tocul = document.createElement('ul'); toctogglelink.addEventListener('click', () => { const $tocul = $(tocul); $tocul.toggle(); if ($tocul.find(':visible').length) { toctogglelink.textContent = 'hide'; } else { toctogglelink.textContent = 'show'; } }, false); toctable.appendChild(tocul); contentdiv.appendChild(toctable); const contentform = document.createElement('form'); contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue contentform.addEventListener('submit', Twinkle.config.save, true); contentdiv.appendChild(contentform); const container = document.createElement('table'); container.style.width = '100%'; contentform.appendChild(container); $(Twinkle.config.sections).each((sectionkey, section) => { if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) { return true; // i.e. "continue" in this context } // add to TOC const tocli = document.createElement('li'); tocli.className = 'toclevel-1'; const toca = document.createElement('a'); toca.setAttribute('href', '#' + section.module); toca.appendChild(document.createTextNode(section.title)); tocli.appendChild(toca); tocul.appendChild(tocli); let row = document.createElement('tr'); let cell = document.createElement('td'); cell.setAttribute('colspan', '3'); const heading = document.createElement('h4'); heading.style.borderBottom = '1px solid gray'; heading.style.marginTop = '0.2em'; heading.id = section.module; heading.appendChild(document.createTextNode(section.title)); cell.appendChild(heading); row.appendChild(cell); container.appendChild(row); let rowcount = 1; // for row banding // add each of the preferences to the form $(section.preferences).each((prefkey, pref) => { if (pref.adminOnly && !Morebits.userIsSysop) { return true; // i.e. "continue" in this context } row = document.createElement('tr'); row.style.marginBottom = '0.2em'; // create odd row banding if (rowcount++ % 2 === 0) { row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)'; } cell = document.createElement('td'); let label, input; const gotPref = Twinkle.getPref(pref.name); switch (pref.type) { case 'boolean': // create a checkbox cell.setAttribute('colspan', '2'); label = document.createElement('label'); input = document.createElement('input'); input.setAttribute('type', 'checkbox'); input.setAttribute('id', pref.name); input.setAttribute('name', pref.name); if (gotPref === true) { input.setAttribute('checked', 'checked'); } label.appendChild(input); label.appendChild(document.createTextNode(pref.label)); cell.appendChild(label); break; case 'string': // create an input box case 'integer': // add label to first column cell.style.textAlign = 'right'; cell.style.paddingRight = '0.5em'; label = document.createElement('label'); label.setAttribute('for', pref.name); label.appendChild(document.createTextNode(pref.label + ':')); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement('td'); cell.style.paddingRight = '1em'; input = document.createElement('input'); input.setAttribute('type', 'text'); input.setAttribute('id', pref.name); input.setAttribute('name', pref.name); if (pref.type === 'integer') { input.setAttribute('size', 6); input.setAttribute('type', 'number'); input.setAttribute('step', '1'); // integers only } if (gotPref) { input.setAttribute('value', gotPref); } cell.appendChild(input); break; case 'enum': // create a combo box // add label to first column // note: duplicates the code above, under string/integer cell.style.textAlign = 'right'; cell.style.paddingRight = '0.5em'; label = document.createElement('label'); label.setAttribute('for', pref.name); label.appendChild(document.createTextNode(pref.label + ':')); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement('td'); cell.style.paddingRight = '1em'; input = document.createElement('select'); input.setAttribute('id', pref.name); input.setAttribute('name', pref.name); $.each(pref.enumValues, (enumvalue, enumdisplay) => { const option = document.createElement('option'); option.setAttribute('value', enumvalue); if ((gotPref === enumvalue) || // Hack to convert old boolean watchlist prefs // to corresponding enums (added in v2.1) (typeof gotPref === 'boolean' && ((gotPref && enumvalue === 'yes') || (!gotPref && enumvalue === 'no')))) { option.setAttribute('selected', 'selected'); } option.appendChild(document.createTextNode(enumdisplay)); input.appendChild(option); }); cell.appendChild(input); break; case 'set': // create a set of check boxes // add label first of all cell.setAttribute('colspan', '2'); label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling label.appendChild(document.createTextNode(pref.label + ':')); cell.appendChild(label); var checkdiv = document.createElement('div'); checkdiv.style.paddingLeft = '1em'; var worker = function(itemkey, itemvalue) { const checklabel = document.createElement('label'); checklabel.style.marginRight = '0.7em'; checklabel.style.display = 'inline-block'; const check = document.createElement('input'); check.setAttribute('type', 'checkbox'); check.setAttribute('id', pref.name + '_' + itemkey); check.setAttribute('name', pref.name + '_' + itemkey); if (gotPref && gotPref.includes(itemkey)) { check.setAttribute('checked', 'checked'); } // cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...) if (pref.name === 'unlinkNamespaces') { if (gotPref && gotPref.includes(parseInt(itemkey, 10))) { check.setAttribute('checked', 'checked'); } } checklabel.appendChild(check); checklabel.appendChild(document.createTextNode(itemvalue)); checkdiv.appendChild(checklabel); }; if (pref.setDisplayOrder) { // add check boxes according to the given display order $.each(pref.setDisplayOrder, (itemkey, item) => { worker(item, pref.setValues[item]); }); } else { // add check boxes according to the order it gets fed to us (probably strict alphabetical) $.each(pref.setValues, worker); } cell.appendChild(checkdiv); break; case 'customList': // add label to first column cell.style.textAlign = 'right'; cell.style.paddingRight = '0.5em'; label = document.createElement('label'); label.setAttribute('for', pref.name); label.appendChild(document.createTextNode(pref.label + ':')); cell.appendChild(label); row.appendChild(cell); // add button to second column cell = document.createElement('td'); cell.style.paddingRight = '1em'; var button = document.createElement('button'); button.setAttribute('id', pref.name); button.setAttribute('name', pref.name); button.setAttribute('type', 'button'); button.addEventListener('click', Twinkle.config.listDialog.display, false); // use jQuery data on the button to store the current config value $(button).data({ value: gotPref, pref: pref }); button.appendChild(document.createTextNode('Edit items')); cell.appendChild(button); break; default: alert('twinkleconfig: unknown data type for preference ' + pref.name); break; } row.appendChild(cell); // add help tip cell = document.createElement('td'); cell.className = 'twinkle-config-helptip'; if (pref.helptip) { // convert mentions of templates in the helptip to clickable links cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g, '{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}'); } // add reset link (custom lists don't need this, as their config value isn't displayed on the form) if (pref.type !== 'customList') { const resetlink = document.createElement('a'); resetlink.setAttribute('href', '#tw-reset'); resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name); resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false); resetlink.style.cssFloat = 'right'; resetlink.style.margin = '0 0.6em'; resetlink.appendChild(document.createTextNode('Reset')); cell.appendChild(resetlink); } row.appendChild(cell); container.appendChild(row); return true; }); return true; }); const footerbox = document.createElement('div'); footerbox.setAttribute('id', 'twinkle-config-buttonpane'); const button = document.createElement('button'); button.setAttribute('id', 'twinkle-config-submit'); button.setAttribute('type', 'submit'); button.appendChild(document.createTextNode('Save changes')); footerbox.appendChild(button); const footerspan = document.createElement('span'); footerspan.className = 'plainlinks'; footerspan.style.marginLeft = '2.4em'; footerspan.style.fontSize = '90%'; const footera = document.createElement('a'); footera.setAttribute('href', '#tw-reset-all'); footera.setAttribute('id', 'twinkle-config-resetall'); footera.addEventListener('click', Twinkle.config.resetAllPrefs, false); footera.appendChild(document.createTextNode('Restore defaults')); footerspan.appendChild(footera); footerbox.appendChild(footerspan); contentform.appendChild(footerbox); // since all the section headers exist now, we can try going to the requested anchor if (window.location.hash) { const loc = window.location.hash; window.location.hash = ''; window.location.hash = loc; } } else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 && mw.config.get('wgPageName').slice(-3) === '.js') { const box = document.createElement('div'); // Styled in twinkle.css box.setAttribute('id', 'twinkle-config-headerbox'); let link; const scriptPageName = mw.config.get('wgPageName').slice( mw.config.get('wgPageName').lastIndexOf('/') + 1, mw.config.get('wgPageName').lastIndexOf('.js') ); if (scriptPageName === 'twinkleoptions') { // place "why not try the preference panel" notice box.setAttribute('class', 'config-twopt-box'); if (mw.config.get('wgArticleId') > 0) { // page exists box.appendChild(document.createTextNode('This page contains your Twinkle preferences. You can change them using the ')); } else { // page does not exist box.appendChild(document.createTextNode('You can customize Twinkle to suit your preferences by using the ')); } link = document.createElement('a'); link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').project] + ':Twinkle/Preferences')); link.appendChild(document.createTextNode('Twinkle preferences panel')); box.appendChild(link); box.appendChild(document.createTextNode(', or by editing this page.')); $(box).insertAfter($('#contentSub')); } else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) { // place "Looking for Twinkle options?" notice box.setAttribute('class', 'config-userskin-box'); box.appendChild(document.createTextNode('If you want to set Twinkle preferences, you can use the ')); link = document.createElement('a'); link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').project] + ':Twinkle/Preferences')); link.appendChild(document.createTextNode('Twinkle preferences panel')); box.appendChild(link); box.appendChild(document.createTextNode('.')); $(box).insertAfter($('#contentSub')); } } }; // custom list-related stuff Twinkle.config.listDialog = {}; Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) { let $contenttr, $valueInput, $labelInput; $dlgtable.append( $contenttr = $('<tr>').append( $('<td>').append( $('<button>') .attr('type', 'button') .on('click', () => { $contenttr.remove(); }) .text('Remove') ), $('<td>').append( $valueInput = $('<input>') .attr('type', 'text') .addClass('twinkle-config-customlist-value') .css('width', '97%') ), $('<td>').append( $labelInput = $('<input>') .attr('type', 'text') .addClass('twinkle-config-customlist-label') .css('width', '98%') ) ) ); if (value) { $valueInput.val(value); } if (label) { $labelInput.val(label); } }; Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) { const $prefbutton = $(e.target); const curvalue = $prefbutton.data('value'); const curpref = $prefbutton.data('pref'); const dialog = new Morebits.SimpleWindow(720, 400); dialog.setTitle(curpref.label); dialog.setScriptName('Twinkle preferences'); let $dlgtbody; dialog.setContent( $('<div>').append( $('<table>') .addClass('wikitable') .css({ margin: '1.4em 1em', width: 'auto' }) .append( $dlgtbody = $('<tbody>').append( // header row $('<tr>').append( $('<th>') // top-left cell .css('width', '5%'), $('<th>') // value column header .css('width', '35%') .text(curpref.customListValueTitle ? curpref.customListValueTitle : 'Value'), $('<th>') // label column header .css('width', '60%') .text(curpref.customListLabelTitle ? curpref.customListLabelTitle : 'Label') ) ), $('<tfoot>').append( $('<tr>').append( $('<td>') .attr('colspan', '3') .append( $('<button>') .text('Add') .css('min-width', '8em') .attr('type', 'button') .on('click', () => { Twinkle.config.listDialog.addRow($dlgtbody); }) ) ) ) ), $('<button>') .text('Save changes') .attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane .on('click', () => { Twinkle.config.listDialog.save($prefbutton, $dlgtbody); dialog.close(); }), $('<button>') .text('Reset') .attr('type', 'submit') .on('click', () => { Twinkle.config.listDialog.reset($prefbutton, $dlgtbody); }), $('<button>') .text('Cancel') .attr('type', 'submit') .on('click', () => { dialog.close(); }) )[0] ); // content rows let gotRow = false; $.each(curvalue, (k, v) => { gotRow = true; Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label); }); // if there are no values present, add a blank row to start the user off if (!gotRow) { Twinkle.config.listDialog.addRow($dlgtbody); } dialog.display(); }; // Resets the data value, re-populates based on the new (default) value, then saves the // old data value again (less surprising behaviour) Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) { // reset value on button const curpref = $button.data('pref'); const oldvalue = $button.data('value'); Twinkle.config.resetPref(curpref); // reset form $tbody.find('tr').slice(1).remove(); // all rows except the first (header) row // add the new values const curvalue = $button.data('value'); $.each(curvalue, (k, v) => { Twinkle.config.listDialog.addRow($tbody, v.value, v.label); }); // save the old value $button.data('value', oldvalue); }; Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) { const result = []; let current = {}; $tbody.find('input[type="text"]').each((inputkey, input) => { if ($(input).hasClass('twinkle-config-customlist-value')) { current = { value: input.value }; } else { current.label = input.value; // exclude totally empty rows if (current.value || current.label) { result.push(current); } } }); $button.data('value', result); }; // reset/restore defaults Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) { const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped // search tactics $(Twinkle.config.sections).each((sectionkey, section) => { if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) { return true; // continue: skip impossibilities } let foundit = false; $(section.preferences).each((prefkey, pref) => { if (pref.name !== wantedpref) { return true; // continue } Twinkle.config.resetPref(pref); foundit = true; return false; // break }); if (foundit) { return false; // break } }); return false; // stop link from scrolling page }; Twinkle.config.resetPref = function twinkleconfigResetPref(pref) { switch (pref.type) { case 'boolean': document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name]; break; case 'string': case 'integer': case 'enum': document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name]; break; case 'set': $.each(pref.setValues, (itemkey) => { if (document.getElementById(pref.name + '_' + itemkey)) { document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey); } }); break; case 'customList': $(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]); break; default: alert('twinkleconfig: unknown data type for preference ' + pref.name); break; } }; Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() { // no confirmation message - the user can just refresh/close the page to abort $(Twinkle.config.sections).each((sectionkey, section) => { if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) { return true; // continue: skip impossibilities } $(section.preferences).each((prefkey, pref) => { if (!pref.adminOnly || Morebits.userIsSysop) { Twinkle.config.resetPref(pref); } }); return true; }); return false; // stop link from scrolling page }; Twinkle.config.save = function twinkleconfigSave(e) { Morebits.Status.init(document.getElementById('twinkle-config-content')); const userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js'; const wikipediaPage = new Morebits.wiki.Page(userjs, 'Saving preferences to ' + userjs); wikipediaPage.setCallbackParameters(e.target); wikipediaPage.load(Twinkle.config.writePrefs); return false; }; Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) { const form = pageobj.getCallbackParameters(); // this is the object which gets serialized into JSON; only // preferences that this script knows about are kept const newConfig = {optionsVersion: 2.1}; // a comparison function is needed later on // it is just enough for our purposes (i.e. comparing strings, numbers, booleans, // arrays of strings, and arrays of { value, label }) // and it is not very robust: e.g. compare([2], ["2"]) === true, and // compare({}, {}) === false, but it's good enough for our purposes here const compare = function(a, b) { if (Array.isArray(a)) { if (a.length !== b.length) { return false; } const asort = a.sort(), bsort = b.sort(); for (let i = 0; asort[i]; ++i) { // comparison of the two properties of custom lists if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label || asort[i].value !== bsort[i].value)) { return false; } else if (asort[i].toString() !== bsort[i].toString()) { return false; } } return true; } return a === b; }; $(Twinkle.config.sections).each((sectionkey, section) => { if (section.adminOnly && !Morebits.userIsSysop) { return; // i.e. "continue" in this context } // reach each of the preferences from the form $(section.preferences).each((prefkey, pref) => { let userValue; // = undefined // only read form values for those prefs that have them if (!pref.adminOnly || Morebits.userIsSysop) { if (!section.hidden) { switch (pref.type) { case 'boolean': // read from the checkbox userValue = form[pref.name].checked; break; case 'string': // read from the input box or combo box case 'enum': userValue = form[pref.name].value; break; case 'integer': // read from the input box userValue = parseInt(form[pref.name].value, 10); if (isNaN(userValue)) { Morebits.Status.warn('Saving', 'The value you specified for ' + pref.name + ' (' + pref.value + ') was invalid. The save will continue, but the invalid data value will be skipped.'); userValue = null; } break; case 'set': // read from the set of check boxes userValue = []; if (pref.setDisplayOrder) { // read only those keys specified in the display order $.each(pref.setDisplayOrder, (itemkey, item) => { if (form[pref.name + '_' + item].checked) { userValue.push(item); } }); } else { // read all the keys in the list of values $.each(pref.setValues, (itemkey) => { if (form[pref.name + '_' + itemkey].checked) { userValue.push(itemkey); } }); } break; case 'customList': // read from the jQuery data stored on the button object userValue = $(form[pref.name]).data('value'); break; default: alert('twinkleconfig: unknown data type for preference ' + pref.name); break; } } else if (Twinkle.prefs) { // Retain the hidden preferences that may have customised by the user from twinkleoptions.js // undefined if not set userValue = Twinkle.prefs[pref.name]; } } // only save those preferences that are *different* from the default if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) { newConfig[pref.name] = userValue; } }); }); let text = '// twinkleoptions.js: personal Twinkle preferences file\n' + '//\n' + '// NOTE: The easiest way to change your Twinkle preferences is by using the\n' + '// Twinkle preferences panel, at [[' + Morebits.pageNameNorm + ']].\n' + '//\n' + '// This file is AUTOMATICALLY GENERATED. Any changes you make (aside from\n' + '// changing the configuration parameters in a valid-JavaScript way) will be\n' + '// overwritten the next time you click "save" in the Twinkle preferences\n' + '// panel. If modifying this file, make sure to use correct JavaScript.\n' + // eslint-disable-next-line no-useless-concat '// <no' + 'wiki>\n' + '\n' + 'window.Twinkle.prefs = '; text += JSON.stringify(newConfig, null, 2); text += ';\n' + '\n' + // eslint-disable-next-line no-useless-concat '// </no' + 'wiki>\n' + '// End of twinkleoptions.js\n'; pageobj.setPageText(text); pageobj.setEditSummary('Saving Twinkle preferences: automatic edit from [[:' + Morebits.pageNameNorm + ']]'); pageobj.setChangeTags(Twinkle.changeTags); pageobj.setCreateOption('recreate'); pageobj.save(Twinkle.config.saveSuccess); }; Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) { pageobj.getStatusElement().info('successful'); const noticebox = document.createElement('div'); noticebox.className = 'cdx-message cdx-message--success'; noticebox.style.fontSize = '100%'; noticebox.innerHTML = '<p><b>Your Twinkle preferences have been saved.</b> To see the changes, you will need to clear your browser cache entirely (see <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> for instructions).</p>'; mw.loader.using('mediawiki.htmlform.codex.styles', () => { Morebits.Status.root.appendChild(noticebox); }); const noticeclear = document.createElement('br'); noticeclear.style.clear = 'both'; Morebits.Status.root.appendChild(noticeclear); }; Twinkle.addInitCallback(Twinkle.config.init); }()); // </nowiki> kvr881cm9p805n8gn410cyk08tqb74o MediaWiki:Gadget-HideCentralNotice 8 24483 268720 2026-04-27T16:56:32Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[meta:CentralNotice|CentralNotices]] görkezilişini ýapmak' 268720 wikitext text/x-wiki [[meta:CentralNotice|CentralNotices]] görkezilişini ýapmak 5jb02ahyujr941guqepvgy0h0ml2g1t MediaWiki:Gadget-HideCentralNotice.css 8 24484 268721 2026-04-27T16:57:35Z Umarxon III 11129 Sahypa döretdi, mazmuny: '#centralNotice { display: none !important; }' 268721 css text/css #centralNotice { display: none !important; } 6yybqmsqes9czpuq466face6r22cxyq MediaWiki:Gadget-HideCentralNotice.js 8 24485 268722 2026-04-27T16:58:18Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/** * Prevent CentralNotice banners from being loaded. * * This script relies on being able to listen for events emitted * by the browser when it starts proccessing a script, image or iframe. * If this script is loaded after the one it is trying to prevent, it will * effectivelty do nothing. */ if ( document.addEventListener ) { function blockBannerLoader (e) { var element, src; if (!e || !e.preventDefault) { return; }; element = e.target; i...' 268722 javascript text/javascript /** * Prevent CentralNotice banners from being loaded. * * This script relies on being able to listen for events emitted * by the browser when it starts proccessing a script, image or iframe. * If this script is loaded after the one it is trying to prevent, it will * effectivelty do nothing. */ if ( document.addEventListener ) { function blockBannerLoader (e) { var element, src; if (!e || !e.preventDefault) { return; }; element = e.target; if (!element) { return; } if (element.nodeName.toLowerCase() === 'script') { src = String(element.src); if (src.indexOf('Special:BannerLoader') !== -1 || src.indexOf('Special:BannerListLoader') !== -1) { e.preventDefault(); } } } // Listen to every script, image, iframe etc. being addded document.addEventListener('beforeload', blockBannerLoader, true); } gl6xan8awlqddueymvdt12k4bkf2z42 MediaWiki:Gadget-ReferenceTooltips 8 24486 268723 2026-04-27T17:04:06Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[mw:Reference Tooltips|Reference Tooltips]]: Makalanyň tekstinden daşlaşmazdan salgylanma maglumatlaryny görmek üçin setir içindäki sitatalaryň üstüne syçanjygy getirmek (eger ýokarda "Nawigasiýa açylýan penjireleri" işjeňleşdirilen bolsa, işlemez)' 268723 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[mw:Reference Tooltips|Reference Tooltips]]: Makalanyň tekstinden daşlaşmazdan salgylanma maglumatlaryny görmek üçin setir içindäki sitatalaryň üstüne syçanjygy getirmek (eger ýokarda "Nawigasiýa açylýan penjireleri" işjeňleşdirilen bolsa, işlemez) cl4d9ao8n9hlcpr7c5it1tag39f4c90 MediaWiki:Gadget-ReferenceTooltips.js 8 24487 268724 2026-04-27T17:05:56Z Umarxon III 11129 Sahypa döretdi, mazmuny: '// See [[mw:Reference Tooltips]] // Source https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js /*eslint space-in-parens: ["error", "always"], array-bracket-spacing: ["error", "always"]*/ ( function () { // If you're loading the script from another wiki and want to set your settings, do that in `window` // properties with `rt_` prefix, e.g. // window.rt_REF_LINK_SELECTOR = '...'; // They will be used instead of enwiki detaults. var REF_LINK_SE...' 268724 javascript text/javascript // See [[mw:Reference Tooltips]] // Source https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js /*eslint space-in-parens: ["error", "always"], array-bracket-spacing: ["error", "always"]*/ ( function () { // If you're loading the script from another wiki and want to set your settings, do that in `window` // properties with `rt_` prefix, e.g. // window.rt_REF_LINK_SELECTOR = '...'; // They will be used instead of enwiki detaults. var REF_LINK_SELECTOR = window.rt_REF_LINK_SELECTOR || '.reference, a[href^="#CITEREF"]', COMMENTED_TEXT_CLASS = window.rt_COMMENTED_TEXT_CLASS || 'rt-commentedText', COMMENTED_TEXT_SELECTOR = ( window.rt_COMMENTED_TEXT_SELECTOR || ( COMMENTED_TEXT_CLASS ? '.' + COMMENTED_TEXT_CLASS + ', ' : '' ) + 'abbr[title]' ); if ( mw.messages.get( 'rt-settings' ) === null ) { mw.messages.set( { 'rt-settings': 'Reference Tooltips settings', 'rt-enable-footer': 'Enable Reference Tooltips', 'rt-settings-title': 'Reference Tooltips', 'rt-save': 'Save', 'rt-enable': 'Enable Reference Tooltips', 'rt-activationMethod': 'Show a tooltip when I\'m', 'rt-hovering': 'hovering a reference', 'rt-clicking': 'clicking a reference', 'rt-delay': 'Delay before the tooltip appears (in milliseconds)', 'rt-tooltipsForComments': 'Show the tooltip over <span title="Tooltip example" class="' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' ) + '" style="border-bottom: 1px dotted; cursor: help;">text with a dotted underline</span> in Reference Tooltips style (allows to see such tooltips on devices with no mouse support)', 'rt-disabledNote': 'You can re-enable Reference Tooltips using a link in the footer of the page.', 'rt-done': 'Done', 'rt-enabled': 'Reference Tooltips are enabled' } ); } // "Global" variables var SECONDS_IN_A_DAY = 60 * 60 * 24, CLASSES = { FADE_IN_DOWN: 'rt-fade-in-down', FADE_IN_UP: 'rt-fade-in-up', FADE_OUT_DOWN: 'rt-fade-out-down', FADE_OUT_UP: 'rt-fade-out-up' }, IS_TOUCHSCREEN = 'ontouchstart' in document.documentElement, // Quite a rough check for mobile browsers, a mix of what is advised at // https://stackoverflow.com/a/24600597 (sends to // https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent) // and https://stackoverflow.com/a/14301832 IS_MOBILE = /Mobi|Android/i.test( navigator.userAgent ) || typeof window.orientation !== 'undefined', CLIENT_NAME = $.client.profile().name, settingsString, settings, enabled, delay, activatedByClick, tooltipsForComments, cursorWaitCss, windowManager, $teleportTarget, $body = $( document.body ), $window = $( window ), $overlay = $( '<div>' ) .addClass( 'rt-overlay' ) .appendTo( $body ); // Can't use before https://phabricator.wikimedia.org/T369880 is resolved // mw.loader.using( 'mediawiki.page.ready' ).then( function ( require ) { // $teleportTarget = $( require( 'mediawiki.page.ready' ).teleportTarget ); // $overlay.appendTo( $teleportTarget ); // } ); function rt( $content ) { // Popups gadget if ( window.pg ) { return; } var teSelector, settingsDialogOpening = false; function setSettingsCookie() { mw.cookie.set( 'RTsettings', ( Number( enabled ) + '|' + delay + '|' + Number( activatedByClick ) + '|' + Number( tooltipsForComments ) ), { path: '/', expires: 90 * SECONDS_IN_A_DAY, prefix: '' } ); } function enableRt() { enabled = true; setSettingsCookie(); $( '.rt-enableItem' ).remove(); rt( $content ); mw.notify( mw.msg( 'rt-enabled' ) ); } function disableRt() { $content.find( teSelector ).removeClass( 'rt-commentedText' ).off( '.rt' ); $body.off( '.rt' ); $window.off( '.rt' ); } function addEnableLink() { // #footer-places – Vector // #f-list – Timeless, Monobook, Modern // parent of #footer li – Cologne Blue var $footer = $( '#footer-places, #f-list' ); if ( !$footer.length ) { $footer = $( '#footer li' ).parent(); } if ( !$footer.find( '.rt-enableItem' ).length ) { $footer.append( $( '<li>' ) .addClass( 'rt-enableItem' ) .append( $( '<a>' ) .text( mw.msg( 'rt-enable-footer' ) ) .attr( 'href', '#' ) .click( function ( e ) { e.preventDefault(); enableRt(); } ) ) ); } } function TooltippedElement( $element ) { var events, te = this; function onStartEvent( e ) { var showRefArgs; if ( activatedByClick && te.type !== 'commentedText' && e.type !== 'contextmenu' ) { e.preventDefault(); } if ( !te.noRef ) { showRefArgs = [ $( this ) ]; if ( te.type !== 'supRef' ) { showRefArgs.push( e.pageX, e.pageY ); } te.showRef.apply( te, showRefArgs ); } } function onEndEvent() { if ( !te.noRef ) { te.hideRef(); } } if ( !$element ) { return; } // TooltippedElement.$element and TooltippedElement.$originalElement will be different when // the first is changed after its cloned version is hovered in a tooltip this.$element = $element; this.$originalElement = $element; if ( this.$element.is( REF_LINK_SELECTOR ) ) { if ( this.$element.prop( 'tagName' ) === 'SUP' ) { this.type = 'supRef'; } else { this.type = 'harvardRef'; } } else { this.type = 'commentedText'; this.comment = this.$element.attr( 'title' ); if ( !this.comment ) { return; } this.$element.addClass( 'rt-commentedText' ); } if ( activatedByClick ) { events = { 'click.rt': onStartEvent }; // Adds an ability to see tooltips for links if ( this.type === 'commentedText' && ( this.$element.closest( 'a' ).length || this.$element.has( 'a' ).length ) ) { events[ 'contextmenu.rt' ] = onStartEvent; } } else { events = { 'mouseenter.rt': onStartEvent, 'mouseleave.rt': onEndEvent }; } this.$element.on( events ); this.hideRef = function ( immediately ) { clearTimeout( te.showTimer ); if ( this.type === 'commentedText' ) { this.$element.attr( 'title', this.comment ); } if ( this.tooltip && this.tooltip.isPresent ) { if ( activatedByClick || immediately ) { this.tooltip.hide(); } else { this.hideTimer = setTimeout( function () { te.tooltip.hide(); }, 200 ); } } else if ( this.$ref && this.$ref.hasClass( 'rt-target' ) ) { this.$ref.removeClass( 'rt-target' ); if ( activatedByClick ) { $body.off( 'click.rt touchstart.rt', this.onBodyClick ); } } }; this.showRef = function ( $element, ePageX, ePageY ) { // Popups gadget if ( window.pg ) { disableRt(); return; } if ( this.tooltip && !this.tooltip.$content.length ) { return; } var tooltipInitiallyPresent = this.tooltip && this.tooltip.isPresent; function reallyShow() { var viewportTop, refOffsetTop, teHref; if ( !te.$ref && !te.comment ) { teHref = te.type === 'supRef' ? te.$element.find( 'a' ).attr( 'href' ) : te.$element.attr( 'href' ); // harvardRef te.$ref = teHref && $( '#' + $.escapeSelector( teHref.slice( 1 ) ) ); if ( !te.$ref || !te.$ref.length || !te.$ref.text() ) { te.noRef = true; return; } } if ( !tooltipInitiallyPresent && !te.comment ) { viewportTop = $window.scrollTop(); refOffsetTop = te.$ref.offset().top; if ( !activatedByClick && viewportTop < refOffsetTop && viewportTop + $window.height() > refOffsetTop + te.$ref.height() && // There can be gadgets/scripts that make references horizontally scrollable. $window.width() > te.$ref.offset().left + te.$ref.width() ) { // Highlight the reference itself te.$ref.addClass( 'rt-target' ); return; } } if ( !te.tooltip ) { te.tooltip = new Tooltip( te ); if ( !te.tooltip.$content.length ) { return; } } // If this tooltip is called from inside another tooltip. We can't define it // in the constructor since a ref can be cloned but have the same Tooltip object; // so, Tooltip.parent is a floating value. te.tooltip.parent = te.$element.closest( '.rt-tooltip' ).data( 'tooltip' ); if ( te.tooltip.parent && te.tooltip.parent.disappearing ) { return; } te.tooltip.show(); if ( tooltipInitiallyPresent ) { if ( te.tooltip.$element.hasClass( 'rt-tooltip-above' ) ) { te.tooltip.$element.addClass( CLASSES.FADE_IN_DOWN ); } else { te.tooltip.$element.addClass( CLASSES.FADE_IN_UP ); } return; } te.tooltip.calculatePosition( ePageX, ePageY ); $window.on( 'resize.rt', te.onWindowResize ); } // We redefine this.$element here because e.target can be a reference link inside // a reference tooltip, not a link that was initially assigned to this.$element this.$element = $element; if ( this.type === 'commentedText' ) { this.$element.attr( 'title', '' ); } if ( activatedByClick ) { if ( tooltipInitiallyPresent || ( this.$ref && this.$ref.hasClass( 'rt-target' ) ) ) { return; } else { setTimeout( function () { $body.on( 'click.rt touchstart.rt', te.onBodyClick ); }, 0 ); } } if ( activatedByClick || tooltipInitiallyPresent ) { reallyShow(); } else { this.showTimer = setTimeout( reallyShow, delay ); } }; this.onBodyClick = function ( e ) { if ( !te.tooltip && !( te.$ref && te.$ref.hasClass( 'rt-target' ) ) ) { return; } var $current = $( e.target ); function contextMatchesParameter( parameter ) { return this === parameter; } // The last condition is used to determine cases when a clicked tooltip is the current // element's tooltip or one of its descendants while ( $current.length && ( !$current.hasClass( 'rt-tooltip' ) || !$current.data( 'tooltip' ) || !$current.data( 'tooltip' ).upToTopParent( contextMatchesParameter, [ te.tooltip ], true ) ) ) { $current = $current.parent(); } if ( !$current.length ) { te.hideRef(); } }; this.onWindowResize = function () { te.tooltip.calculatePosition(); }; } function Tooltip( te ) { function openSettingsDialog() { var settingsDialog, settingsWindow; if ( cursorWaitCss ) { cursorWaitCss.disabled = true; } function SettingsDialog() { SettingsDialog.parent.call( this ); } OO.inheritClass( SettingsDialog, OO.ui.ProcessDialog ); SettingsDialog.static.name = 'settingsDialog'; SettingsDialog.static.title = mw.msg( 'rt-settings-title' ); SettingsDialog.static.actions = [ { modes: 'main', action: 'save', label: mw.msg( 'rt-save' ), flags: [ 'primary', 'progressive' ] }, { modes: 'main', flags: [ 'safe', 'close' ] }, { modes: 'disabled', action: 'deactivated', label: mw.msg( 'rt-done' ), flags: [ 'primary', 'progressive' ] } ]; SettingsDialog.prototype.initialize = function () { var dialog = this; SettingsDialog.parent.prototype.initialize.apply( this, arguments ); this.enableCheckbox = new OO.ui.CheckboxInputWidget( { selected: true } ); this.enableCheckbox.on( 'change', function ( selected ) { dialog.activationMethodSelect.setDisabled( !selected ); dialog.delayInput.setDisabled( !selected || dialog.clickOption.isSelected() ); dialog.tooltipsForCommentsCheckbox.setDisabled( !selected ); } ); this.enableField = new OO.ui.FieldLayout( this.enableCheckbox, { label: mw.msg( 'rt-enable' ), align: 'inline', classes: [ 'rt-enableField' ] } ); this.hoverOption = new OO.ui.RadioOptionWidget( { label: mw.msg( 'rt-hovering' ) } ); this.clickOption = new OO.ui.RadioOptionWidget( { label: mw.msg( 'rt-clicking' ) } ); this.activationMethodSelect = new OO.ui.RadioSelectWidget( { items: [ this.hoverOption, this.clickOption ] } ); this.activationMethodSelect.selectItem( activatedByClick ? this.clickOption : this.hoverOption ); this.activationMethodSelect.on( 'choose', function ( item ) { dialog.delayInput.setDisabled( item === dialog.clickOption ); } ); this.activationMethodField = new OO.ui.FieldLayout( this.activationMethodSelect, { label: mw.msg( 'rt-activationMethod' ), align: 'top' } ); this.delayInput = new OO.ui.NumberInputWidget( { input: { value: delay }, step: 50, min: 0, max: 5000, disabled: activatedByClick, classes: [ 'rt-numberInput' ] } ); this.delayField = new OO.ui.FieldLayout( this.delayInput, { label: mw.msg( 'rt-delay' ), align: 'top' } ); this.tooltipsForCommentsCheckbox = new OO.ui.CheckboxInputWidget( { selected: tooltipsForComments } ); this.tooltipsForCommentsField = new OO.ui.FieldLayout( this.tooltipsForCommentsCheckbox, { label: new OO.ui.HtmlSnippet( mw.msg( 'rt-tooltipsForComments' ) ), align: 'inline', classes: [ 'rt-tooltipsForCommentsField' ] } ); new TooltippedElement( this.tooltipsForCommentsField.$element.find( '.' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' ) ) ); this.fieldset = new OO.ui.FieldsetLayout(); this.fieldset.addItems( [ this.enableField, this.activationMethodField, this.delayField, this.tooltipsForCommentsField ] ); this.panelSettings = new OO.ui.PanelLayout( { padded: true, expanded: false } ); this.panelSettings.$element.append( this.fieldset.$element ); this.panelDisabled = new OO.ui.PanelLayout( { padded: true, expanded: false } ); this.panelDisabled.$element.append( $( '<table>' ) .addClass( 'rt-disabledHelp' ) .append( $( '<tr>' ).append( $( '<td>' ).append( $( '<img>' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/c/c0/MediaWiki_footer_link_ltr.svg' ) ), $( '<td>' ) .addClass( 'rt-disabledNote' ) .text( mw.msg( 'rt-disabledNote' ) ) ) ) ); this.stackLayout = new OO.ui.StackLayout( { items: [ this.panelSettings, this.panelDisabled ] } ); this.$body.append( this.stackLayout.$element ); }; SettingsDialog.prototype.getSetupProcess = function ( data ) { return SettingsDialog.parent.prototype.getSetupProcess.call( this, data ) .next( function () { this.stackLayout.setItem( this.panelSettings ); this.actions.setMode( 'main' ); }, this ); }; SettingsDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action === 'save' ) { return new OO.ui.Process( function () { var newDelay = Number( dialog.delayInput.getValue() ); enabled = dialog.enableCheckbox.isSelected(); if ( newDelay >= 0 && newDelay <= 5000 ) { delay = newDelay; } activatedByClick = dialog.clickOption.isSelected(); tooltipsForComments = dialog.tooltipsForCommentsCheckbox.isSelected(); setSettingsCookie(); if ( enabled ) { dialog.close(); disableRt(); rt( $content ); } else { dialog.actions.setMode( 'disabled' ); dialog.stackLayout.setItem( dialog.panelDisabled ); disableRt(); addEnableLink(); } } ); } else if ( action === 'deactivated' ) { dialog.close(); } return SettingsDialog.parent.prototype.getActionProcess.call( this, action ); }; SettingsDialog.prototype.getBodyHeight = function () { return this.stackLayout.getCurrentItem().$element.outerHeight( true ); }; tooltip.upToTopParent( function adjustRightAndHide() { if ( this.isPresent ) { if ( this.$element[ 0 ].style.right ) { this.$element.css( 'right', '+=' + ( window.innerWidth - $window.width() ) ); } this.te.hideRef( true ); } } ); if ( !windowManager ) { windowManager = new OO.ui.WindowManager(); $body.append( windowManager.$element ); } settingsDialog = new SettingsDialog(); windowManager.addWindows( [ settingsDialog ] ); settingsWindow = windowManager.openWindow( settingsDialog ); settingsWindow.opened.then( function () { settingsDialogOpening = false; } ); settingsWindow.closed.then( function () { windowManager.clearWindows(); } ); } var tooltip = this; // This variable can change: one tooltip can be called from a harvard-style reference link // that is put into different tooltips this.te = te; switch ( this.te.type ) { case 'supRef': this.id = 'rt-' + this.te.$originalElement.attr( 'id' ); this.$content = this.te.$ref .contents() .filter( function ( i ) { var $this = $( this ); if ( $this.hasClass( 'mw-subreference-list' ) ) { return false; } return ( this.nodeType === Node.TEXT_NODE || !( // `a[href^="#cite_ref-"]` is for Wiktionary and possibly other // sites (not English Wikipedia) where the output of the Cite // extension is slightly different $this.is( '.mw-cite-backlink, a[href^="#cite_ref-"]' ) || ( i === 0 && // Template:Cnote, Template:Note ( $this.is( 'b' ) || // Template:Note_label $this.is( 'a' ) && $this.attr( 'href' ).indexOf( '#ref' ) === 0 ) ) ) ); } ) .clone( true ); const $ol = this.te.$ref.closest( 'ol' ); if ( $ol.hasClass( 'mw-subreference-list' ) ) { this.$content = $( '<div>' ).append( $ol.siblings( '.reference-text' ).clone( true ) .css( { display: 'block', 'margin-bottom': '0.7em' } ), this.$content ); } break; case 'harvardRef': this.id = 'rt-' + this.te.$originalElement.closest( 'li' ).attr( 'id' ); this.$content = this.te.$ref .clone( true ) .removeAttr( 'id' ); break; case 'commentedText': this.id = 'rt-' + String( Math.random() ).slice( 2 ); this.$content = $( document.createTextNode( this.te.comment ) ); break; } if ( !this.$content.length ) { return; } this.isInsideWindow = Boolean( this.te.$element.closest( '.oo-ui-window' ).length ); this.$element = $( '<div>' ) .addClass( 'rt-tooltip' ) .attr( 'id', this.id ) .attr( 'role', 'tooltip' ) .data( 'tooltip', this ); var $hoverArea = $( '<div>' ) .addClass( 'rt-hoverArea' ) .appendTo( this.$element ); var $scroll = $( '<div>' ) .addClass( 'rt-scroll' ) .appendTo( $hoverArea ); this.$content = this.$content .wrapAll( '<div>' ) .parent() .addClass( 'rt-content' ) .addClass( 'mw-parser-output' ) .appendTo( $scroll ); if ( !activatedByClick ) { this.$element .on( 'mouseenter linkPopupHover', function ( e ) { if ( !tooltip.disappearing || e.type === 'linkPopupHover' ) { tooltip.upToTopParent( function () { this.show(); } ); } } ) .on( 'mouseleave', function ( e ) { // https://stackoverflow.com/q/47649442 workaround. Relying on relatedTarget // alone has pitfalls: when alt-tabbing, relatedTarget is empty too if ( CLIENT_NAME !== 'chrome' || ( !e.originalEvent || e.originalEvent.relatedTarget !== null || !tooltip.clickedTime || $.now() - tooltip.clickedTime > 50 ) ) { tooltip.upToTopParent( function () { this.te.hideRef(); } ); } } ) .click( function () { tooltip.clickedTime = $.now(); } ); } if ( !this.isInsideWindow ) { $( '<a>' ) .addClass( 'rt-settingsLink' ) .attr( 'role', 'button' ) .attr( 'href', '#' ) .attr( 'title', mw.msg( 'rt-settings' ) ) .click( function ( e ) { e.preventDefault(); if ( settingsDialogOpening ) { return; } settingsDialogOpening = true; if ( mw.loader.getState( 'oojs-ui' ) !== 'ready' ) { if ( cursorWaitCss ) { cursorWaitCss.disabled = false; } else { cursorWaitCss = mw.util.addCSS( 'body { cursor: wait; }' ); } } mw.loader.using( [ 'oojs', 'oojs-ui' ], openSettingsDialog ); } ) .prependTo( this.$content ); } // Tooltip tail element is inside tooltip content element in order for the tooltip // not to disappear when the mouse is above the tail this.$tail = $( '<div>' ) .addClass( 'rt-tail' ) .prependTo( this.$element ); this.disappearing = false; this.show = function () { this.disappearing = false; clearTimeout( this.te.hideTimer ); clearTimeout( this.te.removeTimer ); this.$element .removeClass( CLASSES.FADE_OUT_DOWN ) .removeClass( CLASSES.FADE_OUT_UP ); if ( !this.isPresent ) { $overlay.append( this.$element ); } this.isPresent = true; }; this.hide = function () { var tooltip = this; tooltip.disappearing = true; if ( tooltip.$element.hasClass( 'rt-tooltip-above' ) ) { tooltip.$element .removeClass( CLASSES.FADE_IN_DOWN ) .addClass( CLASSES.FADE_OUT_UP ); } else { tooltip.$element .removeClass( CLASSES.FADE_IN_UP ) .addClass( CLASSES.FADE_OUT_DOWN ); } tooltip.te.removeTimer = setTimeout( function () { if ( tooltip.isPresent ) { tooltip.$element.detach(); tooltip.$tail.css( 'left', '' ); if ( activatedByClick ) { $body.off( 'click.rt touchstart.rt', tooltip.te.onBodyClick ); } $window.off( 'resize.rt', tooltip.te.onWindowResize ); tooltip.isPresent = false; } }, 200 ); }; this.calculatePosition = function ( ePageX, ePageY ) { var teElement, teOffsets, teOffset, targetTailOffsetX, tailLeft; this.$tail.css( 'left', '' ); teElement = this.te.$element.get( 0 ); if ( ePageX !== undefined ) { targetTailOffsetX = ePageX; teOffsets = ( teElement.getClientRects && teElement.getClientRects() ) || teElement.getBoundingClientRect(); if ( teOffsets.length > 1 ) { for ( var i = teOffsets.length - 1; i >= 0; i-- ) { if ( ePageY >= Math.round( $window.scrollTop() + teOffsets[ i ].top ) && ePageY <= Math.round( $window.scrollTop() + teOffsets[i].top + teOffsets[ i ].height ) ) { teOffset = teOffsets[ i ]; } } } } if ( !teOffset ) { teOffset = ( teElement.getClientRects && teElement.getClientRects()[ 0 ] ) || teElement.getBoundingClientRect(); } teOffset = { top: $window.scrollTop() + teOffset.top, left: $window.scrollLeft() + teOffset.left, width: teOffset.width, height: teOffset.height }; if ( !targetTailOffsetX ) { targetTailOffsetX = teOffset.left + ( teOffset.width / 2 ); } // Value of `left` in `.rt-tooltip-above .rt-tail` var defaultTailLeft = 19; // Value of `width` in `.rt-tail` var tailSideWidth = 13; // We tilt the square 45 degrees, so we need square root to calculate the distance. var tailWidth = tailSideWidth * Math.SQRT2; var tailHeight = tailWidth / 2; var tailCenterDelta = tailSideWidth + 1 - ( tailWidth / 2 ); var tooltip = this; var getTop = function ( isBelow ) { var delta = isBelow ? teOffset.height + tailHeight : -tooltip.$element.outerHeight() - tailHeight + 1; return teOffset.top + delta; }; this.$element.css( { top: getTop(), left: targetTailOffsetX - defaultTailLeft - tailCenterDelta, right: '' } ); // Is it squished against the right side of the page? if ( this.$element.offset().left + this.$element.outerWidth() > $window.width() - 1 ) { this.$element.css( { left: '', right: 0 } ); tailLeft = targetTailOffsetX - this.$element.offset().left - tailCenterDelta; } // Is a part of it above the top of the screen? if ( teOffset.top < this.$element.outerHeight() + $window.scrollTop() + tailHeight ) { this.$element .removeClass( 'rt-tooltip-above' ) .addClass( 'rt-tooltip-below' ) .addClass( CLASSES.FADE_IN_UP ) .css( { top: getTop( true ) } ); if ( tailLeft ) { this.$tail.css( 'left', ( tailLeft + tailSideWidth ) + 'px' ); } } else { this.$element .removeClass( 'rt-tooltip-below' ) .addClass( 'rt-tooltip-above' ) .addClass( CLASSES.FADE_IN_DOWN ) // A fix for cases when a tooltip shown once is then wrongly positioned when it // is shown again after a window resize. .css( { top: getTop() } ); if ( tailLeft ) { this.$tail.css( 'left', tailLeft + 'px' ); } } }; // Run some function for all the tooltips up to the top one in a tree. Its context will be // the tooltip, while its parameters may be passed to Tooltip.upToTopParent as an array // in the second parameter. If the third parameter passed to ToolTip.upToTopParent is true, // the execution stops when the function in question returns true for the first time, // and ToolTip.upToTopParent returns true as well. this.upToTopParent = function ( func, parameters, stopAtTrue ) { var returnValue, currentTooltip = this; do { returnValue = func.apply( currentTooltip, parameters ); if ( stopAtTrue && returnValue ) { break; } } while ( ( currentTooltip = currentTooltip.parent ) ); if ( stopAtTrue ) { return returnValue; } }; } if ( !enabled ) { addEnableLink(); return; } teSelector = REF_LINK_SELECTOR; if ( tooltipsForComments ) { teSelector += ', ' + COMMENTED_TEXT_SELECTOR; } $content.find( teSelector ).each( function () { new TooltippedElement( $( this ) ); } ); } settingsString = mw.cookie.get( 'RTsettings', '' ); if ( settingsString ) { settings = settingsString.split( '|' ); enabled = Boolean( Number( settings[ 0 ] ) ); delay = Number( settings[ 1 ] ); activatedByClick = Boolean( Number( settings[ 2 ] ) ); // The forth value was added later, so we provide for a default value. See comments below // for why we use "IS_TOUCHSCREEN && IS_MOBILE". tooltipsForComments = settings[ 3 ] === undefined ? IS_TOUCHSCREEN && IS_MOBILE : Boolean( Number( settings[ 3 ] ) ); } else { enabled = true; delay = 200; // Since the mobile browser check is error-prone, adding IS_MOBILE condition here would probably // leave cases where a user interacting with the browser using touches doesn't know how to call // a tooltip in order to switch to activation by click. Some touch-supporting laptop users // interacting by touch (though probably not the most popular use case) would not be happy too. activatedByClick = IS_TOUCHSCREEN; // Arguably we shouldn't convert native tooltips into gadget tooltips for devices that have // mouse support, even if they have touchscreens (there are laptops with touchscreens). // IS_TOUCHSCREEN check here is for reliability, since the mobile check is prone to false // positives. tooltipsForComments = IS_TOUCHSCREEN && IS_MOBILE; } mw.hook( 'wikipage.content' ).add( rt ); }() ); a43nofchz87jsl0z00dy2djz6ctocgi MediaWiki:Gadget-ReferenceTooltips.css 8 24488 268725 2026-04-27T17:06:48Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* See [[mw:Reference Tooltips]] */ .rt-overlay { position: absolute; width: 100%; font-size: calc(var(--font-size-medium, 1rem) * (13 / 14)); line-height: 1.5em; /* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */ z-index: 800; /* match z-index-tooltip in https://doc.wikimedia.org/codex/latest/design-tokens/z-index.html */ top: 0; } /* Remove after https://phabricator.wikimedia.org/T369880 is resolve...' 268725 css text/css /* See [[mw:Reference Tooltips]] */ .rt-overlay { position: absolute; width: 100%; font-size: calc(var(--font-size-medium, 1rem) * (13 / 14)); line-height: 1.5em; /* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */ z-index: 800; /* match z-index-tooltip in https://doc.wikimedia.org/codex/latest/design-tokens/z-index.html */ top: 0; } /* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */ .skin-vector-legacy .rt-overlay { font-size: 13px; } .skin-monobook .rt-overlay { font-size: 12.7px; } .rt-tooltip { position: absolute; max-width: 27em; background: var(--background-color-base, #fff); color: var(--color-base, #202122); border: 1px solid var(--border-color-subtle, #c8ccd1); border-radius: 2px; box-shadow: 0 20px 48px 0 rgba(0, 0, 0, 0.2); } html.skin-theme-clientpref-night .rt-tooltip { box-shadow: 0 20px 48px 0 rgba(0, 0, 0, 1); } /* Extend the tooltip vertically to make sure it doesn't disappear while the user moves the mouse to it */ .rt-tooltip-above .rt-hoverArea { margin-bottom: -0.6em; padding-bottom: 0.6em; } .rt-tooltip-below .rt-hoverArea { margin-top: -0.7em; padding-top: 0.7em; } .rt-scroll { overflow-x: auto; } .rt-content { padding: 0.7em 0.9em; overflow-wrap: break-word; } .rt-tail { /* Use 48%, not 50%, to make the tail start at a right place in Blink browsers in Windows on bigger system font sizes */ background: linear-gradient(to top right, var(--border-color-subtle, #c8ccd1) 48%, rgba(0, 0, 0, 0) 48%); --tail-left: 19px; --tail-side-width: 13px; } .rt-tail, .rt-tail:after { position: absolute; /* Make sure the tail is behind the scrollbar, e.g. [73] at https://en.wikipedia.org/w/index.php?title=Lemniscate_elliptic_functions&oldid=1231701944#cite_ref-73 if .rt-tooltip has width of 25em */ z-index: -1; width: var(--tail-side-width); height: var(--tail-side-width); } .rt-tail:after { content: ''; background: var(--background-color-base, #fff); bottom: 1px; left: 1px; } .rt-tooltip-above .rt-tail { transform: rotate(-45deg); transform-origin: 100% 100%; bottom: 0; left: var(--tail-left); } .rt-tooltip-below .rt-tail { transform: rotate(135deg); transform-origin: 0 0; top: 0; left: calc(var(--tail-left) + var(--tail-side-width)); } .rt-settingsLink { background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%0D%0A%20%20%20%20%3Cpath%20fill%3D%22%2354595d%22%20d%3D%22M20%2014.5v-2.9l-1.8-.3c-.1-.4-.3-.8-.6-1.4l1.1-1.5-2.1-2.1-1.5%201.1c-.5-.3-1-.5-1.4-.6L13.5%205h-2.9l-.3%201.8c-.5.1-.9.3-1.4.6L7.4%206.3%205.3%208.4l1%201.5c-.3.5-.4.9-.6%201.4l-1.7.2v2.9l1.8.3c.1.5.3.9.6%201.4l-1%201.5%202.1%202.1%201.5-1c.4.2.9.4%201.4.6l.3%201.8h3l.3-1.8c.5-.1.9-.3%201.4-.6l1.5%201.1%202.1-2.1-1.1-1.5c.3-.5.5-1%20.6-1.4l1.5-.3zM12%2016c-1.7%200-3-1.3-3-3s1.3-3%203-3%203%201.3%203%203-1.3%203-3%203z%22%2F%3E%0D%0A%3C%2Fsvg%3E); float: right; margin: -0.5em -0.5em 0 0.5em; box-sizing: border-box; height: 32px; width: 32px; border: 1px solid transparent; border-radius: 2px; background-position: center center; background-repeat: no-repeat; background-size: 24px 24px; } html.skin-theme-clientpref-night .rt-settingsLink { background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%0D%0A%20%20%20%20%3Cpath%20fill%3D%22%23c8ccd1%22%20d%3D%22M20%2014.5v-2.9l-1.8-.3c-.1-.4-.3-.8-.6-1.4l1.1-1.5-2.1-2.1-1.5%201.1c-.5-.3-1-.5-1.4-.6L13.5%205h-2.9l-.3%201.8c-.5.1-.9.3-1.4.6L7.4%206.3%205.3%208.4l1%201.5c-.3.5-.4.9-.6%201.4l-1.7.2v2.9l1.8.3c.1.5.3.9.6%201.4l-1%201.5%202.1%202.1%201.5-1c.4.2.9.4%201.4.6l.3%201.8h3l.3-1.8c.5-.1.9-.3%201.4-.6l1.5%201.1%202.1-2.1-1.1-1.5c.3-.5.5-1%20.6-1.4l1.5-.3zM12%2016c-1.7%200-3-1.3-3-3s1.3-3%203-3%203%201.3%203%203-1.3%203-3%203z%22%2F%3E%0D%0A%3C%2Fsvg%3E); } .rt-settingsLink:hover, .rt-settingsLink:active { background-color: var(--background-color-interactive, #eaecf0); } .rt-settingsLink:active { border-color: var(--border-color-interactive, #72777d); } .rt-settingsLink:focus { outline: 1px solid transparent; } .rt-settingsLink: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); } .rt-target { background-color: var(--background-color-progressive-subtle, #eaf3ff); } .rt-enableField { font-weight: bold; margin-bottom: 1.25em; } .rt-numberInput.rt-numberInput { width: 10em; } .rt-tooltipsForCommentsField.rt-tooltipsForCommentsField.rt-tooltipsForCommentsField { margin-top: 1.25em; } .rt-disabledHelp { border-collapse: collapse; } .rt-disabledHelp td { padding: 0; } .rt-disabledNote.rt-disabledNote { vertical-align: bottom; padding-left: 0.36em; font-weight: bold; } @keyframes rt-fade-in-up { 0% { opacity: 0; transform: translate(0, 20px); } 100% { opacity: 1; transform: translate(0, 0); } } @keyframes rt-fade-in-down { 0% { opacity: 0; transform: translate(0, -20px); } 100% { opacity: 1; transform: translate(0, 0); } } @keyframes rt-fade-out-down { 0% { opacity: 1; transform: translate(0, 0); } 100% { opacity: 0; transform: translate(0, 20px); } } @keyframes rt-fade-out-up { 0% { opacity: 1; transform: translate(0, 0); } 100% { opacity: 0; transform: translate(0, -20px); } } .rt-fade-in-up { animation: rt-fade-in-up 0.2s ease forwards; } .rt-fade-in-down { animation: rt-fade-in-down 0.2s ease forwards; } .rt-fade-out-down { animation: rt-fade-out-down 0.2s ease forwards; } .rt-fade-out-up { animation: rt-fade-out-up 0.2s ease forwards; } 8zpwcq3vyh7m42hvp1h7840jq6sb25g MediaWiki:Gadget-formWizard 8 24489 268726 2026-04-27T17:10:05Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[meta:Meta:FormWizard|FormWizard]]: taslama sahypalaryny döretmek we giňeltmek üçin wizard' 268726 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[meta:Meta:FormWizard|FormWizard]]: taslama sahypalaryny döretmek we giňeltmek üçin wizard 7h70mpsfxr3rir7i9d4vzilebu45kwg MediaWiki:Gadget-formWizard.js 8 24490 268727 2026-04-27T17:12:22Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* ______________________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] | * | before...' 268727 javascript text/javascript /* ______________________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] | * | before editing. | * |_____________________________________________________________________________________| * * See https://meta.wikimedia.org/wiki/Meta:FormWizard for usage and description. */ /* global mw */ if ( mw.config.get('wgCanonicalNamespace') == 'Project' ) { mw.loader.load( 'ext.gadget.formWizard-core' ); } jn0dzk5mqlbsut7j3cnq8o2m7kyqp85 MediaWiki:Gadget-formWizard.css 8 24491 268728 2026-04-27T17:13:24Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] before editing. | * |________________________________...' 268728 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] before editing. | * |_____________________________________________________________________________| * * "Forms" feature, to be used by the Wikimedia Foundation's Grants Programme, */ /* .formsGadget * { box-sizing: border-box; } */ .formsGadget .ui-state-hover{ background: transparent; border-color: #a1a1a1; } .formsGadget.ui-dialog .ui-icon-closethick{ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTEuMzEzcHgiIGhlaWdodD0iMTEuMzEzcHgiIHZpZXdCb3g9IjAgMCAxMS4zMTMgMTEuMzEzIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMS4zMTMgMTEuMzEzIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNCMEIxQjEiIHBvaW50cz0iMTEuMzEzLDIuMTIxIDkuMTkyLDAgNS42NTcsMy41MzUgMi4xMjIsMCAwLDIuMTIxIDMuNTM2LDUuNjU2IDAuMDAxLDkuMTkxIDIuMTIyLDExLjMxMyANCgk1LjY1Nyw3Ljc3NyA5LjE5MiwxMS4zMTIgMTEuMzEzLDkuMTkxIDcuNzc4LDUuNjU2ICIvPg0KPC9zdmc+DQo=) no-repeat 50% 50% !important; } .formsGadget .ui-widget-content{ background: #EEE; } .formsGadget.ui-dialog .ui-widget-header{ border: none; background: #EEE !important; padding-left: 11px !important; margin-top: 25px; } .formsGadget.ui-widget-content{ background: #EEE; padding: 10px 18px; color: #444; border: 1px solid #ddd; border-bottom: 3px solid #D0D0D0; border-radius: 1px; } .formsGadget .gadgetControls{ text-align: right; margin: 10px 0px; } /*Check*/ .formsGadget .messageDescription{ margin: 10px 0px; } .formsGadget textarea, .formsGadget input[type=text], .formsGadget input[type=number]{ outline: 0px; padding: 14px 7px; resize: none; background-color: white; border: 1px solid #DDD; border-radius: 1px; margin-bottom: 1em; } .formsGadget textarea:focus, .formsGadget input[type=text]:focus{ box-shadow: inset .45em 0 0 #5088f7; } div.grantsHide{ display: none; } .formsGadget mw-ui-vform{ width: 100%; } .formsGadget textarea::-webkit-input-placeholder, .formsGadget textarea::-moz-placeholder, /* Firefox 19+ */ .formsGadget textarea:-ms-input-placeholder, .formsGadget input::-webkit-input-placeholder, .formsGadget input::-moz-placeholder, /* Firefox 19+ */ .formsGadget input:-ms-input-placeholder { font-style: italic; } .formsGadget input.entryNotSatisfying:focus{ box-shadow: inset .45em 0 0 #D11D13; } .formsGadget input.entrySatisfying:focus{ box-shadow: inset .45em 0 0 #5088f7; } .formsGadget .title{ font-weight: bold; } .formsGadget .text{ line-height: 15px; margin: 6px 0px; } .formsGadget .elementContainer{ padding: 7px 0px; } .formsGadget .inputListItem{ width: 50px; margin-right: 30px; } .formsGadget input[type=text], .formsGadget input[type=number]{ padding: 7px; } .formsGadget input[type=number]{ text-align: center; } .formsGadget .inputListItemDescription{ /* width: 120px; */ display: inline-block; } .formsGadget .ui-widget-content a{ color: #0b0080; } .formsGadget .ui-widget-content a span{ color: #444444; } .formsGadget .validationContainer.entrySatisfying:after{ content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAICAYAAAAvOAWIAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB94HFgU5GtaftYcAAACqSURBVBjTfZA7SkMBFAXPi5UfcAHBwl6wt7HOfnQZgr0bSGdt6x7ERlDEyiIgCPLAM2NhIhE/U94Lc5k75A/UDMMQYDvJRZK3/Ie62XbeVuAm6q56BAT4srbdaXsFCNwC0wAPwKt6vGbcAuZ+8qQerBYny+ELMEsS4BJQfQb2V9eibgBnwLu6UK/b2vax7Z76PWQcxwlwvrQJ3KuHP4rXoibqKXAHTH/7zge2aLmJza+PXAAAAABJRU5ErkJggg==); padding: 7px 8px 7px 8px; background-color: #347BFF; border: 1px solid #DDD; border-width: 1px 1px 1px 0; } .formsGadget .validationContainer.entryNotSatisfying:after{ content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB94HFgU0Ikszc1QAAADOSURBVBjTbZAhTkNBFEXvT7+gCRLDLkgTUtUF1NSxnErkl4gmX4DuQuoKC/iWYKqqyzmYGZgAN3kZc/LmnRv1BhjULdDnV4CV+qIuow6qgMAIXFVQ3QDvgOoUYFth9aLuysa1+mEJcIjaq2MBa/bqVBYIHNW7etdc3TU/tHlV7/OPzL6lgJO6+AOqa3VqwDpP6rwFN62MemrOuZSW+pm6SvKc5FZNkrckD13XfSZZJJmV9zql8JpjlSnSYyN9DrAEJuDwXc+PcK8OwBl4/AKbp0zxmA5MdAAAAABJRU5ErkJggg==); padding: 7px 8px 7px 8px; background-color: #D11D13; border: 1px solid #DDD; border-width: 1px 1px 1px 0; } .formsGadget .inputElementWrapper{ position: relative; } .formsGadget .mandatoryContainer:before{ position: absolute; left: -6px; content: "*"; color: #D11D13; } .formsGadget img{ height: 100px; } .formsGadget input[type=text]{ margin: 0 0 10px 0; width: 90%; } .formsGadget input[type=submit]{ margin-right: .5em; float: right; } /* Dropdown styling */ .formsGadget .chzn-container-single .chzn-single{ background-image: none; border-radius: 1px; box-shadow: none; border: 1px solid #ddd; padding: .25em; padding-left: 14px; } .formsGadget .chzn-container .chzn-results li { padding-left: 10px; } .formsGadget .chzn-container-active.chzn-with-drop .chzn-single{ /*Get a better color*/ background: #F6F3F3; border: 1px solid #aaa; padding: .25em; padding-left: 14px; } .formsGadget .chzn-container .chzn-results .highlighted{ background-image: none; background-color: #347bff; } .formsGadget .chzn-container-single .chzn-single div b{ background-position: 0px 5px; } .formsGadget .chzn-container-active.chzn-with-drop .chzn-single div b{ background-position: -18px 5px; } /* Dialog Loading*/ .formsGadget .loading{ background: url(data:image/gif;base64,R0lGODlhgAAPAPEAAP///6fX+eXy/KfX+SH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAgAAPAAACo5QvoIC33NKKUtF3Z8RbN/55CEiNonMaJGp1bfiaMQvBtXzTpZuradUDZmY+opA3DK6KwaQTCbU9pVHc1LrDUrfarq765Ya9u+VRzLyO12lwG10yy39zY11Jz9t/6jf5/HfXB8hGWKaHt6eYyDgo6BaH6CgJ+QhnmWWoiVnI6ddJmbkZGkgKujhplNpYafr5OooqGst66Uq7OpjbKmvbW/p7UAAAIfkECQoAAAAsAAAAAIAADwAAArCcP6Ag7bLYa3HSZSG2le/Zgd8TkqODHKWzXkrWaq83i7V5s6cr2f2TMsSGO9lPl+PBisSkcekMJphUZ/OopGGfWug2Jr16x92yj3w247bh6teNXseRbyvc0rbr6/x5Ng0op4YSJDb4JxhI58eliEiYYujYmFi5eEh5OZnXhylp+RiaKQpWeDf5qQk6yprawMno2nq6KlsaSauqS5rLu8cI69k7+ytcvGl6XDtsyzxcAAAh+QQJCgAAACwAAAAAgAAPAAACvpw/oIC3IKIUb8pq6cpacWyBk3htGRk1xqMmZviOcemdc4R2kF3DvfyTtFiqnPGm+yCPQdzy2RQMF9Moc+fDArU0rtMK9SYzVUYxrASrxdc0G00+K8ruOu+9tmf1W06ZfsfXJfiFZ0g4ZvEndxjouPfYFzk4mcIICJkpqUnJWYiYs9jQVpm4edqJ+lkqikDqaZoquwr7OtHqAFerqxpL2xt6yQjKO+t7bGuMu1L8a5zsHI2MtOySVwo9fb0bVQAAIfkECQoAAAAsAAAAAIAADwAAAsucP6CAt9zSErSKZyvOd/KdgZaoeaFpRZKiPi1aKlwnfzBF4jcNzDk/e7EiLuLuhzwqayfmaNnjCCGNYhXqw9qcsWjT++TqxIKp2UhOprXf7PoNrpyvQ3p8fAdu82o+O5w3h2A1+Nfl5geHuLgXhEZVWBeZSMnY1oh5qZnyKOhgiGcJKHqYOSrVmWpHGmpauvl6CkvhaUD4qejaOqvH2+doV7tSqdsrexybvMsZrDrJaqwcvSz9i9qM/Vxs7Qs6/S18a+vNjUx9/v1TAAAh+QQJCgAAACwAAAAAgAAPAAAC0Zw/oIC33NKKUomLxct4c718oPV5nJmhGPWwU9TCYTmfdXp3+aXy+wgQuRRDSCN2/PWAoqVTCSVxilQZ0RqkSXFbXdf3ZWqztnA1eUUbEc9wm8yFe+VguniKPbNf6mbU/ubn9ieUZ6hWJAhIOKbo2Pih58C3l1a5OJiJuflYZidpgHSZCOnZGXc6l3oBWrE2aQnLWYpKq2pbV4h4OIq1eldrigt8i7d73Ns3HLjMKGycHC1L+hxsXXydO9wqOu3brPnLXL3C640sK+6cTaxNflEAACH5BAkKAAAALAAAAACAAA8AAALVnD+ggLfc0opS0SeyFnjn7oGbqJHf4mXXFD2r1bKNyaEpjduhPvLaC5nJEK4YTKhI1ZI334m5g/akJacAiDUGiUOHNUd9ApTgcTN81WaRW++Riy6Tv/S4dQ1vG4ps4NwOaBYlOEVYhYbnplexyJf3ZygGOXkWuWSZuNel+aboV0k5GFo4+qN22of6CMoq2kr6apo6m5fJWCoZm+vKu2Hr6KmqiHtJLKebRhuszNlYZ3ncewh9J9z8u3mLHA0rvetrzYjd2Wz8bB6oNO5MLq6FTp2+bVUAACH5BAkKAAAALAAAAACAAA8AAALanD+ggLfc0opS0XeX2Fy8zn2gp40ieHaZFWHt9LKNO5eo3aUhvisj6RutIDUZgnaEFYnJ4M2Z4210UykQ8BtqY0yHstk1UK+/sdk63i7VYLYX2sOa0HR41S5wi7/vcMWP1FdWJ/dUGIWXxqX3xxi4l0g4GEl5yOHIBwmY2cg1aXkHSjZXmbV4uoba5kkqelbaapo6u0rbN/SZG7trKFv7e6savKTby4voaoVpNAysiXscV4w8fSn8fN1pq1kd2j1qDLK8yYy9/ff9mgwrnv2o7QwvGO1ND049UgAAIfkECQoAAAAsAAAAAIAADwAAAticP6CAt9zSilLRd2d8onvBfV0okp/pZdamNRi7ui3yyoo4Ljio42h+w6kgNiJt5kAaasdYE7D78YKlXpX6GWphxqTT210qK1Cf9XT2SKXbYvv5Bg+jaWD5ekdjU9y4+PsXRuZHRrdnZ5inVidAyCTXF+nGlVhpdjil2OE49hjICVh4qZlpibcDKug5KAlHOWqqR8rWCjl564oLFruIucaYGlz7+XoKe2wsIqxLzMxaxIuILIs6/JyLbZsdGF063Uu6vH2tXc79LZ1MLWS96t4JH/rryzhPWgAAIfkECQoAAAAsAAAAAIAADwAAAtWcP6CAt9zSilLRd2fEe4kPCk8IjqTonZnVsQ33arGLwLV8Kyeqnyb5C60gM2LO6MAlaUukwdbcBUspYFXYcla00KfSywRzv1vpldqzprHFoTv7bsOz5jUaUMer5vL+Mf7Hd5RH6HP2AdiUKLa41Tj1Acmjp0bJFuinKKiZyUhnaBd5OLnzSNbluOnZWQZqeVdIYhqWyop6ezoquTs6O0aLC5wrHErqGnvJibms3LzKLIYMe7xnO/yL7TskLVosqa1aCy3u3FrJbSwbHpy9fr1NfR4fUgAAIfkECQoAAAAsAAAAAIAADwAAAsqcP6CAt9zSilLRd2fEW7cnhKIAjmFpZla3fh7CuS38OrUR04p5Ljzp46kgMqLOaJslkbhbhfkc/lAjqmiIZUFzy2zRe5wGTdYQuKs9N5XrrZPbFu94ZYE6ms5/9cd7/T824vdGyIa3h9inJQfA+DNoCHeomIhWGUcXKFIH6RZZ6Bna6Zg5l8JnSamayto2WtoI+4jqSjvZelt7+URKpmlmKykM2vnqa1r1axdMzPz5LLooO326Owxd7Bzam4x8pZ1t3Szu3VMOdF4AACH5BAkKAAAALAAAAACAAA8AAAK/nD+ggLfc0opS0XdnxFs3/i3CSApPSWZWt4YtAsKe/DqzXRsxDqDj6VNBXENakSdMso66WzNX6fmAKCXRasQil9onM+oziYLc8tWcRW/PbGOYWupG5Tsv3TlXe9/jqj7ftpYWaPdXBzbVF2eId+jYCAn1KKlIApfCSKn5NckZ6bnJpxB2t1kKinoqJCrlRwg4GCs4W/jayUqamaqryruES2b72StsqgvsKlurDEvbvOx8mzgazNxJbD18PN1aUgAAIfkECQoAAAAsAAAAAIAADwAAArKcP6CAt9zSilLRd2fEWzf+ecgjlKaQWZ0asqPowAb4urE9yxXUAqeZ4tWEN2IOtwsqV8YkM/grLXvTYbV4PTZpWGYU9QxTxVZyd4wu975ZZ/qsjsPn2jYpatdx62b+2y8HWMTW5xZoSIcouKjYePeTh7TnqFcpabmFSfhHeemZ+RkJOrp5OHmKKapa+Hiyyokaypo6q1CaGDv6akoLu3DLmLuL28v7CdypW6vsK9vsE1UAACH5BAkKAAAALAAAAACAAA8AAAKjnD+ggLfc0opS0XdnxFs3/nkISI2icxokanVt+JoxC8G1fNOlm6tp1QNmZj6ikDcMrorBpBMJtT2lUdzUusNSt9qurvrlhr275VHMvI7XaXAbXTLLf3NjXUnP23/qN/n8d9cHyEZYpoe3p5jIOCjoFofoKAn5CGeZZaiJWcjp10mZuRkaSAq6OGmU2lhp+vk6iioay3rpSrs6mNsqa9tb+ntQAAA7AAAAAAAAAAAA); width: 128px; height: 15px; margin: auto; } mt9woicuo7pzgtxrsr203bq61au9zmx MediaWiki:Gadget-formWizard-core 8 24492 268729 2026-04-27T17:14:28Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'FormWizard üçin komponentler' 268729 wikitext text/x-wiki FormWizard üçin komponentler 8sq6ybw02k4rpmppyuenfxyhet76hfw MediaWiki:Gadget-formWizard-core.js 8 24493 268730 2026-04-27T17:15:16Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* ______________________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] | * | before...' 268730 javascript text/javascript /* ______________________________________________________________________________________ * | | * | === WARNING: GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] | * | before editing. | * |_____________________________________________________________________________________| * * See https://meta.wikimedia.org/wiki/Meta:FormWizard for usage and description. */ //<nowiki> var formsGadget = { 'createDialog' : function(){ var that = this; var dialogDict = { dialogClass: 'formsGadget', autoOpen: false, //title: that.formDict.config['dialog-title'], width: '495px', modal: true, closeOnEscape: true, resizable: false, draggable: false, }; var dialog = $('#formsDialogExpand'); if(dialog.length){ this.dialog = dialog.dialog(dialogDict); } else{ this.dialog = $('<div id="formsDialogExpand"></div>').dialog(dialogDict); } dialog.append('<div class="loading"></div>'); }, 'dialog' : null, 'openPanel': function(){ this.dialog.dialog('open'); }, 'openDialog' : function () { if (this.dialog === null){ this.createDialog(); } else{ this.dialog.dialog('open'); } }, 'cleanupDialog': function (){ if (this.dialog){ this.dialog.dialog('destroy'); } this.dialog = null; $('#formsDialogExpand').text(''); }, 'utilities' : { /* * Path to the gadget config file */ 'configPath' : 'MediaWiki:Gadget-formWizard', 'apiUrl' : '//en.wikipedia.org/w/api.php?callback=?', 'gadgetNamespace' : function(){ var grant = mw.config.get('wgTitle').replace(/ /g,'_'); return grant; }, /* * To detect the users default language */ 'userLanguage' : function(){ return mw.config.get('wgUserLanguage'); }, /* * To detect the language of the page */ 'contentLanguage' : function(){ return mw.config.get('wgContentLanguage'); }, /* * Removes leading/trailing spaces & user signature * ( It is added through the code) */ 'cleanupText' : function(text){ text = $.trim(text)+' '; var indexOf = text.indexOf('~~~~'); if ( indexOf == -1 ){ return text; } else{ return text.slice(0,indexOf)+text.slice(indexOf+4); } }, /* * The config files which can be translated with the help of the * translation tool generates the dict with the values having a * lot of space in the key value pairs. This function strips the * whitespace. */ 'stripWhiteSpace' : function(dict){ for (key in dict){ dict[key] = typeof(dict[key]) == 'object' ? this.stripWhiteSpace(dict[key]) : $.trim(dict[key]); } return dict; }, }, 'formElement' : { /* * Elements being supported * Small textbox * Large textbox * Checkbox list * Radio button list * Stepper list * Image/s * Dropdown * Link * Text */ 'hiddenInfoboxFields' : [], 'found' : false, 'timestamp' : 0, 'defaultTextBoxConfig': { 'type': 'smallTextBox', 'placeholder': 'Enter the text', 'title': 'Textbox', 'characterLength':100, 'mandatory':false, 'error-messageLength': 'Max length reached', 'error-notFilled': 'Mandatory field', 'value': '', 'parent': '', 'id': null, 'comment': '' }, 'elementContainer' : function(){ var div = document.createElement('div'); div.className = 'elementContainer'; return div; }, 'addDescription': function(dict,div){ for (key in dict){ if(key.indexOf('text') != -1){ this.addText(div,dict[key],'text'); delete dict[key]; } } return div; }, 'checkTitle' : function(string,exists,titleStem,type){ var that = this; //Url to the api var apiUrl = formsGadget.utilities.apiUrl; var title = titleStem + string; var searchDict = { 'action':'query', 'format':'json', 'titles':title, 'prop':'imageinfo' }; var timestamp = Date.now(); //console.log('String before ajax', string); return $.getJSON(apiUrl,searchDict,function(data){ var query = data['query']; var pages = data['query']['pages']; var pageId = Object.keys(pages); var pageExists = pageId != -1 ? true : false; var imageExists = pages[pageId]['imagerepository'] ? true : false; var value = 0; if (type == 'image'){ value = imageExists; } else{ value = pageExists; } if(that.timestamp < timestamp){ that.timestamp = timestamp; that.found = !(value ^ exists) ; //console.log('String ',string, 'found ',that.found); } }); }, 'inputList': function(type,list,title,dict,role){ var div = this.elementContainer(); div = this.addText(div,title,'title'); this.addDescription(dict,div); for (elem in list){ var label = document.createElement('div'); var input = document.createElement('input'); var key = list[elem]['key']; var value = list[elem]['value']; input.type = type; if (type == 'number'){ input.min = dict['min']; input.max = dict['max']; } input.value = value; input.setAttribute('data-add-to',dict['add-to']); if(role){ input.setAttribute('data-role',true); } input.className = 'inputListItem'; input.setAttribute('data-add-to-attribute',key); var descriptionText = key.replace(/_/g,' '); descriptionText = descriptionText.slice(0,1).toUpperCase() + descriptionText.slice(1); var description = document.createElement('span'); description.className = 'inputListItemDescription'; description.textContent = descriptionText; label.appendChild(input); label.appendChild(description); div.appendChild(label); } return div; }, 'createTextBoxConfig':function(defaultConfig,actualConfig){ var config = {}; for (key in defaultConfig){ actualConfig[key] = key in actualConfig? actualConfig[key] : defaultConfig[key]; if (key == 'mandatory' && (typeof(actualConfig[key]) == 'string')){ if (actualConfig[key] == 'true'){ actualConfig[key] = true; } else{ actualConfig[key] = false; } } } return actualConfig; }, 'textBox': function(dict,type,callback,element){ var config = this.createTextBoxConfig(this.defaultTextBoxConfig,dict); var className = type == 'small'? 'smallTextBox': 'largeTextBox'; var div = this.elementContainer(); div = this.addText(div,config['title'],'title'); this.addDescription(dict,div); if (type == 'large'){ var input = document.createElement('textarea'); } else{ var input = document.createElement('input'); } //cleanup if(dict['visibility'] == 'hidden'){ div.style['display'] = 'none'; input.value = dict['value']; } //Cleanup if('page-title' in dict){ input.setAttribute('page-title',true); } if (dict['id']){ input.id = dict['id']; } input.setAttribute('type','text'); input.setAttribute('class',className); input.setAttribute('placeholder',config['placeholder']); input.setAttribute('maxlength',config['characterLength']); input.setAttribute('data-mandatory',config['mandatory']); input.setAttribute('data-comment',config['comment']); input.setAttribute('data-add-to',config['add-to']); var conditionalAttr = config['add-to'] == 'infobox' ? config['infobox-param'] : config['section-header']; input.setAttribute('data-add-to-attribute',conditionalAttr); var that = this; /* Word limit */ $(input).on('change keyup paste',function(){ /* Checking if link/file/page exists */ var inputTextBox = this; var enteredString = $(this).val(); if(!enteredString && !dict['mandatory']){ $('#formsDialogExpand [elemType="button"]').trigger('enableButtons'); $(inputTextBox).parent().removeClass('entrySatisfying entryNotSatisfying'); that.timestamp = Date.now(); that.found = true; } else{ if( 'validate' in dict && enteredString){ var exists = dict['validate'] == 'exists' ? 1:0; var titleStem = 'image' in dict ? '' : that.formDict.config['page-home']; $.when(that.checkTitle(enteredString,exists,titleStem,dict['type'])).then(function(){ //Cleanup & remove redundant code $(inputTextBox).removeClass('entrySatisfying entryNotSatisfying'); $(inputTextBox).addClass(that.found ? 'entrySatisfying' : 'entryNotSatisfying'); $(inputTextBox).parent().removeClass('entrySatisfying entryNotSatisfying'); $(inputTextBox).parent().addClass(that.found ? 'entrySatisfying' : 'entryNotSatisfying'); if (that.found){ $('#formsDialogExpand [elemType="button"]').trigger('enableButtons'); if(typeof(callback) === 'function' && that.found){ //Api url var apiUrl = formsGadget.utilities.apiUrl; $.getJSON(apiUrl,{'action':'parse', 'format':'json', 'text':'[['+enteredString+']]' },function(data){ //console.log(data['parse']['text']['*']); var src = $('<div>').html(data['parse']['text']['*']).find('img').attr('src'); if(src){ callback(element, src); } }); } } else{ $('#formsDialogExpand [elemType="button"]').trigger('disableButtons'); } }); } } }); //To show validation inputElementWrapper = document.createElement('span'); $(inputElementWrapper).addClass('inputElementWrapper'); if('validate' in dict){ $(inputElementWrapper).addClass('validationContainer'); } if(dict['mandatory']){ $(inputElementWrapper).addClass('mandatoryContainer'); } inputElementWrapper.appendChild(input); div.appendChild(inputElementWrapper); return div; }, 'smallTextBox': function (dict,callback,element) { return this.textBox(dict,'small',callback,element); }, 'largeTextBox': function (dict,callback,element) { return this.textBox(dict,'large',callback,element); }, 'checkboxList': function (dict) { var list = dict['choiceList']; var hidden = dict['hidden']; if('hidden' in dict){ this.hiddenInfoboxFields = this.hiddenInfoboxFields.concat(dict['hidden']); } return this.inputList('checkbox',list,dict['title'],dict); }, 'addText': function(container,text,type){ var textHolder = $('<p>'); textHolder.text(text); if (type == 'title'){ textHolder.addClass('title'); } else if (type == 'text'){ textHolder.addClass('text'); } else{ textHolder.addClass(type); } container.appendChild(textHolder[0]); return container; }, 'text': function(dict){ var textHolder = $('<p>'); return textHolder.text(dict['string'])[0]; }, 'stepperList': function (dict) { var list = dict['choiceList']; if('hidden' in dict){ this.hiddenInfoboxFields = this.hiddenInfoboxFields.concat(dict['hidden']); } dict['min'] = 0; if(!('max' in dict)){ dict['max'] = 9; } return this.inputList('number',list,dict['title'],dict,true); }, 'dropdownList': function(dict){ var div = this.elementContainer(); div = this.addText(div,dict['title'],'title'); this.addDescription(dict,div); var values = dict['values']; var select = document.createElement('select'); select.setAttribute('class','dropdown'); select.setAttribute('data-placeholder',dict['placeholder']); select.setAttribute('data-add-to',dict['add-to']); select.setAttribute('data-add-to-attribute',dict['infobox-param']); var option; for (elem in values){ option = $('<option>').attr('value',values[elem]).text(values[elem]); select.appendChild(option[0]); } div.appendChild(select); return div; }, 'link': function(dict){ var link = document.createElement('a'); link.href = 'href' in dict? dict['href'] : '//commons.wikimedia.org/wiki/Main_Page'; link.target = '_blank'; var innerText = 'link' in dict? dict['link'] : 'Search Wikimedia Commons for an image'; $(link).text(innerText); return link; }, 'image': function (dict) { var url = dict['url']; var text = dict['title']; dict['add-to'] = 'infobox'; dict['infobox-param'] = 'image'; dict['validate'] = 'exists'; //cleanup dict['placeholder'] = 'placeholder' in dict ? dict['placeholder'] : 'File:Test.png'; var div = this.elementContainer(); this.addText(div,dict['title'],'title'); this.addDescription(dict,div); var img = document.createElement('img'); img.src = url; dict['title'] = 'imageTitleBox' in dict ? dict['imageTitleBox'] : 'Enter the file name'; //cleanup dict['image'] = true; var textbox = this.smallTextBox(dict,function(elem,src){ img.src = src; },img); div.appendChild(img); div.appendChild(textbox); var commonsLink = this.link(dict); div.appendChild(commonsLink); return div; }, 'button': function(type,text){ var a = document.createElement('input'); a.type='submit'; a.setAttribute('elemType','button'); if(type == 'cancel' || type == 'back'){ a.className = 'mw-ui-button cancel mw-ui-quiet'; } else { a.className = 'mw-ui-button mw-ui-constructive'; } a.value = text; $(a).on('disableButtons',function(){ $(this).attr('disabled',true); }); $(a).on('enableButtons',function(){ $(this).attr('disabled',false); }); return a; }, 'cancelButton': function(dict){ var button = this.button('cancel',dict['title']); button.onclick = function(){ formsGadget.dialog.dialog('close'); }; return button; }, 'doneButton': function(dict){ var that = this; var button = this.button('done',dict['title']); button.onclick = function(){ that.validateForm(); }; return button; }, 'nextButton': function(dict){ var button = this.button('next',dict['title']); var that = this; button.onclick = function(){ $('#formsDialogExpand [step]').hide(); $('#formsDialogExpand'+' #'+dict['step']).next().show(); }; return button; }, 'backButton': function(dict){ var button = this.button('back',dict['title']); var that = this; button.onclick = function(){ $('#formsDialogExpand [step]').hide(); $('#formsDialogExpand'+' #'+dict['step']).prev().show(); }; return button; }, 'validateForm': function(){ var counter = 0; var firstElem; $('#formsDialogExpand [data-mandatory="true"]').each(function(){ var elem = $(this); if(!elem.val()){ if (counter == 0){ firstElem = elem; } counter++; elem.addClass('mandatoryInput'); } }); //Add mandatory filed Event & styling if(firstElem){ $('#formsDialogExpand [step]').hide(); while(true){ if (firstElem.attr('step')){ firstElem.show(); break; } firstElem = firstElem.parent(); } } else{ if (formsGadget.type == 'create'){ this.createWikiPage(); } else{ this.modifyWikiPage(); } } }, 'infoboxString': '', 'remainingSectionString': '', 'extractInfobox' : function(markup, infoboxTemplate){ var startIndex = markup.indexOf('{{' + infoboxTemplate); var counter = 0; var endIndex = 0; for (i=startIndex;i<markup.length;i++){ if(markup[i] == '}' && markup[i+1] == '}'){ counter++; } if(markup[i] == '{' && markup[i+1] == '{'){ counter--; } if(counter == 0){ var endIndex = i+2; break; } } if (counter != 0){ return ''; } var infobox = { 'infobox' : markup.slice(startIndex,endIndex), 'before' : markup.slice(0,startIndex), 'after' : markup.slice(endIndex), }; return infobox; }, 'infoboxObjectify': function(infoboxString){ var paramRe = /( )*\|( )*[A-Za-z0-9_]+( )*=/gi; var units = infoboxString.split('\n'); var infobox = []; var infoboxParams = {}; var parts,line,param,value; for (unit in units){ line = units[unit]; if(line.search(paramRe) != -1){ parts = line.split('='); param = $.trim(parts[0].replace('|','')); value = $.trim(parts[1]); //infoboxParams[param] = value; infobox.push({'param': param, 'value': value}); } else{ infobox.push($.trim(line)); } } return infobox; }, 'modifyInfoboxParam': function(infobox,param,newValue){ var flag = true; for (elem in infobox){ if(typeof(infobox[elem]) == 'object' && infobox[elem]['param'] == param){ infobox[elem]['value'] = newValue; flag = false; } } if (flag){ infobox.splice(-1,0,{'param':param, 'value':newValue}); } return infobox; }, 'stringifyInfobox' : function(infobox){ var infoboxString = ''; for (elem in infobox){ if (typeof(infobox[elem]) == 'object'){ if(infobox[elem]['value']){ infoboxString = infoboxString + '|' + infobox[elem]['param'] + '=' + infobox[elem]['value'] + '\n'; } else{ infoboxString = infoboxString + '|' + infobox[elem]['param'] + '=' + '\n'; } } else{ infoboxString = infoboxString + infobox[elem] +'\n'; } } return infoboxString; }, 'createEditSummary' : function(title,subcomment){ var summary = ''; var formsConfig = formsGadget.formDict['config']; if (formsConfig['edit-comment-prefix']){ summary = formsConfig['edit-comment-prefix'] + ' '; } else{ summary = formsConfig['edit-comment-default'] + ' '; } if(subcomment){ summary = summary + title + ' (' + subcomment + ') '; } else{ summary = summary + title + ' '; } if (formsConfig['edit-comment-suffix']){ summary = summary + formsConfig['edit-comment-suffix']; } return summary; }, 'modifyWikiPage' : function(){ var that = this; var infobox = ''; var infoboxString = ''; var sections = ''; var api = new mw.Api(); var roots = this.wikiSectionTree.roots; for (elem in roots){ //console.log('---------'); this.wikiSectionTree.traverse([roots[elem]],1,function(id){ var elem = $('#formsDialogExpand #'+id); value = elem.val() ? elem.val() : ''; var heading = elem.attr('data-add-to-attribute'); return { 'heading': heading, 'value': value}; }); } //Disabling buttons on ajax post $('#formsDialogExpand [elemType="button"]').trigger('disableButtons'); //refractor hardcoding '/Toolkit' var title = mw.config.get('wgPageName').replace('/Toolkit',''); //Getting the infobox var gettingInfobox = api.get({ 'format':'json', 'action':'query', 'prop':'revisions', 'rvprop': 'content', 'rvslots': 'main', 'rvsection':0, 'titles': title, }).then(function(result){ var pages = result.query.pages; var key = Object.keys(result.query.pages)[0]; var content = pages[key]['revisions'][0]['slots']['main']['*']; var infoboxTemplate = formsGadget.formDict.config['infobox'] ? formsGadget.formDict.config['infobox'] : 'Probox/Idealab'; var elements = that.extractInfobox(content, infoboxTemplate); var infobox = that.infoboxObjectify(elements['infobox']); var before = elements['before']; var after = elements['after']; $('#formsDialogExpand [data-add-to]').each(function(index,elem){ var elem = $(elem); if(elem.attr('data-add-to') == 'infobox' ){ if(elem.attr('type') == 'checkbox'){ if (elem.is(':checked')){ infobox = that.modifyInfoboxParam(infobox,elem.attr('data-add-to-attribute'),elem.val()); } else{ infobox = that.modifyInfoboxParam(infobox,elem.attr('data-add-to-attribute'),null); } } else if(elem.attr('data-role')){ for (var i=0;i<elem.val(); i++){ infobox = that.modifyInfoboxParam(infobox,elem.attr('data-add-to-attribute')+(i+1), null); } } else{ infobox = that.modifyInfoboxParam(infobox,elem.attr('data-add-to-attribute'),elem.val()); } } }); /* * infobox entries */ var hiddenFields = that.hiddenInfoboxFields; for(entry in hiddenFields){ infobox = infobox.push({'param':hiddenFields[entry]['key'],'value':hiddenFields[entry]['value']}); } modifiedSection = before + $.trim(that.stringifyInfobox(infobox)) + after; var formsConfig = formsGadget.formDict['config']; api.post({ 'action' : 'edit', 'title' : title, 'text' : modifiedSection, 'summary' : that.createEditSummary(title,'editing infobox parameters'), 'section': 0, 'watchlist':'watch', 'token' : mw.user.tokens.get('csrfToken') }).then(function(){ var newSections = '\n' + that.wikiSectionTree.sections; api.post({ 'action' : 'edit', 'title' : title, 'summary' : that.createEditSummary(title,'editing section'), 'appendtext':newSections, 'watchlist':'watch', 'token' : mw.user.tokens.get('csrfToken') }).then(function(){ // Redirecting to idea page //console.log('Successfully Added new sections & modified the infobox'); var postEditMessage = formsGadget.formDict['config']['post-edit']; //Cleanup formsGadget.dialog.dialog('close'); mw.cookie.set('formsGadgetNotify',postEditMessage); window.location.href = location.origin + '/wiki/' + title; }); }); }); }, 'createWikiPage' : function(){ var that = this; var infobox = ''; var page = ''; var api = new mw.Api(); var pageTitle = $('#formsDialogExpand [page-title]').val(); var roots = this.wikiSectionTree.roots; for (elem in roots){ //console.log('---------'); this.wikiSectionTree.traverse([roots[elem]],1,function(id){ var elem = $('#formsDialogExpand #'+id); value = elem.val() ? elem.val() : ''; var heading = elem.attr('data-add-to-attribute'); var comment = elem.attr('data-comment'); return { 'heading': heading, 'value': value, 'comment': comment }; }); } $('#formsDialogExpand [data-add-to]').each(function(index,elem){ var elem = $(elem); if(elem.attr('data-add-to') == 'section' ){ //var value = elem.val() ? elem.val() : ''; //var section = '==' + elem.attr('data-add-to-attribute') + '==' + '\n' + elem.val() + '\n'; //page = page + section; } else{ //Cleanup & Simplify if (elem.attr('data-role')){ for (var i=0;i<elem.val(); i++){ infobox = infobox + '|'+ elem.attr('data-add-to-attribute') + (i+1) + '=\n'; } } else if(elem.attr('type') == 'checkbox'){ if (elem.is(':checked')){ infobox = infobox + '|'+ elem.attr('data-add-to-attribute') + '=' + elem.val() + '\n'; } else{ infobox = infobox + '|'+ elem.attr('data-add-to-attribute') + '=\n'; } } //Fix this hardcoding more elegantly else if(elem.attr('data-add-to-attribute') == 'image'){ var image = elem.val() ? elem.val() : elem.attr('placeholder'); infobox = infobox + '|'+ elem.attr('data-add-to-attribute') + '=' + image + '\n'; } else{ infobox = infobox + '|'+ elem.attr('data-add-to-attribute') + '=' + elem.val() + '\n'; } } }); /* * infobox entries */ var hiddenFields = this.hiddenInfoboxFields; for(entry in hiddenFields){ infobox = infobox + '|' + hiddenFields[entry]['key'] + '=' + hiddenFields[entry]['value'] + '\n'; } //Hardcoding creator/timestamp infobox = infobox + '|' + 'timestamp = ~~~~~' + '\n' ; infobox = infobox + '|' + 'creator = ' + mw.user.getName() + '\n' ; var probox = this.formDict.config['infobox'] ? this.formDict.config['infobox'] : 'Probox/Idealab'; infobox = '{{' + probox + '\n' + infobox + '}} \n'; page = infobox + this.wikiSectionTree.sections; /* * Creating a new page * */ var title = formsGadget.formDict['config']['page-home'] + pageTitle; //Disabling buttons on ajax post $('#formsDialogExpand [elemType="button"]').trigger('disableButtons'); api.post({ 'action': 'edit', //Cleanup 'title': title, 'summary': that.createEditSummary(title), 'text': page, 'watchlist':'watch', token: mw.user.tokens.get('csrfToken') }).then(function () { //Creating Idea Toolkit var formsConfig = formsGadget.formDict['config']; var toolkit = formsConfig['toolkit-name']; var toolkitContent = '{{' + formsConfig['toolkit-template'] + '}}'; var createToolkit = true; if (toolkit && toolkitContent){ var toolkitTitle = title + '/' + toolkit; var summary = 'Adding the toolkit for ' + title; createToolkit = api.post({ 'action': 'edit', //Cleanup 'title': toolkitTitle, 'summary': summary, 'text': toolkitContent, 'watchlist':'watch', token: mw.user.tokens.get('csrfToken') }); } // Redirecting to idea page //console.log('Successfully created new page'); $.when(createToolkit).then(function(){ formsGadget.dialog.dialog('close'); var postEditMessage = formsGadget.formDict['config']['post-edit']; mw.cookie.set('formsGadgetNotify',postEditMessage); window.location.href = location.origin + '/wiki/' + title; },function(){ $('#formsDialogExpand [elemType="button"]').trigger('enableButtons'); }); },function(){ $('#formsDialogExpand [elemType="button"]').trigger('enableButtons'); }); //console.log(title,page); } }, 'createForm' : function(formDict){ //cleanup fixing the fallbacks if( !formDict.config['page-home'].match(/\/$/) ){ formDict.config['page-home'] = formDict.config['page-home'] + '/'; } this.formDict = formDict; this.formElement.formDict = formDict; this.formElement.wikiSectionTree = this.wikiSectionTree; var dialogInternal = document.createElement('div'); //User not logged in if (! mw.user.getName()){ var errorMessage = formDict['config']['error-not-logged-in']; var errorDiv = document.createElement('div'); errorDiv.className = 'mw-ui-vform'; this.formElement.addText(errorDiv,errorMessage,'error'); dialogInternal.appendChild(errorDiv); } var counter = 0; for (step in formDict){ if (step != 'config'){ counter++; var stepDict = formDict[step]; var panel = document.createElement('div'); panel.id = step; if(counter != 1){ panel.style['display'] = 'none'; } panel.setAttribute('step',step); for (elem in stepDict){ elemDict = stepDict[elem]; elemDict['elem'] = elem; elemDict['step'] = step; elemDict['id'] = elem; panel.appendChild(this.formElement[elemDict.type](elemDict)); //Creating the hierarchial structure of the sections & subsections if (elemDict['add-to'] == 'section' ){ var parent = 'parent' in elemDict ? elemDict['parent'] : null; var node = elem; if(parent){ this.wikiSectionTree.addLink(parent,node); } else{ this.wikiSectionTree.addLink(node); } } } dialogInternal.appendChild(panel); } } $('#formsDialogExpand').append(dialogInternal); $('.formsGadget .dropdown').chosen({ disable_search: true, width: '50%', }); return true; }, 'Tree' : function(){ var rootList = {}; var nodeList = {}; this.sections = ''; this.roots = rootList; var Node = function(parent,child,id){ this.parent = parent; this.id = id; this.child = child; }; var getNode = function(id){ if (id in nodeList){ return nodeList[id]; } else{ var node = new Node(null,null,id); nodeList[id] = node; rootList[id] = node; return node; } }; this.addLink = function(startId,endId){ if (endId){ var startNode = getNode(startId); var endNode = getNode(endId); endNode.parent = startNode; if (startNode.child){ startNode.child.push(endNode); } else{ startNode.child = [endNode]; } delete rootList[endNode.id]; } else{ getNode(startId); } }; var sectionLevel = function(indent){ var string = ''; for (var i=0;i<indent;i++){ string = string + '='; } return string; }; this.traverse = function(rootList,level,callback){ if(!rootList){ return; } level++; var wikiSectionHeaderMarkup = sectionLevel(level); for (elem in rootList){ var root = rootList[elem]; var sectionValues = callback(root.id); var section = wikiSectionHeaderMarkup + sectionValues['heading'] + wikiSectionHeaderMarkup + '\n' ; var section = section + ( sectionValues['comment'] ? sectionValues['comment'] + '\n' : '' ) + sectionValues['value'] + '\n'; this.sections = this.sections + section; root = root.child; this.traverse(root,level,callback); } }; } }; $(function() { (function(){ var api = new mw.Api(); var utility = formsGadget.utilities; //Retrieving the post edit feedback if any var postEditMessage = mw.cookie.get('formsGadgetNotify'); if (postEditMessage){ //clearing the cookie mw.cookie.set('formsGadgetNotify', null); //displaying the post edit message mw.notify(postEditMessage,{autoHide:false}); } $('.wp-formsGadget').click(function(e){ e.preventDefault(); formsGadgetNamespace = utility.gadgetNamespace(); formsGadgetType = $(this).attr('data-type') || 'Idea'; formsGadgetMode = $(this).attr('data-mode') || 'create'; formsGadget.cleanupDialog(); formsGadget.openDialog(); formsGadget.openPanel(); $('#formsDialogExpand .loading').show(); var configFullPath = utility.configPath+'/'+formsGadgetNamespace+'/'+formsGadgetType; var configUrl = '//en.wikipedia.org/w/index.php?title='+encodeURIComponent(configFullPath)+'&action=raw&ctype=text/javascript'; var api = new mw.Api(); var promise = api.get({ 'format':'json', 'formatversion': 2, 'action':'query', 'prop':'revisions', 'rvprop': 'contentmodel', 'rvsection':0, 'titles': configFullPath, }); //Get the config for the language above $.when(jQuery.getScript(configUrl), promise).then(function(text, result){ if ( result[0].query.pages[0].revisions[0].contentmodel == 'javascript' ) { var config = utility.stripWhiteSpace(formsGadgetConfig[formsGadgetMode]); formsGadget['formDict'] = config; //Cleanup $('.formsGadget .ui-dialog-title').text(config.config['dialog-title']); formsGadget['wikiSectionTree'] = new formsGadget.Tree(); formsGadget.openDialog(); formsGadget.createForm(config); formsGadget.type = formsGadgetMode; formsGadget.openDialog(); $('#formsDialogExpand .loading').hide(); } }); }); })(); }); // </nowiki> k0frz5eqqwdl2z48cp2msapu6tmgpuf MediaWiki:Gadget-Prosesize 8 24494 268731 2026-04-27T17:19:58Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[Wikipedia:Prosesize|Prosesize]]: Sahypadaky sözleriň ululygyny we sanyny görkezmek üçin gurallar gutusy baglanyşygyny goşmak' 268731 wikitext text/x-wiki [[Wikipedia:Prosesize|Prosesize]]: Sahypadaky sözleriň ululygyny we sanyny görkezmek üçin gurallar gutusy baglanyşygyny goşmak rq8t7ts48y157rxpwjrtn0p3yvcv3xc MediaWiki:Gadget-Prosesize.css 8 24495 268732 2026-04-27T17:20:34Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268732 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * */ .prosesize-highlight { background-color: yellow; color:black; } .prosesize-portlet-link-edit-mode:first-child { color:black; } .prosesize-special-template { background-color: none; } #document-size { margin-top: 0.5em; } 47x54n4r9d228bkkwmkmjn0vu9g8cdb MediaWiki:Gadget-Prosesize.js 8 24496 268733 2026-04-27T17:21:48Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268733 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * */ /** * Prosesize * Documentation at en.wikipedia.org/wiki/Wikipedia:Prosesize * Rewrite of [[User:Dr_pda/prosesize.js]]. */ 'use strict'; ( function () { function sizeFormatter( size ) { var nbsp = "\xA0"; // Equivalent to &nbsp; if ( size > 10240 ) { return ( Math.round( size / 1024 ) + nbsp + 'kB' ); } else { return ( size + nbsp + 'B' ); } } function sizeElement( id, text, size, extraText ) { return $( '<li>' ) .prop( 'id', id ) .append( $( '<b>' ).text( text ), document.createTextNode( ' ' + sizeFormatter( size ) + ( extraText || '' ) ) ); } function getRevisionSize( proseValue ) { var Api = new mw.Api(); function appendResult( size ) { var wikiValue = sizeElement( 'wiki-size', 'Wiki text:', size ); proseValue.before( wikiValue ); } if ( mw.config.get( 'wgAction' ) === 'submit' ) { // Get size of text in edit box // eslint-disable-next-line no-jquery/no-global-selector appendResult( $( '#wpTextbox1' ).textSelection( 'getContents' ).length ); } else if ( mw.config.get( 'wgIsArticle' ) ) { // Get revision size from API Api.get( { action: 'query', prop: 'revisions', rvprop: 'size', revids: mw.config.get( 'wgRevisionId' ), formatversion: 2 } ).then( function ( result ) { appendResult( result.query.pages[ 0 ].revisions[ 0 ].size ); } ); } } function getFileSize( proseHtmlValue ) { // HTML document size not well defined for preview mode or section edit if ( mw.config.get( 'wgAction' ) !== 'submit' ) { $.get( location ).then( function ( result ) { var fsize = sizeElement( 'total-size', 'HTML document size:', result.length ); proseHtmlValue.before( fsize ); } ); } } function getLength( id ) { var i; var textLength = 0; for ( i = 0; i < id.childNodes.length; i++ ) { if ( id.childNodes[ i ].nodeType === Node.TEXT_NODE ) { textLength += id.childNodes[ i ].nodeValue.length; } else if ( id.childNodes[ i ].nodeType === Node.ELEMENT_NODE && ( id.childNodes[ i ].id === 'coordinates' || id.childNodes[ i ].className.indexOf( 'emplate' ) !== -1 ) ) { // special case for {{coord}} and {{fact}}-like templates // Exclude from length, and don't set background yellow id.childNodes[ i ].className += ' prosesize-special-template'; } else if (id.childNodes[ i ].tagName !== 'STYLE') { // Exclude style tags textLength += getLength( id.childNodes[ i ] ); } } return textLength; } function getRefMarkLength( id, html ) { var i; var textLength = 0; for ( i = 0; i < id.childNodes.length; i++ ) { if ( id.childNodes[ i ].nodeType === Node.ELEMENT_NODE && id.childNodes[ i ].className === 'reference' ) { textLength += ( html ) ? id.childNodes[ i ].innerHTML.length : getLength( id.childNodes[ i ] ); } } return textLength; } function main() { var prosePromise, proseValue, refValue, refHtmlValue, proseHtmlValue; // eslint-disable-next-line no-jquery/no-global-selector var parserOutput = $( '#mw-content-text .mw-parser-output' ); // eslint-disable-next-line no-jquery/no-global-selector var prevStats = $( '#document-size-stats' ); // eslint-disable-next-line no-jquery/no-global-selector var prevHeader = $( '#document-size-header' ); var proseSize = 0; var proseSizeHtml = 0; var refmarksize = 0; var refmarkSizeHtml = 0; var wordCount = 0; var refSize = 0; var refSizeHtml = 0; var header = $( '<span>' ) .prop( 'id', 'document-size-header' ) .html( 'Document statistics <small>(<a href="//en.wikipedia.org/wiki/Wikipedia:Prosesize">more information</a>)</small>:' ); var output = $( '<ul>' ) .prop( 'id', 'document-size-stats' ); var combined = $( '<div>' ) .prop( 'id', 'document-size' ) .append( header, output ); if ( parserOutput.length === 0 ) { return; } if ( prevStats.length ) { // If statistics already exist, turn them off and remove highlighting prevStats.remove(); prevHeader.remove(); parserOutput.children( 'p' ).removeClass( 'prosesize-highlight' ); } else { // Use prosesize API to get a more accurate prose size account // The calculations below are left in for the highlighting prosePromise = $.getJSON( 'https://prosesize.toolforge.org/api/' + mw.config.get( 'wgServerName' ) + '/' + encodeURIComponent( mw.config.get( 'wgPageName' ) ) + '?revision=' + mw.config.get( 'wgRevisionId' ) ); // Calculate prose size and size of reference markers ([1] etc) parserOutput.children( 'p' ).each( function () { $( this ).addClass( 'prosesize-highlight' ); proseSize += getLength( this ); proseSizeHtml += this.innerHTML.length; refmarksize += getRefMarkLength( this, false ); refmarkSizeHtml += getRefMarkLength( this, true ); wordCount += this.innerHTML.replace( /(<([^>]+)>)/ig, '' ).split( ' ' ).length; } ); // Calculate size of references (i.e. output of <references/>) parserOutput.find( 'ol.references' ).each( function () { refSize = getLength( this ); refSizeHtml = this.innerHTML.length; } ); proseSize -= refmarksize; function show_output() { proseValue = sizeElement( 'prose-size', 'Prose size (text only):', proseSize, ' (' + wordCount + ' words) "readable prose size"' ); refValue = sizeElement( 'ref-size', 'References (text only):', refSize + refmarksize ); refHtmlValue = sizeElement( 'ref-size-html', 'References (including all HTML code):', refSizeHtml + refmarkSizeHtml ); proseHtmlValue = sizeElement( 'prose-size-html', 'Prose size (including all HTML code):', proseSizeHtml - refmarkSizeHtml ); output.append( proseHtmlValue, refHtmlValue, proseValue, refValue ); parserOutput.prepend( combined ); getFileSize( proseHtmlValue ); getRevisionSize( proseValue ); } // Add the relevant outputs once we have fetched the prose size. prosePromise.then( function( data ) { if ( mw.config.get( 'wgIsArticle' ) ) { // Tool doesn't work on previews proseSize = data.prose_size; wordCount = data.word_count; } show_output(); }, // If tool is down fallback to our prose count show_output ); } } if ( !mw.config.get( 'wgCanonicalSpecialPageName' ) ) { $.ready.then( function () { /** * Depending on whether in edit mode or preview/view mode, * show the approppiate response upon clicking the portlet link */ var func, $portlet, notEnabled = false; if ( mw.config.get( 'wgAction' ) === 'edit' || ( mw.config.get( 'wgAction' ) === 'submit' && document.getElementById( 'wikiDiff' ) ) ) { notEnabled = true; func = function () { mw.notify( 'You need to preview the text for the prose size script to work in edit mode.' ); }; } else if ( [ 'view', 'submit', 'historysubmit', 'purge' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) { func = main; } if ( func ) { $portlet = $( mw.util.addPortletLink( 'p-tb', '#', 'Page size', 't-page-size', 'Calculate page and prose size' ) ); if ( notEnabled ) { $portlet.addClass( 'prosesize-portlet-link-edit-mode' ); } $portlet.on( 'click', function ( e ) { e.preventDefault(); if ( window.ve && ve.init && ve.init.target && ve.init.target.active ) { mw.notify( 'Prosesize does not work with the Visual Editor.' ); } else { func(); } } ); } } ); } }() ); dg9upr2nz8c1otkiyznhr826umlexqi MediaWiki:Gadget-find-archived-section 8 24497 268734 2026-04-27T17:24:30Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[User:SD0001/find-archived-section|find-archived-section]]: arhiwlenen bölüme onuň döwülen baglanyşygyny yzarlandan soň aňsatlyk bilen barmak' 268734 wikitext text/x-wiki [[User:SD0001/find-archived-section|find-archived-section]]: arhiwlenen bölüme onuň döwülen baglanyşygyny yzarlandan soň aňsatlyk bilen barmak 952hhw01fbnnur1g4ixmzci3ghyey15 MediaWiki:Gadget-find-archived-section.js 8 24498 268735 2026-04-27T17:25:16Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/** _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |________________________________________________...' 268735 javascript text/javascript /** _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Gadget to navigate to an archived section after following its broken link. * * Author: SD0001 * Documentation: [[User:SD0001/find-archived-section]] * */ // START EDITING HERE FOR LOCALISATION // Messages: translate these strings if porting to a non-English language wiki mw.messages.set({ "fas-finding": 'Looks like the discussion "$1" has been archived. Finding archived discussion...', "fas-exact-match": 'Looks like the discussion "$1" has been archived. <b><a href="$2">Click to see archived discussion</a></b> <small>(<a href="$3">or search in archives</a>)</small>', "fas-inexact-match": 'Looks like the discussion "$1" has been archived. <a href="$2">Click to search in archives</a>.', "fas-no-results": 'No search results found for section "$1" in archives. It may have been removed or renamed, or you may have followed a malformed link.', }); var config = { // Function to introduce arbitrary changes to prefix. // Used here as archive page names used for admin noticeboards on enwiki are unusual prefixNormaliser: function(prefix) { switch (prefix) { case "Wikipedia:Administrators' noticeboard/Incidents": return "Wikipedia:Administrators' noticeboard/IncidentArchive"; case "Wikipedia:Administrators' noticeboard/Edit warring": return "Wikipedia:Administrators' noticeboard/3RRArchive"; case "Wikipedia:Administrators' noticeboard": return "Wikipedia:Administrators' noticeboard/Archive"; case "Wikipedia:Reference desk/Science": return "Wikipedia:Reference desk/Archives/Science"; case "Wikipedia:Reference desk/Miscellaneous": return "Wikipedia:Reference desk/Archives/Miscellaneous"; case "Wikipedia:Reference desk/Mathematics": return "Wikipedia:Reference desk/Archives/Mathematics"; case "Wikipedia:Reference desk/Language": return "Wikipedia:Reference desk/Archives/Language"; case "Wikipedia:Reference desk/Humanities": return "Wikipedia:Reference desk/Archives/Humanities"; case "Wikipedia:Reference desk/Entertainment": return "Wikipedia:Reference desk/Archives/Entertainment"; case "Wikipedia:Reference desk/Computing": return "Wikipedia:Reference desk/Archives/Computing"; case "Wikipedia:Reference desk": return "Wikipedia:Reference desk/Archives"; case "Wikipedia:Teahouse": return "Wikipedia:Teahouse/Questions"; default: return prefix; } } }; // STOP EDITING HERE FOR LOCALISATION $(function() { var addsection = document.getElementById('ca-addsection'); var correctNs = mw.config.get('wgNamespaceNumber') % 2 === 1 || mw.config.get('wgNamespaceNumber') === 4; var minerva = mw.config.get('skin') === 'minerva'; // Show only on discussion pages (pages with "add section" button) // On minerva skin (which doesn't use the add section button) show on all talk & project space pages if (!addsection && (!correctNs || !minerva)) { return; } var sectionName = decodeURIComponent( window.location.hash.slice(1) // to remove the leading # .replace(/_/g, ' ') ); // For anchor-encoded (UTF-8 percent encoding but with % replaced by a period (.) ), try to undo the encoding. // For some strange reason, MediaWiki doesn't encode . itself, because of this the encoding process isn't // exactly reversible. But this should work for the vast majority of cases. var sectionNameDotDecoded = decodeURIComponent(sectionName.replace(/\.([0-9A-F]{2})/g, '%$1')); if (!sectionName || // no section name in URL sectionName.indexOf('/media/') === 0 || // URLs used by MediaViewer /^c-/.test(sectionName) || //URLs used by DiscussionTools /^\d{12} /.test(sectionName) || // URLs used by convenientDiscussions /^noticeApplied-/.test(sectionName) || // URLs used by RedWarn document.getElementById(sectionName.replace(/ /g, '_')) !== null) { // section exists on page return; } var escapeQuotes = function(str) { return str.replace(/"/g, '\\"'); }; $('#mw-content-text').before( $('<div>') .text(mw.msg('fas-finding', sectionName)) .addClass('archived-section-prompt') .css({ 'font-size': '90%', 'padding': '0 0 10px 20px' }) ); var prefix = mw.config.get('wgPageName').replace(/_/g, ' '); // Apply normalisation for for admin noticeboards if (typeof config.prefixNormaliser === 'function') { prefix = config.prefixNormaliser(prefix); } var searchQuery = sectionNameDotDecoded === sectionName ? '"' + escapeQuotes(sectionName) + '" prefix:"' + prefix + '"' : '"' + escapeQuotes(sectionName) + '" OR "' + escapeQuotes(sectionNameDotDecoded) + '" prefix:"' + prefix + '"'; mw.loader.using(['mediawiki.util', 'mediawiki.api']).then(function() { return new mw.Api({ ajax: { headers: { 'Api-User-Agent': 'w:en:MediaWiki:Gadget-find-archived-section.js' } } }).get({ action: 'query', list: 'search', srsearch: searchQuery, srprop: 'sectiontitle', srsort: 'create_timestamp_desc', // list more recent archives first srlimit: '20' }); }).then(function(json) { if (!json || !json.query || !json.query.search) { return; } var results = json.query.search; if (results.length === 0) { $('.archived-section-prompt').html(mw.msg('fas-no-results', mw.html.escape(sectionName))); } else { var pageTitle, sectionNameFound; // will either be sectionName or sectionNameDotDecoded // obtain the the first exact section title match (which would be from the most recent archive) // this loop iterates over just one item in the vast majority of cases for (var i in results) { var result = results[i]; if ( result.sectiontitle && (result.sectiontitle === sectionName || result.sectiontitle === sectionNameDotDecoded) ) { pageTitle = result.title; sectionNameFound = result.sectiontitle; break; } } var searchLink = mw.util.getUrl('Special:Search', { search: '~' + searchQuery, // ~ in the beginning forces a search even if a page of the same name exists, see [[H:FORCE]] prefix: prefix, sort: 'create_timestamp_desc' }); var escapedSectionName = mw.html.escape(sectionNameFound || sectionName); if (pageTitle) { // if a section with the same name was found var discussionLink = mw.util.getUrl(pageTitle) + '#' + mw.util.wikiUrlencode(sectionNameFound); $('.archived-section-prompt').html(mw.msg('fas-exact-match', escapedSectionName, discussionLink, searchLink)); } else { $('.archived-section-prompt').html(mw.msg('fas-inexact-match', escapedSectionName, searchLink)); } } }).catch(function(err) { console.error('[find-archived-section]: ', JSON.stringify(err)); }); }); sw1l0clmnztceq648dpbh8arhz7lrzb MediaWiki:Gadget-geonotice 8 24499 268737 2026-04-28T09:38:20Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[Wikipedia:Geonotice|Geonotice]]: sebitiňizdäki wakalar barada gözegçilik [[Special:Watchlist|sanawyňyzda]] bildirişleri görkezmek' 268737 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> [[Wikipedia:Geonotice|Geonotice]]: sebitiňizdäki wakalar barada gözegçilik [[Special:Watchlist|sanawyňyzda]] bildirişleri görkezmek d377t54jmfimuqj69val04jgjzyip2w MediaWiki:Gadget-geonotice-list.js 8 24500 268738 2026-04-28T09:41:29Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * |_____________________________________________________________________________| * * Defines the list of notices to be shown to regi...' 268738 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * |_____________________________________________________________________________| * * Defines the list of notices to be shown to registered users based on their location. * * USE [[Wikipedia:Geonotice/list.json]] TO CONFIGURE YOUR NOTICES. Changes there will be * synced here by a bot every five minutes. Edits directly to this page will be overriden. * * If your changes were not synced, review [[User:MusikBot II/GeonoticeSync/Report]] for errors. */ window.GeoNotice = {}; window.GeoNotice.notices = { "PDX20260502": { "begin": "24 April 2026 00:00 UTC", "end": "03 May 2026 00:00 UTC", "corners": [ [ 43.7, -124.82 ], [ 46.29, -120.39 ] ], "text": "Join your fellow Wikimedians at the <b><a href=\"/wiki/Event:May_2026_meetup_in_Portland,_Oregon\" title=\"Event:May 2026 meetup in Portland, Oregon\">May 2026 meetup in Portland, Oregon</a></b>. Sign up now!" }, "UK20260419": { "begin": "19 April 2026 00:00 UTC", "end": "17 May 2026 20:00 UTC", "country": "GB", "text": "Interested in having a chat with fellow Wikipedians? There are upcoming meetups in: <a href=\"https://meta.wikimedia.org/wiki/Meetup/Brixton/16\" class=\"extiw\" title=\"m:Meetup/Brixton/16\">Brixton, 27 April</a>; <a href=\"https://meta.wikimedia.org/wiki/Meetup/London/228\" class=\"extiw\" title=\"m:Meetup/London/228\">London, 10 May</a>; and <a href=\"https://meta.wikimedia.org/wiki/Meetup/Cardiff/7\" class=\"extiw\" title=\"m:Meetup/Cardiff/7\">Cardiff, 17 May</a>!", "comments": "Last 8 chars of ID is date of last amendment in CCYYMMDD format - change this if making major amendment or adding a meetup; leave alone if minor amendment or removing a meetup. Set the 'begin' parameter to yesterday's date - amend only if the ID was altered. Set the 'end' parameter to the date of last meetup shown. Try to limit 'text' to four meetups, no more than one per town/city, and no more than four weeks in advance; shorten month names to three letters if four meetups are shown." }, "NYCaprwikiwed": { "begin": "15 April 2026 00:00 UTC", "end": "30 April 2026 00:00 UTC", "corners": [ [ 40.5, -72.0 ], [ 42.0, -75.0 ] ], "text": "Join <a href=\"/wiki/Wikipedia:Meetup/NYC/April_2026\" title=\"Wikipedia:Meetup/NYC/April 2026\">Wikimedia NYC WikiWednesday</a> at 6-9PM on April 29!" }, "SanDiegoMay2026": { "begin": "12 April 07:00 UTC", "end": "10 May 22:00 UTC", "corners": [ [ 34.0833, -118.1167 ], [ 32.53, -114.4333 ] ], "text": "Join other Wikimedians at a <b><a href=\"/wiki/Wikipedia:Meetup/San_Diego/May_2026\" title=\"Wikipedia:Meetup/San Diego/May 2026\">salon &amp; edit-a-thon</a></b> in San Marcos." }, "SanDiegoJun2026": { "begin": "11 May 2026 07:00 UTC", "end": "13 June 2026 21:45 UTC", "corners": [ [ 33.51, -117.61 ], [ 32.4, -114.43 ] ], "text": "Join other Wikimedians at a <b><a href=\"/wiki/Wikipedia:Meetup/San_Diego/June_2026\" title=\"Wikipedia:Meetup/San Diego/June 2026\">salon &amp; edit-a-thon</a></b> in Portola, California." }, "SanDiegoJul2026": { "begin": "14 June 2026 08:00 UTC", "end": "18 July 2026 22:00 UTC", "corners": [ [ 33.429, -116.082 ], [ 32.524, -117.6 ] ], "text": "Join other Wikimedians for a pre-<a href=\"/wiki/San_Diego_Comic-Con\" title=\"San Diego Comic-Con\">San Diego Comic-Con</a> <b><a href=\"/wiki/Wikipedia:Meetup/San_Diego/July_2026\" title=\"Wikipedia:Meetup/San Diego/July 2026\">edit-a-thon/salon</a></b> in Mission Valley, San Diego." }, "SanDiegoAug2026": { "begin": "15 May 2026 07:00 UTC", "end": "2 August 2026 05:10 UTC", "corners": [ [ 42.0, -124.4333 ], [ 31.333333, -109.05 ] ], "text": "Join other Wikimedians at a <b><a href=\"/wiki/Wikipedia:Meetup/San_Diego/August_2026_Wiknic\" title=\"Wikipedia:Meetup/San Diego/August 2026 Wiknic\">Wiknic in Mission Bay, San Diego</a></b>. Potluck, beach campfire, and fireworks!" }, "WikiMediaWikiMay2026": { "begin": "15 April 2026 00:00 UTC", "end": "3 May 2026 14:00 UTC", "corners": [ [ 38.85, -120.5 ], [ 36.6, -123.5 ] ], "text": "<a href=\"/wiki/Event:Wikimediawiki_Workshop_May_2026\" title=\"Event:Wikimediawiki Workshop May 2026\">Join open source developers</a> on May 3rd for a WikiMediaWiki Workshop. Learn how you can contribute to the technical side of Wikipedia or meet with your fellow contributors and hack away! May the 4th be with you!" }, "OaklandMay2026": { "begin": "10 May 2026 00:00 UTC", "end": "23 May 2026 14:00 UTC", "corners": [ [ 38.85, -120.5 ], [ 36.6, -123.5 ] ], "text": "<a href=\"/wiki/Event:Bay_Area_Meetup_Oakland_May_2026\" title=\"Event:Bay Area Meetup Oakland May 2026\">Join</a> Wikipedians in Oakland and the East Bay Area on May 23rd for a casual meetup over coffee and tea. All editors and friends of Wikipedia are welcome." }, "BayAreaMay2026": { "begin": "4 May 2026 00:00 UTC", "end": "12 May 2026 18:00 UTC", "corners": [ [ 38.85, -120.5 ], [ 36.6, -123.5 ] ], "text": "<a href=\"/wiki/Event:Bay_Area_Meetup_May_2026\" title=\"Event:Bay Area Meetup May 2026\">Join</a> Wikipedians in the Bay Area on May 12th for their monthly WikiSalon. All editors and friends of Wikipedia are welcome." }, "WikiClubCanada202605": { "begin": "29 April 2026 00:00 UTC", "end": "13 May 2026 23:00 UTC", "country": "CA", "text": "Want to meet other editors interested in Wikimedia projects and based in Canada? WikiClub Canada is hosting its next meeting! Join us online on the evening of Wednesday May 13. Sign up at <a href=\"https://meta.wikimedia.org/wiki/Event:WikiClub_Canada/May_2026\" class=\"extiw\" title=\"meta:Event:WikiClub Canada/May 2026\">our event page</a>." }, "WikiClubCanada202606": { "begin": "27 May 2026 00:00 UTC", "end": "10 June 2026 23:00 UTC", "country": "CA", "text": "Want to meet other editors interested in Wikimedia projects and based in Canada? WikiClub Canada is hosting its next meeting! Join us online on the evening of Wednesday June 10. Sign up at <a href=\"https://meta.wikimedia.org/wiki/Event:WikiClub_Canada/June_2026\" class=\"extiw\" title=\"meta:Event:WikiClub Canada/June 2026\">our event page</a>." }, "EdmontonMeetUp": { "begin": "15 May 2026 00:00 UTC", "end": "31 May 2026 20:00 UTC", "corners": [ [ 53.775, -112.755 ], [ 52.959, -114.412 ] ], "text": "Wikimedia Canada is bringing its next in-person meetup to Edmonton! Come meet like-minded people or have wiki-chats about editing on Sunday May 31. Sign up at <a href=\"https://meta.wikimedia.org/wiki/Event:Edmonton_Meetup_May_2026\" class=\"extiw\" title=\"meta:Event:Edmonton Meetup May 2026\">our event page</a>. Light refreshments will be provided." }, "KitchenerMeetUp": { "begin": "24 April 2026 00:00 UTC", "end": "3 May 2026 00:00 UTC", "corners": [ [ 43.7561, -79.9372 ], [ 43.017, -81.0185 ] ], "text": "Wikimedia Canada is bringing its next in-person meetup to the Kitchener-Waterloo region! Come meet like-minded people or have wiki-chats about editing on Saturday May 2. Sign up at <a href=\"/wiki/Event:Kitchener,_Ontario_meetup_in_May_2026\" title=\"Event:Kitchener, Ontario meetup in May 2026\">our event page</a>. Light snacks will be provided." } }; 23snso0ykkiuyw3tkg77iyck7sittxf MediaWiki:Gadget-geonotice-core.js 8 24501 268739 2026-04-28T09:42:09Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268739 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Imported as of 8 august 2014 from [[testwiki:MediaWiki:Gadget-geonotice-core.js]] * Shows notices to registered users based on their location */ /* global jQuery, mediaWiki */ ( function ( mw, $ ) { 'use strict'; mw.messages.set( { 'gn-hideButton': 'Hide' } ); /** * Namespace for all Geonotice methods and properties. * @class * @singleton */ var gn = {}; /** * @param {string} str Wiki-text of the link * @param {string} page The title of the target page of the link * @param {string} text The text to be used for the link */ /* jshint unused: true */ gn.geoWikiLinker = function (str, page, text) { text = text || page; return mw.html.element( 'a', { href: mw.util.getUrl( page ), title: page }, text ); }; /** * Handle click events. * * @param {jQuery.Event} e Click event */ gn.hideGeonotice = function (e) { e.preventDefault(); var parentId = $(e.target).closest('li').attr('id').replace( /^geonotice/, ''); var hiddenNotices = gn.getHiddenNotices(); hiddenNotices.push(parentId); gn.saveHiddenNotices(hiddenNotices); $( '#geonotice' + parentId ).hide(); $( '#geonotice-hr' ).hide(); return false; }; /** * Boolean indicating whether this will be the first notice added to the page */ gn.firstnotice = true; /** * Regular expression used to detect links in wiki-text */ gn.regexForInternalLinks = /\[\[([^{|}\[\]\n]+)(?:\|(.*?))?\]\]/g; /** * A key used to store the array of hidden notices in Web Storage */ gn.storageKey = 'hidegeonotices'; /** * Get a list of the hidden notices */ gn.getHiddenNotices = function () { var hiddenNotices = mw.storage.get( gn.storageKey ) || mw.storage.session.get( gn.storageKey ); try { return JSON.parse(hiddenNotices) || []; } catch (e) { return []; } }; /** * Save the list of the hidden notices */ gn.saveHiddenNotices = function ( notices ) { notices = JSON.stringify( notices ); mw.storage.set( gn.storageKey, notices ) || mw.storage.session.set( gn.storageKey, notices ); }; /** * Removes ids from localstorage that are no longer in use */ gn.expungeOldNotices = function( currentList ) { var savedList = gn.getHiddenNotices(), originalLength = savedList.length; for (var i = savedList.length - 1; i >= 0; i--) { if( !( savedList[i] in currentList ) ) { savedList.splice( i, 0 ); } } if( originalLength !== savedList.length ) { gn.saveHiddenNotices( savedList ); } }; /** * Add a notice on top of the watchlist * * @param {Object} notice Object representing a notice */ gn.displayGeonotice = function (notice) { var geonoticeText = notice.text.replace( gn.regexForInternalLinks, gn.geoWikiLinker ); if (gn.firstnotice) { gn.firstnotice = false; $('#watchlist-message').prepend( $( '<hr>' ).attr({ 'id' : 'geonotice-hr' }) ); } $('#watchlist-message').prepend( $('<li>') .attr({ 'class' : 'geonotice plainlinks', 'id' : 'geonotice' + notice.id }) .append( $( '<span>' ) .html( geonoticeText ), $( '<small>' ) .append( $('<a>') .text( mw.msg( 'gn-hideButton' ) ) .click( gn.hideGeonotice ) .attr( { 'href' : '#' } ) ) ) ).show(); }; /** * Determine which notices are still valid and are targeted to the location of the current user */ gn.runGeonotice = function () { var now = new Date(), hide, id, notice, minlat, maxlat, minlon, maxlon, startNotice, endNotice, hiddenNotices = gn.getHiddenNotices(); for (id in gn.notices) { hide = hiddenNotices.indexOf( id ) >= 0; if (!hide) { notice = gn.notices[id]; notice.id = id; if (!notice || !notice.begin || !notice.end) { continue; } startNotice = Date.parse(notice.begin); endNotice = Date.parse(notice.end); if ( now.getTime() > startNotice && now.getTime() < endNotice ) { if (notice.country && Geo.country === notice.country) { gn.displayGeonotice(notice); } else { if (notice.corners) { minlat = Math.min(notice.corners[0][0], notice.corners[1][0]); maxlat = Math.max(notice.corners[0][0], notice.corners[1][0]); minlon = Math.min(notice.corners[0][1], notice.corners[1][1]); maxlon = Math.max(notice.corners[0][1], notice.corners[1][1]); // Geo coordinates can be empty string if unknown. parseFloat makes // these NaN, so that you do not get to see a notice in that case. if ( minlat < parseFloat( Geo.lat ) && parseFloat( Geo.lat ) < maxlat && minlon < parseFloat( Geo.lon ) && parseFloat( Geo.lon ) < maxlon ) { gn.displayGeonotice(notice); } } } } } } gn.expungeOldNotices( gn.notices ); }; // Attach to window window.GeoNotice = $.extend( gn, window.GeoNotice ); if ( window.Geo !== undefined ) { $( gn.runGeonotice ); } }( mediaWiki, jQuery ) ); gih5jt35mvvxzcgnmgn9qnw8z90a8au MediaWiki:Gadget-geonotice.js 8 24502 268740 2026-04-28T09:43:00Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268740 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Shows notices to registered users based on their location */ /* global mw */ if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Watchlist' ) { mw.loader.load( 'ext.gadget.geonotice-core' ); } nmojtoo74o0crfmk751cslvhz0edpg1 MediaWiki:Gadget-geonotice-core.css 8 24503 268741 2026-04-28T09:44:20Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268741 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| */ #watchlist-message .geonotice { width: 98%; background: transparent; text-align: left; line-height: 1.8em; } #watchlist-message .geonotice span { font-size: 120%; } #watchlist-message .geonotice small { font-style: italic; margin-left: .5em; } #watchlist-message .geonotice small a::before { content: "["; } #watchlist-message .geonotice small a::after { content: "]"; } 4l51cqhdz1drifx4md5l6b1akwg4af1 MediaWiki:Gadget-geonotice-core 8 24504 268742 2026-04-28T09:45:22Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Geonotice enjamy bilen ulanylýan goşmaça komponentler' 268742 wikitext text/x-wiki Geonotice enjamy bilen ulanylýan goşmaça komponentler itomkrq7dwztflep57n7aljrbzy68kv MediaWiki:Gadget-watchlist-notice 8 24505 268743 2026-04-28T09:47:58Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik [[Wikipedia:Watchlist notices|sanawynyň]] bildirişlerini görkezmek' 268743 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik [[Wikipedia:Watchlist notices|sanawynyň]] bildirişlerini görkezmek 7dc7ryuyd8iyfpzqzhfqmylj86hk670 MediaWiki:Gadget-watchlist-notice.js 8 24506 268744 2026-04-28T09:52:43Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Watchlist' ) { mw.loader.load( 'ext.gadget.watchlist-notice-core' ); }' 268744 javascript text/javascript if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Watchlist' ) { mw.loader.load( 'ext.gadget.watchlist-notice-core' ); } 0qcadfoa32w7c6pmxhzlc84tlwctq6d MediaWiki:Gadget-watchlist-notice-core 8 24507 268745 2026-04-28T10:16:53Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Gözegçilik sanawy bildiriş gadjetiniň komponentleri' 268745 wikitext text/x-wiki Gözegçilik sanawy bildiriş gadjetiniň komponentleri e3wvoo5q12f7zcx0quof4bpdpg3fcx3 MediaWiki:Gadget-WatchlistGreenIndicators 8 24508 268746 2026-04-28T10:24:36Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik sanawyňyzdaky üýtgedilen sahypalar, sahypa taryhy we soňky üýtgeşmeler üçin ýaşyl reňkli ýygnalyp bilinýän oklary we ýaşyl markerleri görkeziň (gowulandyrylan Gözegçilik sanawy ulanyjy interfeýsi bilen elýeterli däl)' 268746 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik sanawyňyzdaky üýtgedilen sahypalar, sahypa taryhy we soňky üýtgeşmeler üçin ýaşyl reňkli ýygnalyp bilinýän oklary we ýaşyl markerleri görkeziň (gowulandyrylan Gözegçilik sanawy ulanyjy interfeýsi bilen elýeterli däl) dnh39l6shd2hf9xp35xswk61owmtfd4 268747 268746 2026-04-28T10:24:57Z Umarxon III 11129 268747 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik sanawyňyzdaky üýtgedilen sahypalar, sahypa taryhy we soňky üýtgeşmeler üçin ýaşyl reňkli ýygnalyp bilinýän oklary we ýaşyl markerleri görkezmek (gowulandyrylan Gözegçilik sanawy ulanyjy interfeýsi bilen elýeterli däl) rsofxmmmpzxv3ah32qmnvwyk0stuon5 MediaWiki:Gadget-WatchlistGreenIndicators.css 8 24509 268748 2026-04-28T10:25:32Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268748 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Imported as of 27 may 2015 from [[User:Edokter/vector.css]], [[MediaWiki:Common.css]] and [[MediaWiki:Vector.css]] * Display green collapsible arrows and green bullets for changed pages in Watchlist, history and recent changes * * Please prefix selectors with .mw-rcfilters-disabled unless they are tested with and intended to work with the new ChangesList UI (RCFilters / WLFilters / Structured Change Filters) */ /* Standard watchlist, history and recent changes */ .mw-rcfilters-disabled #mw-wlheader-showupdated, .mw-rcfilters-disabled #mw-wlheader-green { display: inline; } .mw-rcfilters-disabled #mw-watchlist-resetbutton { display: block; } .mw-rcfilters-disabled li.mw-changeslist-line-watched { list-style-image: url(//upload.wikimedia.org/wikipedia/commons/1/19/ChangedBulletVector.svg); } /* Enhanced watchlist and recent changes */ .mw-rcfilters-disabled td.mw-enhanced-rc, .mw-rcfilters-disabled .mw-enhanced-rc-time { /* [[WP:MONO]] */ font-family: monospace, monospace; } .mw-rcfilters-disabled .mw-enhanced-rc-nested { background-position: 0 3px; } .mw-rcfilters-disabled .mw-enhancedchanges-arrow-space { background-position: center top; } .mw-rcfilters-disabled .mw-enhanced-rc-nested, .mw-rcfilters-disabled .mw-enhancedchanges-arrow-space { background-repeat: no-repeat; background-image: url(//upload.wikimedia.org/wikipedia/commons/b/bf/Vector-bullet-icon.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } /* T352456 */ .mw-rcfilters-disabled .mw-enhancedchanges-arrow-space { transform: none !important; background-size: auto !important; } .mw-rcfilters-disabled .mw-enhanced-watched .mw-enhanced-rc-nested, .mw-rcfilters-disabled .mw-changeslist-line-watched .mw-enhancedchanges-arrow-space { background-image: url(//upload.wikimedia.org/wikipedia/commons/1/19/ChangedBulletVector.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } .mw-rcfilters-disabled .mw-enhancedchanges-checkbox:not( :checked ) + .mw-changeslist-line-not-watched .mw-enhancedchanges-arrow { background-position: center top; background-image: url(//upload.wikimedia.org/wikipedia/commons/3/39/Vector_right_arrow_link.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } .mw-rcfilters-disabled .mw-enhancedchanges-checkbox:checked + .mw-changeslist-line-not-watched .mw-enhancedchanges-arrow { background-position: center top; background-image: url(//upload.wikimedia.org/wikipedia/commons/d/db/Vector_down_arrow_link.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } .mw-rcfilters-disabled .mw-enhancedchanges-checkbox:not( :checked ) + .mw-changeslist-line-watched .mw-enhancedchanges-arrow { background-image: url(//upload.wikimedia.org/wikipedia/commons/0/03/Vector_right_arrow_changed.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } .mw-rcfilters-disabled .mw-enhancedchanges-checkbox:checked + .mw-changeslist-line-watched .mw-enhancedchanges-arrow { background-image: url(//upload.wikimedia.org/wikipedia/commons/1/12/Vector_down_arrow_changed.svg); mask-image: none; -webkit-mask-image: none; background-color: transparent; } /* T352456 */ /* Workaround for MediaWiki bug applied even when the rest of the gadget is inactive */ /* Remove this after https://gerrit.wikimedia.org/r/c/mediawiki/core/+/981405 is deployed */ .mw-enhancedchanges-arrow-space { width: 15px !important; height: 15px !important; } ok01qgj93seq43tnvrwr8z5pnoldn8r MediaWiki:Gadget-WatchlistGreenIndicatorsMono 8 24510 268749 2026-04-28T10:26:55Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik sanawyňyzda, taryhyňyzda we soňky üýtgeşmeleriňizde üýtgedilen sahypalar üçin ýaşyl ýygnalyp bilinýän oklary we ýaşyl markerleri görkezmek (gowulandyrylan Gözegçilik sanawy ulanyjy interfeýsi bilen elýeterli däl)' 268749 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Gözegçilik sanawyňyzda, taryhyňyzda we soňky üýtgeşmeleriňizde üýtgedilen sahypalar üçin ýaşyl ýygnalyp bilinýän oklary we ýaşyl markerleri görkezmek (gowulandyrylan Gözegçilik sanawy ulanyjy interfeýsi bilen elýeterli däl) 3uxmhqvl1aucl7q753lrs3vn9shn63y MediaWiki:Gadget-WatchlistGreenIndicatorsMono.css 8 24511 268750 2026-04-28T10:28:30Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268750 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Imported as of 27 may 2015 from [[User:Edokter/vector.css]], [[MediaWiki:Common.css]] and [[MediaWiki:Monobook.css]] * Display green collapsible arrows and green bullets for changed pages in Watchlist, history and recent changes * * Please prefix selectors with .mw-rcfilters-disabled unless they are tested with and intended to work with the new ChangesList UI (RCFilters / WLFilters / Structured Change Filters) */ /* Standard watchlist, history and recent changes */ .mw-rcfilters-disabled #mw-wlheader-showupdated, .mw-rcfilters-disabled #mw-wlheader-green { display: inline; } .mw-rcfilters-disabled #mw-watchlist-resetbutton { display: block; } .mw-rcfilters-disabled li.mw-changeslist-line-watched { list-style-image: url(//upload.wikimedia.org/wikipedia/commons/f/fa/ChangedBulletMono.png); } /* Enhanced watchlist and recent changes */ .mw-rcfilters-disabled td.mw-enhanced-rc, .mw-rcfilters-disabled .mw-enhanced-rc-time { font-family: monospace, monospace; } .mw-rcfilters-disabled .mw-enhanced-rc-nested { background-position: 0 1px; } .mw-rcfilters-disabled .mw-enhancedchanges-arrow-space { background-position: center top; } .mw-rcfilters-disabled .mw-enhanced-rc-nested, .mw-rcfilters-disabled .mw-enhancedchanges-arrow-space { background-repeat: no-repeat; background-image: url(//upload.wikimedia.org/wikipedia/commons/7/7a/Bullet.png); } .mw-rcfilters-disabled .mw-enhanced-watched .mw-enhanced-rc-nested, .mw-rcfilters-disabled .mw-changeslist-line-watched .mw-enhancedchanges-arrow-space { background-image: url(//upload.wikimedia.org/wikipedia/commons/f/fa/ChangedBulletMono.png); } .mw-rcfilters-disabled .mw-changeslist-line-not-watched .mw-collapsible-arrow.mw-collapsible-toggle-collapsed { background-position: center top; background-image: url(//upload.wikimedia.org/wikipedia/commons/b/bc/Vector_right_arrow_link.png); } .mw-rcfilters-disabled .mw-changeslist-line-not-watched .mw-collapsible-arrow.mw-collapsible-toggle-expanded { background-position: center top; background-image: url(//upload.wikimedia.org/wikipedia/commons/2/27/Vector_down_arrow_link.png); } .mw-rcfilters-disabled .mw-changeslist-line-watched .mw-collapsible-arrow.mw-collapsible-toggle-collapsed { background-image: url(//upload.wikimedia.org/wikipedia/commons/a/af/Vector_right_arrow_changed.png); } .mw-rcfilters-disabled .mw-changeslist-line-watched .mw-collapsible-arrow.mw-collapsible-toggle-expanded { background-image: url(//upload.wikimedia.org/wikipedia/commons/e/e5/Vector_down_arrow_changed.png); } g28rmutejabcal6bysjk1pgdbkw2vk5 MediaWiki:Gadget-WatchlistBase 8 24512 268751 2026-04-28T10:31:33Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> <small>(Bu gözegçilik [[MediaWiki:Gadget-WatchlistBase.css|sanawy]] üçin esasy stili ýükleýär. Bu opsiýany öçürmäň.)</small>' 268751 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> <small>(Bu gözegçilik [[MediaWiki:Gadget-WatchlistBase.css|sanawy]] üçin esasy stili ýükleýär. Bu opsiýany öçürmäň.)</small> 1zhnx811z5vlz4fy3z4b2kwchzc1euv MediaWiki:Gadget-WatchlistChangesBold 8 24513 268752 2026-04-28T10:34:53Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Soňky saparyňyzdan bäri üýtgedilen sahypalary gözegçilik sanawyňyzda '''goýy''' harplar bilen görkezmek' 268752 wikitext text/x-wiki Soňky saparyňyzdan bäri üýtgedilen sahypalary gözegçilik sanawyňyzda '''goýy''' harplar bilen görkezmek tqk0bqiiqzybv18p7v66ajqq0srubz7 MediaWiki:Gadget-WatchlistChangesBold.css 8 24514 268753 2026-04-28T10:35:33Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268753 css text/css /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Display pages on your watchlist that have changed since your last visit in '''bold'''. */ #mw-wlheader-green { display: none; } #mw-wlheader-showupdated, #mw-wlheader-bold { display: inline; } #mw-watchlist-resetbutton { display: block; } .mw-special-Watchlist .mw-changeslist-line-watched .mw-title { font-weight: bold; } 46n1bm2gwp82khc4paex86uqyeon8rt MediaWiki:Gadget-SubtleUpdatemarker 8 24515 268754 2026-04-28T10:37:31Z Umarxon III 11129 Sahypa döretdi, mazmuny: '<sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Inçe täzelenme belgisi: Taryh sahypalaryndaky "Soňky sapardan bäri üýtgedildi" görkezijisini peselmek (Adaty ýagdaýda, ol ýaşyl reňkli zolak hökmünde görkezilýär, bu bolsa bu enjamy işjeňleşdirmek bilen ony ýaşyl tekste öwürýär.)' 268754 wikitext text/x-wiki <sup><abbr title="{{int:gadgets-default}}">(D)</abbr></sup> Inçe täzelenme belgisi: Taryh sahypalaryndaky "Soňky sapardan bäri üýtgedildi" görkezijisini peselmek (Adaty ýagdaýda, ol ýaşyl reňkli zolak hökmünde görkezilýär, bu bolsa bu enjamy işjeňleşdirmek bilen ony ýaşyl tekste öwürýär.) 1vceudawr0fd1ree11njo2mm4mnprvf MediaWiki:Gadget-SubtleUpdatemarker.css 8 24516 268755 2026-04-28T10:38:01Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* Tone down 'Changed since last visit' colors */ span.updatedmarker { background-color: transparent; color: var(--color-content-added, #006400); } /* * this was originally in the WatchlistGreenIndicators.css gadget, but it loads * on history pages, which we'd like to avoid in that gadget which is currently * watchlist only */ li.mw-history-line-updated { list-style-image: url(//upload.wikimedia.org/wikipedia/commons/1/19/ChangedBulletVector.svg); }' 268755 css text/css /* Tone down 'Changed since last visit' colors */ span.updatedmarker { background-color: transparent; color: var(--color-content-added, #006400); } /* * this was originally in the WatchlistGreenIndicators.css gadget, but it loads * on history pages, which we'd like to avoid in that gadget which is currently * watchlist only */ li.mw-history-line-updated { list-style-image: url(//upload.wikimedia.org/wikipedia/commons/1/19/ChangedBulletVector.svg); } cryl0xwu6n6p959i6ams99x1hgic9nl MediaWiki:Gadget-defaultsummaries 8 24517 268756 2026-04-28T10:40:52Z Umarxon III 11129 Sahypa döretdi, mazmuny: 'Redaktirlemegiň gysgaça mazmuny gutusynyň aşagynda käbir peýdaly standart gysgaça maglumatlar bilen iki täze açylýan gutu goşmek' 268756 wikitext text/x-wiki Redaktirlemegiň gysgaça mazmuny gutusynyň aşagynda käbir peýdaly standart gysgaça maglumatlar bilen iki täze açylýan gutu goşmek awd9qwdtxdhqr0dze13sxm35j35nr6n MediaWiki:Gadget-defaultsummaries.js 8 24518 268757 2026-04-28T10:45:06Z Umarxon III 11129 Sahypa döretdi, mazmuny: '/* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________...' 268757 javascript text/javascript /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_____________________________________________________________________________| * * Imported as of 09/06/2011 from [[User:ErrantX/defaultsummaries.js]] * Edited version from [[User:MC10/defaultsummaries.js]] * Implements default edit summary dropdown boxes */ /* global mw, ve */ /* eslint-disable no-jquery/no-global-selector */ ( function () { // Wrap with anonymous function var $summaryBox = $( '#wpSummary' ), minorSummaries = [ 'Spelling/grammar/punctuation/typographical correction', 'Fixing style/layout errors', '[[Help:Reverting|Reverting]] [[Wikipedia:Vandalism|vandalism]] or test edit', '[[Help:Reverting|Reverting]] unexplained content removal', 'Copyedit (minor)' ], articleSummaries = [ 'Expanding article', 'Adding/improving reference(s)', 'Adding/removing wikilink(s)', 'Clean up/copyedit', 'Adding/removing category/ies', 'Adding/removing external link(s)', 'Removing unsourced content' ], nonArticleSummaries = [ 'Reply', 'Comment', 'Suggestion' ], talkPageSummaries = [ '[[Wikipedia:WikiProject|WikiProject]] tagging', '[[Wikipedia:WikiProject|WikiProject]] assessment' ]; function addOptionsToDropdown( dropdown, optionTexts ) { dropdown.menu.addItems( optionTexts.map( function ( optionText ) { return new OO.ui.MenuOptionWidget( { label: optionText } ); } ) ); } function onSummarySelect( option ) { // Save the original value of the edit summary field var editsummOriginalSummary = $summaryBox.val(), canned = option.getLabel(), newSummary = editsummOriginalSummary; // Append old edit summary with space, if exists, // and last character != space if ( newSummary.length !== 0 && newSummary.charAt( newSummary.length - 1 ) !== ' ' ) { newSummary += ' '; } newSummary += canned; $summaryBox.val( newSummary ).trigger( 'change' ); } function getSummaryDropdowns() { // For convenience, add a dropdown box with some canned edit // summaries to the form. var namespace = mw.config.get( 'wgNamespaceNumber' ), dropdown = new OO.ui.DropdownWidget( { label: 'Common edit summaries – click to use' } ), minorDropdown = new OO.ui.DropdownWidget( { label: 'Common minor edit summaries – click to use' } ); dropdown.menu.on( 'select', onSummarySelect ); minorDropdown.menu.on( 'select', onSummarySelect ); addOptionsToDropdown( minorDropdown, minorSummaries ); if ( namespace === 0 ) { addOptionsToDropdown( dropdown, articleSummaries ); } else { addOptionsToDropdown( dropdown, nonArticleSummaries ); if ( namespace % 2 !== 0 && namespace !== 3 ) { addOptionsToDropdown( dropdown, talkPageSummaries ); } else if (namespace === 118 ) { addOptionsToDropdown( dropdown, articleSummaries ); } } return dropdown.$element.add( minorDropdown.$element ); } // VisualEditor mw.hook( 've.saveDialog.stateChanged' ).add( function () { var target, $saveOptions, $dropdowns; // .ve-init-mw-viewPageTarget-saveDialog-checkboxes if ( $( 'body' ).data( 'wppresent' ) ) { return; } $( 'body' ).data( 'wppresent', 'true' ); target = ve.init.target; $saveOptions = target.saveDialog.$saveOptions; $summaryBox = target.saveDialog.editSummaryInput.$input; if ( !$saveOptions.length ) { return; } $dropdowns = getSummaryDropdowns(); $saveOptions.before( $dropdowns ); } ); // WikiEditor $.when( mw.loader.using( 'oojs-ui-core' ), $.ready ).then( function () { var $dropdowns, $editCheckboxes = $( '.editCheckboxes' ); // If we failed to find the editCheckboxes class if ( !$editCheckboxes.length ) { return; } $dropdowns = getSummaryDropdowns(); $dropdowns.css( { width: '48%', 'padding-bottom': '1em' } ); $editCheckboxes.before( $dropdowns ); } ); }() ); tlurpz64h5b7m0x9s8pjlufak1cjhbb MediaWiki:Gadget-citations 8 24519 268758 2026-04-28T11:52:59Z Umarxon III 11129 Sahypa döretdi, mazmuny: '[[WP:Citation expander|Citation expander]]: [[WP:UCB|Citation bot]] yardaminde sitirlemeleri awtomatiki usulda giňeldýär we formatlaýar' 268758 wikitext text/x-wiki [[WP:Citation expander|Citation expander]]: [[WP:UCB|Citation bot]] yardaminde sitirlemeleri awtomatiki usulda giňeldýär we formatlaýar dr85ja3tsi8sodthbpin3y7m4t8cv4w