Wikipedia testwiki https://test.wikipedia.org/wiki/Main_Page MediaWiki 1.47.0-wmf.5 first-letter Media Special Talk User User talk Wikipedia Wikipedia talk File File talk MediaWiki MediaWiki talk Template Template talk Help Help talk Category Category talk Thread Thread talk Summary Summary talk Test namespace 1 Test namespace 1 talk Test namespace 2 Test namespace 2 talk Draft Draft talk Campaign Campaign talk TimedText TimedText talk Module Module talk SecurePoll SecurePoll talk CNBanner CNBanner talk Translations Translations talk Event Event talk Topic Newsletter Newsletter talk Template:Infobox settlement 10 41275 745213 487021 2026-06-04T23:45:27Z SSethi (WMF) 36407 Imported from https://test.wikipedia.org/wiki/User:Iiirxs/Template:Infobox_settlement by StarterKit infobox tool (content under CC BY-SA) 745213 wikitext text/x-wiki {{Infobox | above = {{{name|{{PAGENAME}}}}} | image = {{{image|{{#if:{{#property:P18}}|[[File:{{#property:P18}}|frameless|upright=1|alt=]]}}}}} | label1 = Country | data1 = {{{country|{{#property:P17}}}}} | label2 = Region | data2 = {{{region|{{#property:P131}}}}} | label3 = Population | data3 = {{{population|{{#property:P1082}}}}} | label4 = Coordinates | data4 = {{{coordinates|{{#property:P625}}}}} }} <noinclude> {{documentation}} </noinclude> 4ffm1eezt7jry44cwcyymaethp657sa Template:No article text 10 78344 745184 740781 2026-06-04T14:41:57Z Sheffalump 71152 745184 wikitext text/x-wiki <div class="mw-parser-output"><!-- don't touch this div to the left, it makes TemplateStyles vroom vroom --> {{New page DYM}} {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} |{{#ifexist: {{SUBJECTPAGENAME}}|This is the talk page for [[:{{SUBJECTPAGENAME}}]].|[[:{{SUBJECTPAGENAME}}]] has not yet been created. {{Viewing talk page without subject}}|}}}} {{fmbox | type = system | id = noarticletext | image = none | textstyle = padding: 0.6em 0.9em; <!--Large box needs more padding--> | text = {{#tag:strong|WARNING:}} {{ #ifeq: {{ #ifeq: {{FULLPAGENAME}} | Template:No article text || }} | <!--Mainspace sister project infobox--> | {{No article text/sister projects}} }}{{ #ifeq: {{NAMESPACE}} | {{ns:User talk}} | {{#tag:strong|No messages}} have been posted for {{BASEPAGENAME}} yet. | {{#tag:strong|Wikipedia does not contain {{ #switch: {{NAMESPACE}} | {{ns:0}} = a {{#tag:em|[[{{ns:Project}}:Using pages without namespaces|page]]}} | {{ns:Talk}} = a {{#tag:em|[[{{ns:Help}}:Using talk pages|discussion page]]}} | {{TALKSPACE}} = the {{#tag:em|[[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s discussion page]]}} | {{ns:Category}} = a {{#tag:em|[[{{ns:Project}}:Categorization|category]]}} | {{ns:Help}} = a {{#tag:em|[[{{ns:Help}}:Contents|help page]]}} | {{ns:File}} = a {{#tag:em|[[{{ns:Project}}:Files to upload|file]]}} | {{ns:Portal}} = a {{#tag:em|[[{{ns:Project}}:Portal|portal]]}} | {{ns:Template}} = a {{#tag:em|[[{{ns:Project}}:Template messages|template]]}} | {{ns:User}} = a {{#tag:em|[[{{ns:Project}}:User pages|user page]]}} | {{ns:Project}} = a {{#tag:em|[[{{ns:Project}}:Project namespace|project page]]}} | {{ns:MediaWiki}} = a {{#tag:em|[[Special:AllMessages|system message]]}} | #default = a {{#tag:em|[[{{ns:Project}}:Namespaces|{{NAMESPACE}} page]]}} }} with this exact name yet.}} {{ #ifeq: {{#invoke:String|replace|source={{PAGENAME}}|­}} | {{#invoke:String|replace|source={{SUBPAGENAME}}|­}} | {{ #switch: {{NAMESPACE}} | {{ns:0}} = Please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=search for ''{{#invoke:String|replace|source={{PAGENAME}}|­}}'' in Test Wikipedia}} to check for alternative titles, names, or spellings. | {{ns:Talk}} = Before creating this page, please verify that a page called [[{{PAGENAME}}]] exists. | {{TALKSPACE}} = Before creating this page, please verify that [[:{{SUBJECTPAGENAME}}]] is already made. | {{ns:Category}} = Please browse the [[Portal:Contents|existing categories]] to check if this category is covered under another name. | {{ns:Help}} = Please browse the [[{{ns:Help}}:Contents|existing help pages]] to check if this help topic is covered under another name. | {{ns:File}} = Please do not manually create this page. If you want to upload ''File:{{PAGENAME}}'', Please see the [[{{ns:Project}}:Files to upload|Files to upload]] for instructions & tutorials. | {{ns:Portal}} = Please browse the [[{{ns:Portal}}:Contents/Portals|existing portals]] to check for similar topics. | {{ns:Template}} = Please browse the [[{{ns:Project}}:Template messages|existing templates]] to check if this standardized message is available under another name. | {{ns:User}} = In general, this page should be created and edited by [[User:{{PAGENAME}}]]. If you're already registered, Please [[Special:UserLogin|log in]]. If you don't have an account, [[Special:CreateAccount|create one]]. If you are {{PAGENAME}}, click [[Special:Edit/User:{{PAGENAME}}|create this page]]. If you are not {{PAGENAME}}, don't click "create this page". | {{ns:Project}} = Please browse the [[{{ns:Project}}:List of policies and guidelines|existing policies and guidelines]] or [[Special:Search/{{ns:Project}}:{{PAGENAME}}|search]] for similar existing project pages. | {{ns:MediaWiki}} = Please browse the [[Special:AllMessages|existing system messages]] to check if this Non-CSS, Non-JavaScript, Non-JSON, CSS, JavaScript, & JSON pages are available under another name. | #default = Please browse the [[{{ns:Project}}:Namespaces|existing {{NAMESPACE}} pages]] to check if this {{NAMESPACE}} namespace is available under another name. }} | {{ #ifeq: {{{nopermission}}} | yes |{{int:Namespaceprotected|{{#if: {{NAMESPACE}} | [[Wikipedia:Namespaces|{{NAMESPACE}}]] | [[Wikipedia:Namespaces|Page]] }}}} Please [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in] or [{{fullurl:Special:UserLogin/signup|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} sign up] if you want to create this page. (This page is currently protected so only administrators, system administrators, template editors, extended confirmed users, page movers, autoconfirmed users, & logged in users can make it, '''not''' temporary accounts or unregistered (IP) users). (If you're logged out, {{SITENAME}} has restricted the ability to create new pages. You can go back and edit an existing page, or [[Special:UserLogin|log in or create an account]]. If you're logged in, You do not have permission to create new pages.) Please see [[{{ns:Project}}:Subpages|Subpages]] for subpages if you do not create it because this isn't needed. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].| Before creating this page in the {{#if: {{NAMESPACE}} | '''[[Wikipedia:Namespaces|{{NAMESPACE}}]]''' namespace | '''[[Wikipedia:Namespaces|Page]]''' namespace }}, please see [[{{ns:Project}}:Subpages|Subpages]] for subpages. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].}} }} }}{{ #ifeq: {{{nopermission}}} | yes |* {{ #switch: {{#invoke:Effective protection level|create|{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}} | sysop = Administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:Administrators|administrators]] to create new pages, system messages, & JSON pages. | interfaceadmin = System administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[Wikipedia:System administrators|system administrators]] to create new CSS & JavaScript pages. | templateeditor = Template editors & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[WP:Template editor|template editors]] to create new pages. | extendedconfirmed = Extended confirmed users & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:User access levels#Extendedconfirmed|extended confirmed users]] to create new pages. | autoconfirmed = Autoconfirmed users can't edit this {{NAMESPACE}} page, but still be able to edit pages by users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to create new pages. (After 4 days & 10 edits on English Wikipedia.) | user = Some users can't edit this {{NAMESPACE}} page, but still be able to edit pages by anonymous users & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] to make this page. {{#ifeq:{{NAMESPACE}}|{{ns:Category}}|Or, you can [[Wikipedia:Articles for creation/Categories|request the creation of a new category]].}} | #default = Users with the "Create" permission can't edit this {{NAMESPACE}} page. (You do not have permission to create this page when not logged in). You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to make your own page. }} |{{#ifeq:{{#invoke:Title blacklist|main|action=create|pagename={{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}|templateeditor|* Administrators, template editors, & page movers can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[Wikipedia:Administrators|administrators]], [[Wikipedia:Template editors|template editors]], & [[Wikipedia:Page movers|page movers]] to create new ones.|* ''' {{ #switch: {{NAMESPACE}} | {{ns:3}} = {{#ifeq:{{ROOTPAGENAME}}|{{PAGENAME}}|[[Special:Edit/{{FULLPAGENAME}}|Post a message to ''{{#invoke:String|replace|source={{PAGENAME}}|­}}]]''|[[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]''}} | {{ns:5}} | {{ns:7}} | {{ns:9}} | {{ns:11}} | {{ns:13}} | {{ns:15}} | {{ns:101}} | {{ns:119}} | {{ns:711}} | {{ns:829}} | {{ns:1}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]'' | {{ns:Category}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'']] | {{ns:Project}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|start a discussion about the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}]]'' | #default = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'' page]] }}''' {{ #if: {{NAMESPACE}} |by using the '''"[[Wikipedia:Namespaces|{{NAMESPACE}}]]"''' namespace|by using the '''"[[Wikipedia:Namespaces|Page]]"''' namespace, using the [[Wikipedia:Article wizard|Article Wizard]] if you want to create it & submit it for review, or [[{{ns:Project}}:Requested articles|add a request for the new page]] }}.}} }}{{ #switch: {{NAMESPACE}} | {{ns:0}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing pages. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:User}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user page]]. | {{ns:User talk}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user talk page]]. | {{ns:Category}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing categories. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:File}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing files. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:Template}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing templates. * [[wikimedia:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wikimedia, the Free Templates, Categories, & More! | #default = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing {{#if: {{NAMESPACE}} | pages with the '''"[[{{ns:Project}}:Namespaces|{{NAMESPACE}}]]"''' namespace | pages without using [[{{ns:Project}}:Namespaces|namespaces]] }}. * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this title]]. }} * {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | You can go back to your [[:{{SUBJECTPAGENAME}}|subject page]] | Please discuss changes on your [[{{TALKPAGENAME}}|talk page]] }}. <div id="noarticletext_technical"> ---- '''Other reasons that may be displayed this message:''' * If {{ #ifeq: {{NAMESPACE}} | {{ns:File}} | a file | a page }} was recently edited here, it may be invisible. (Because of a delay in updating the database.) You can wait a few minutes to complete, you can [[Special:Purge/{{FULLPAGENAME}}|click it]] for purging, or you may want to [[Special:Protect/{{FULLPAGENAME}}|protect]] this page. You can also want to [[Special:Edit/{{FULLPAGENAME}}|edit this page]] or [[Special:Delete/{{FULLPAGENAME}}|delete this page]]. * Titles on the Test Wikipedia is in the '''[[w:en:Case sensitivity|case sensitivity]]''' except for this first character; please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=check the Test Wikipedia's alternative capitalizations}} and consider adding some [[Wikipedia:Redirects|redirects]] here to the correct title. If the page you are looking for is not TestSite, see its corresponding [[w:en:Main Page|language Wikipedia]]. If the page is not a [[w:en:test wiki|test wiki]], check one of the [[wikimedia:Main Page|other Foundation wikis]]. * If this page has been deleted, try [{{fullurl:Special:Log/delete|page={{#invoke:String|replace|source={{FULLPAGENAMEE}}|­}}}} checking the '''deletion log'''], and see [[Wikipedia:Why was the page I created deleted?|the Why was the page I created deleted?]] for possible reasons. </div> }} <!--End fmbox --><!--Start book box-->{{#if:{{#invoke:String|match|{{FULLPAGENAME}}|^Book:|nomatch= }}{{#invoke:String|match|{{FULLPAGENAME}}|^Book talk:|nomatch= }}|{{fmbox|type=system|text=<strong>This page may have existed as part of the former book & talk namespace.</strong> If so it was likely moved to [[{{#invoke:String|replace|{{#invoke:String|replace|{{FULLPAGENAME}}|^Book talk:|Book:|plain=False}}|^Book:|Wikipedia:Books/archive/|plain=False}}]] before being deleted. See [[Wikipedia:Books]] for more information.}}}}<!--End book box--> </div> == {{ #switch: {{NAMESPACE}} | {{ns:0}} = Page | {{ns:Talk}} = [[{{ns:Help}}:Using talk pages|Discussion page]] | {{TALKSPACE}} = [[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s Discussion page]] | {{ns:Category}} = [[{{ns:Project}}:Categorization|Category]] | {{ns:Help}} = [[{{ns:Help}}:Contents|Help page]] | {{ns:File}} = [[{{ns:Project}}:Files to upload|File]] | {{ns:Portal}} = [[{{ns:Project}}:Portal|Portal]] | {{ns:Template}} = [[{{ns:Project}}:Template messages|Template]] | {{ns:User}} = [[{{ns:Project}}:User pages|User page]] | {{ns:Project}} = [[{{ns:Project}}:Project namespace|Project page]] | {{ns:MediaWiki}} = [[Special:AllMessages|System message]] | #default = {{NAMESPACE}} page}} not showing up? == Reload the page you just created "'''{{FULLPAGENAME}}'''." <small>You may also [[Wikipedia:Administrators' noticeboard|contact an administrator]] before recreating this page.</small> If you can't find the page, you can [[Special:Search/{{FULLPAGENAME}}|search for an existing page]] or {{ #ifeq: {{{nopermission}}} | yes | <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{urlencode:{{FULLPAGENAME}}}}}} search the related logs]</span> | [[Special:Edit/{{FULLPAGENAME}}|create it]] }}. {{ #ifeq: {{{nopermission}}} | yes |<div class="mw-parser-output">{{fmbox | type = warning | id = noarticletext | image = none | text = You do not have permission to create this page. It may have been protected from creation. <span class="group-user-show">If I think this page should be created here, please remove protection at the [[Wikipedia:Administrators' noticeboard|admin noticeboard]].</span>}}</div>|<div class="mw-parser-output">{{fmbox | type = system | id = noarticletext | image = none | text = If you can't find {{FULLPAGENAME}}, you can also include the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|veaction=edit}} visual editor]</span>, {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span>, or [[Special:NewSection/{{FULLPAGENAME}}|create a new section]] | or the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span> }} for editing.}}</div>}} <noinclude> {{documentation}} </noinclude> 9mvswy85r30io6gv0dfqaw899pqmrqf 745185 745184 2026-06-04T14:43:53Z Sheffalump 71152 Removing span 745185 wikitext text/x-wiki <div class="mw-parser-output"><!-- don't touch this div to the left, it makes TemplateStyles vroom vroom --> {{New page DYM}} {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} |{{#ifexist: {{SUBJECTPAGENAME}}|This is the talk page for [[:{{SUBJECTPAGENAME}}]].|[[:{{SUBJECTPAGENAME}}]] has not yet been created. {{Viewing talk page without subject}}|}}}} {{fmbox | type = system | id = noarticletext | image = none | textstyle = padding: 0.6em 0.9em; <!--Large box needs more padding--> | text = {{#tag:strong|WARNING:}} {{ #ifeq: {{ #ifeq: {{FULLPAGENAME}} | Template:No article text || }} | <!--Mainspace sister project infobox--> | {{No article text/sister projects}} }}{{ #ifeq: {{NAMESPACE}} | {{ns:User talk}} | {{#tag:strong|No messages}} have been posted for {{BASEPAGENAME}} yet. | {{#tag:strong|Wikipedia does not contain {{ #switch: {{NAMESPACE}} | {{ns:0}} = a {{#tag:em|[[{{ns:Project}}:Using pages without namespaces|page]]}} | {{ns:Talk}} = a {{#tag:em|[[{{ns:Help}}:Using talk pages|discussion page]]}} | {{TALKSPACE}} = the {{#tag:em|[[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s discussion page]]}} | {{ns:Category}} = a {{#tag:em|[[{{ns:Project}}:Categorization|category]]}} | {{ns:Help}} = a {{#tag:em|[[{{ns:Help}}:Contents|help page]]}} | {{ns:File}} = a {{#tag:em|[[{{ns:Project}}:Files to upload|file]]}} | {{ns:Portal}} = a {{#tag:em|[[{{ns:Project}}:Portal|portal]]}} | {{ns:Template}} = a {{#tag:em|[[{{ns:Project}}:Template messages|template]]}} | {{ns:User}} = a {{#tag:em|[[{{ns:Project}}:User pages|user page]]}} | {{ns:Project}} = a {{#tag:em|[[{{ns:Project}}:Project namespace|project page]]}} | {{ns:MediaWiki}} = a {{#tag:em|[[Special:AllMessages|system message]]}} | #default = a {{#tag:em|[[{{ns:Project}}:Namespaces|{{NAMESPACE}} page]]}} }} with this exact name yet.}} {{ #ifeq: {{#invoke:String|replace|source={{PAGENAME}}|­}} | {{#invoke:String|replace|source={{SUBPAGENAME}}|­}} | {{ #switch: {{NAMESPACE}} | {{ns:0}} = Please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=search for ''{{#invoke:String|replace|source={{PAGENAME}}|­}}'' in Test Wikipedia}} to check for alternative titles, names, or spellings. | {{ns:Talk}} = Before creating this page, please verify that a page called [[{{PAGENAME}}]] exists. | {{TALKSPACE}} = Before creating this page, please verify that [[:{{SUBJECTPAGENAME}}]] is already made. | {{ns:Category}} = Please browse the [[Portal:Contents|existing categories]] to check if this category is covered under another name. | {{ns:Help}} = Please browse the [[{{ns:Help}}:Contents|existing help pages]] to check if this help topic is covered under another name. | {{ns:File}} = Please do not manually create this page. If you want to upload ''File:{{PAGENAME}}'', Please see the [[{{ns:Project}}:Files to upload|Files to upload]] for instructions & tutorials. | {{ns:Portal}} = Please browse the [[{{ns:Portal}}:Contents/Portals|existing portals]] to check for similar topics. | {{ns:Template}} = Please browse the [[{{ns:Project}}:Template messages|existing templates]] to check if this standardized message is available under another name. | {{ns:User}} = In general, this page should be created and edited by [[User:{{PAGENAME}}]]. If you're already registered, Please [[Special:UserLogin|log in]]. If you don't have an account, [[Special:CreateAccount|create one]]. If you are {{PAGENAME}}, click [[Special:Edit/User:{{PAGENAME}}|create this page]]. If you are not {{PAGENAME}}, don't click "create this page". | {{ns:Project}} = Please browse the [[{{ns:Project}}:List of policies and guidelines|existing policies and guidelines]] or [[Special:Search/{{ns:Project}}:{{PAGENAME}}|search]] for similar existing project pages. | {{ns:MediaWiki}} = Please browse the [[Special:AllMessages|existing system messages]] to check if this Non-CSS, Non-JavaScript, Non-JSON, CSS, JavaScript, & JSON pages are available under another name. | #default = Please browse the [[{{ns:Project}}:Namespaces|existing {{NAMESPACE}} pages]] to check if this {{NAMESPACE}} namespace is available under another name. }} | {{ #ifeq: {{{nopermission}}} | yes |{{int:Namespaceprotected|{{#if: {{NAMESPACE}} | [[Wikipedia:Namespaces|{{NAMESPACE}}]] | [[Wikipedia:Namespaces|Page]] }}}} Please [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in] or [{{fullurl:Special:UserLogin/signup|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} sign up] if you want to create this page. (This page is currently protected so only administrators, system administrators, template editors, extended confirmed users, page movers, autoconfirmed users, & logged in users can make it, '''not''' temporary accounts or unregistered (IP) users). (If you're logged out, {{SITENAME}} has restricted the ability to create new pages. You can go back and edit an existing page, or [[Special:UserLogin|log in or create an account]]. If you're logged in, You do not have permission to create new pages.) Please see [[{{ns:Project}}:Subpages|Subpages]] for subpages if you do not create it because this isn't needed. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].| Before creating this page in the {{#if: {{NAMESPACE}} | '''[[Wikipedia:Namespaces|{{NAMESPACE}}]]''' namespace | '''[[Wikipedia:Namespaces|Page]]''' namespace }}, please see [[{{ns:Project}}:Subpages|Subpages]] for subpages. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].}} }} }}{{ #ifeq: {{{nopermission}}} | yes |* {{ #switch: {{#invoke:Effective protection level|create|{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}} | sysop = Administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:Administrators|administrators]] to create new pages, system messages, & JSON pages. | interfaceadmin = System administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[Wikipedia:System administrators|system administrators]] to create new CSS & JavaScript pages. | templateeditor = Template editors & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[WP:Template editor|template editors]] to create new pages. | extendedconfirmed = Extended confirmed users & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:User access levels#Extendedconfirmed|extended confirmed users]] to create new pages. | autoconfirmed = Autoconfirmed users can't edit this {{NAMESPACE}} page, but still be able to edit pages by users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to create new pages. (After 4 days & 10 edits on English Wikipedia.) | user = Some users can't edit this {{NAMESPACE}} page, but still be able to edit pages by anonymous users & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] to make this page. {{#ifeq:{{NAMESPACE}}|{{ns:Category}}|Or, you can [[Wikipedia:Articles for creation/Categories|request the creation of a new category]].}} | #default = Users with the "Create" permission can't edit this {{NAMESPACE}} page. (You do not have permission to create this page when not logged in). You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to make your own page. }} |{{#ifeq:{{#invoke:Title blacklist|main|action=create|pagename={{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}|templateeditor|* Administrators, template editors, & page movers can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[Wikipedia:Administrators|administrators]], [[Wikipedia:Template editors|template editors]], & [[Wikipedia:Page movers|page movers]] to create new ones.|* ''' {{ #switch: {{NAMESPACE}} | {{ns:3}} = {{#ifeq:{{ROOTPAGENAME}}|{{PAGENAME}}|[[Special:Edit/{{FULLPAGENAME}}|Post a message to ''{{#invoke:String|replace|source={{PAGENAME}}|­}}]]''|[[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]''}} | {{ns:5}} | {{ns:7}} | {{ns:9}} | {{ns:11}} | {{ns:13}} | {{ns:15}} | {{ns:101}} | {{ns:119}} | {{ns:711}} | {{ns:829}} | {{ns:1}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]'' | {{ns:Category}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'']] | {{ns:Project}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|start a discussion about the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}]]'' | #default = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'' page]] }}''' {{ #if: {{NAMESPACE}} |by using the '''"[[Wikipedia:Namespaces|{{NAMESPACE}}]]"''' namespace|by using the '''"[[Wikipedia:Namespaces|Page]]"''' namespace, using the [[Wikipedia:Article wizard|Article Wizard]] if you want to create it & submit it for review, or [[{{ns:Project}}:Requested articles|add a request for the new page]] }}.}} }}{{ #switch: {{NAMESPACE}} | {{ns:0}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing pages. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:User}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user page]]. | {{ns:User talk}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user talk page]]. | {{ns:Category}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing categories. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:File}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing files. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:Template}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing templates. * [[wikimedia:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wikimedia, the Free Templates, Categories, & More! | #default = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing {{#if: {{NAMESPACE}} | pages with the '''"[[{{ns:Project}}:Namespaces|{{NAMESPACE}}]]"''' namespace | pages without using [[{{ns:Project}}:Namespaces|namespaces]] }}. * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this title]]. }} * {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | You can go back to your [[:{{SUBJECTPAGENAME}}|subject page]] | Please discuss changes on your [[{{TALKPAGENAME}}|talk page]] }}. <div id="noarticletext_technical"> ---- '''Other reasons that may be displayed this message:''' * If {{ #ifeq: {{NAMESPACE}} | {{ns:File}} | a file | a page }} was recently edited here, it may be invisible. (Because of a delay in updating the database.) You can wait a few minutes to complete, you can [[Special:Purge/{{FULLPAGENAME}}|click it]] for purging, or you may want to [[Special:Protect/{{FULLPAGENAME}}|protect]] this page. You can also want to [[Special:Edit/{{FULLPAGENAME}}|edit this page]] or [[Special:Delete/{{FULLPAGENAME}}|delete this page]]. * Titles on the Test Wikipedia is in the '''[[w:en:Case sensitivity|case sensitivity]]''' except for this first character; please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=check the Test Wikipedia's alternative capitalizations}} and consider adding some [[Wikipedia:Redirects|redirects]] here to the correct title. If the page you are looking for is not TestSite, see its corresponding [[w:en:Main Page|language Wikipedia]]. If the page is not a [[w:en:test wiki|test wiki]], check one of the [[wikimedia:Main Page|other Foundation wikis]]. * If this page has been deleted, try [{{fullurl:Special:Log/delete|page={{#invoke:String|replace|source={{FULLPAGENAMEE}}|­}}}} checking the '''deletion log'''], and see [[Wikipedia:Why was the page I created deleted?|the Why was the page I created deleted?]] for possible reasons. </div> }} <!--End fmbox --><!--Start book box-->{{#if:{{#invoke:String|match|{{FULLPAGENAME}}|^Book:|nomatch= }}{{#invoke:String|match|{{FULLPAGENAME}}|^Book talk:|nomatch= }}|{{fmbox|type=system|text=<strong>This page may have existed as part of the former book & talk namespace.</strong> If so it was likely moved to [[{{#invoke:String|replace|{{#invoke:String|replace|{{FULLPAGENAME}}|^Book talk:|Book:|plain=False}}|^Book:|Wikipedia:Books/archive/|plain=False}}]] before being deleted. See [[Wikipedia:Books]] for more information.}}}}<!--End book box--> </div> == {{ #switch: {{NAMESPACE}} | {{ns:0}} = Page | {{ns:Talk}} = [[{{ns:Help}}:Using talk pages|Discussion page]] | {{TALKSPACE}} = [[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s Discussion page]] | {{ns:Category}} = [[{{ns:Project}}:Categorization|Category]] | {{ns:Help}} = [[{{ns:Help}}:Contents|Help page]] | {{ns:File}} = [[{{ns:Project}}:Files to upload|File]] | {{ns:Portal}} = [[{{ns:Project}}:Portal|Portal]] | {{ns:Template}} = [[{{ns:Project}}:Template messages|Template]] | {{ns:User}} = [[{{ns:Project}}:User pages|User page]] | {{ns:Project}} = [[{{ns:Project}}:Project namespace|Project page]] | {{ns:MediaWiki}} = [[Special:AllMessages|System message]] | #default = {{NAMESPACE}} page}} not showing up? == Reload the page you just created "'''{{FULLPAGENAME}}'''." <small>You may also [[Wikipedia:Administrators' noticeboard|contact an administrator]] before recreating this page.</small> If you can't find the page, you can [[Special:Search/{{FULLPAGENAME}}|search for an existing page]] or {{ #ifeq: {{{nopermission}}} | yes | <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{urlencode:{{FULLPAGENAME}}}}}} search the related logs]</span> | [[Special:Edit/{{FULLPAGENAME}}|create it]] }}. {{ #ifeq: {{{nopermission}}} | yes |<div class="mw-parser-output">{{fmbox | type = warning | id = noarticletext | image = none | text = You do not have permission to create this page. It may have been protected from creation. If I think this page should be created here, please remove protection at the [[Wikipedia:Administrators' noticeboard|admin noticeboard]].}}</div>|<div class="mw-parser-output">{{fmbox | type = system | id = noarticletext | image = none | text = If you can't find {{FULLPAGENAME}}, you can also include the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|veaction=edit}} visual editor]</span>, {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span>, or [[Special:NewSection/{{FULLPAGENAME}}|create a new section]] | or the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span> }} for editing.}}</div>}} <noinclude> {{documentation}} </noinclude> k7rgbdeo6h081id5o8mrv2e7jqikgvv 745186 745185 2026-06-04T14:49:47Z Sheffalump 71152 Removing the "Fmbox" template for |nopermission=yes 745186 wikitext text/x-wiki <div class="mw-parser-output"><!-- don't touch this div to the left, it makes TemplateStyles vroom vroom --> {{New page DYM}} {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} |{{#ifexist: {{SUBJECTPAGENAME}}|This is the talk page for [[:{{SUBJECTPAGENAME}}]].|[[:{{SUBJECTPAGENAME}}]] has not yet been created. {{Viewing talk page without subject}}|}}}} {{fmbox | type = system | id = noarticletext | image = none | textstyle = padding: 0.6em 0.9em; <!--Large box needs more padding--> | text = {{#tag:strong|WARNING:}} {{ #ifeq: {{ #ifeq: {{FULLPAGENAME}} | Template:No article text || }} | <!--Mainspace sister project infobox--> | {{No article text/sister projects}} }}{{ #ifeq: {{NAMESPACE}} | {{ns:User talk}} | {{#tag:strong|No messages}} have been posted for {{BASEPAGENAME}} yet. | {{#tag:strong|Wikipedia does not contain {{ #switch: {{NAMESPACE}} | {{ns:0}} = a {{#tag:em|[[{{ns:Project}}:Using pages without namespaces|page]]}} | {{ns:Talk}} = a {{#tag:em|[[{{ns:Help}}:Using talk pages|discussion page]]}} | {{TALKSPACE}} = the {{#tag:em|[[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s discussion page]]}} | {{ns:Category}} = a {{#tag:em|[[{{ns:Project}}:Categorization|category]]}} | {{ns:Help}} = a {{#tag:em|[[{{ns:Help}}:Contents|help page]]}} | {{ns:File}} = a {{#tag:em|[[{{ns:Project}}:Files to upload|file]]}} | {{ns:Portal}} = a {{#tag:em|[[{{ns:Project}}:Portal|portal]]}} | {{ns:Template}} = a {{#tag:em|[[{{ns:Project}}:Template messages|template]]}} | {{ns:User}} = a {{#tag:em|[[{{ns:Project}}:User pages|user page]]}} | {{ns:Project}} = a {{#tag:em|[[{{ns:Project}}:Project namespace|project page]]}} | {{ns:MediaWiki}} = a {{#tag:em|[[Special:AllMessages|system message]]}} | #default = a {{#tag:em|[[{{ns:Project}}:Namespaces|{{NAMESPACE}} page]]}} }} with this exact name yet.}} {{ #ifeq: {{#invoke:String|replace|source={{PAGENAME}}|­}} | {{#invoke:String|replace|source={{SUBPAGENAME}}|­}} | {{ #switch: {{NAMESPACE}} | {{ns:0}} = Please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=search for ''{{#invoke:String|replace|source={{PAGENAME}}|­}}'' in Test Wikipedia}} to check for alternative titles, names, or spellings. | {{ns:Talk}} = Before creating this page, please verify that a page called [[{{PAGENAME}}]] exists. | {{TALKSPACE}} = Before creating this page, please verify that [[:{{SUBJECTPAGENAME}}]] is already made. | {{ns:Category}} = Please browse the [[Portal:Contents|existing categories]] to check if this category is covered under another name. | {{ns:Help}} = Please browse the [[{{ns:Help}}:Contents|existing help pages]] to check if this help topic is covered under another name. | {{ns:File}} = Please do not manually create this page. If you want to upload ''File:{{PAGENAME}}'', Please see the [[{{ns:Project}}:Files to upload|Files to upload]] for instructions & tutorials. | {{ns:Portal}} = Please browse the [[{{ns:Portal}}:Contents/Portals|existing portals]] to check for similar topics. | {{ns:Template}} = Please browse the [[{{ns:Project}}:Template messages|existing templates]] to check if this standardized message is available under another name. | {{ns:User}} = In general, this page should be created and edited by [[User:{{PAGENAME}}]]. If you're already registered, Please [[Special:UserLogin|log in]]. If you don't have an account, [[Special:CreateAccount|create one]]. If you are {{PAGENAME}}, click [[Special:Edit/User:{{PAGENAME}}|create this page]]. If you are not {{PAGENAME}}, don't click "create this page". | {{ns:Project}} = Please browse the [[{{ns:Project}}:List of policies and guidelines|existing policies and guidelines]] or [[Special:Search/{{ns:Project}}:{{PAGENAME}}|search]] for similar existing project pages. | {{ns:MediaWiki}} = Please browse the [[Special:AllMessages|existing system messages]] to check if this Non-CSS, Non-JavaScript, Non-JSON, CSS, JavaScript, & JSON pages are available under another name. | #default = Please browse the [[{{ns:Project}}:Namespaces|existing {{NAMESPACE}} pages]] to check if this {{NAMESPACE}} namespace is available under another name. }} | {{ #ifeq: {{{nopermission}}} | yes |{{int:Namespaceprotected|{{#if: {{NAMESPACE}} | [[Wikipedia:Namespaces|{{NAMESPACE}}]] | [[Wikipedia:Namespaces|Page]] }}}} Please [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in] or [{{fullurl:Special:UserLogin/signup|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} sign up] if you want to create this page. (This page is currently protected so only administrators, system administrators, template editors, extended confirmed users, page movers, autoconfirmed users, & logged in users can make it, '''not''' temporary accounts or unregistered (IP) users). (If you're logged out, {{SITENAME}} has restricted the ability to create new pages. You can go back and edit an existing page, or [[Special:UserLogin|log in or create an account]]. If you're logged in, You do not have permission to create new pages.) Please see [[{{ns:Project}}:Subpages|Subpages]] for subpages if you do not create it because this isn't needed. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].| Before creating this page in the {{#if: {{NAMESPACE}} | '''[[Wikipedia:Namespaces|{{NAMESPACE}}]]''' namespace | '''[[Wikipedia:Namespaces|Page]]''' namespace }}, please see [[{{ns:Project}}:Subpages|Subpages]] for subpages. To protect the page, click [[Special:Protect/{{FULLPAGENAME}}|protect]].}} }} }}{{ #ifeq: {{{nopermission}}} | yes |* {{ #switch: {{#invoke:Effective protection level|create|{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}} | sysop = Administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:Administrators|administrators]] to create new pages, system messages, & JSON pages. | interfaceadmin = System administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[Wikipedia:System administrators|system administrators]] to create new CSS & JavaScript pages. | templateeditor = Template editors & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[WP:Template editor|template editors]] to create new pages. | extendedconfirmed = Extended confirmed users & administrators can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will</span> be [[Wikipedia:User access levels#Extendedconfirmed|extended confirmed users]] to create new pages. | autoconfirmed = Autoconfirmed users can't edit this {{NAMESPACE}} page, but still be able to edit pages by users, anonymous users, & temporally accounts. You will <span class="anononly">need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will </span>be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to create new pages. (After 4 days & 10 edits on English Wikipedia.) | user = Some users can't edit this {{NAMESPACE}} page, but still be able to edit pages by anonymous users & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] to make this page. {{#ifeq:{{NAMESPACE}}|{{ns:Category}}|Or, you can [[Wikipedia:Articles for creation/Categories|request the creation of a new category]].}} | #default = Users with the "Create" permission can't edit this {{NAMESPACE}} page. (You do not have permission to create this page when not logged in). You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[WP:User access levels#Autoconfirmed users|autoconfirmed users]] to make your own page. }} |{{#ifeq:{{#invoke:Title blacklist|main|action=create|pagename={{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}|templateeditor|* Administrators, template editors, & page movers can't edit this {{NAMESPACE}} page, but still be able to edit pages by autoconfirmed users, users, anonymous users, & temporally accounts. You will need to [{{fullurl:Special:UserLogin|returnto={{ urlencode: {{#invoke:String|replace|source={{FULLPAGENAME}}|­}} }}}} log in or create an account] and will be [[Wikipedia:Administrators|administrators]], [[Wikipedia:Template editors|template editors]], & [[Wikipedia:Page movers|page movers]] to create new ones.|* ''' {{ #switch: {{NAMESPACE}} | {{ns:3}} = {{#ifeq:{{ROOTPAGENAME}}|{{PAGENAME}}|[[Special:Edit/{{FULLPAGENAME}}|Post a message to ''{{#invoke:String|replace|source={{PAGENAME}}|­}}]]''|[[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]''}} | {{ns:5}} | {{ns:7}} | {{ns:9}} | {{ns:11}} | {{ns:13}} | {{ns:15}} | {{ns:101}} | {{ns:119}} | {{ns:711}} | {{ns:829}} | {{ns:1}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|leave a message about the ''{{#invoke:String|replace|source={{SUBJECTPAGENAME}}|­}}]]'' | {{ns:Category}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'']] | {{ns:Project}} = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}'' page]] or [[Special:NewSection/{{FULLPAGENAME}}|start a discussion about the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}]]'' | #default = [[Special:Edit/{{FULLPAGENAME}}|Start the ''{{#invoke:String|replace|source={{FULLPAGENAME}}|}}'' page]] }}''' {{ #if: {{NAMESPACE}} |by using the '''"[[Wikipedia:Namespaces|{{NAMESPACE}}]]"''' namespace|by using the '''"[[Wikipedia:Namespaces|Page]]"''' namespace, using the [[Wikipedia:Article wizard|Article Wizard]] if you want to create it & submit it for review, or [[{{ns:Project}}:Requested articles|add a request for the new page]] }}.}} }}{{ #switch: {{NAMESPACE}} | {{ns:0}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing pages. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:User}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user page]]. | {{ns:User talk}} = * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this user talk page]]. | {{ns:Category}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing categories. * [[wiktionary:en:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wiktionary, our sister dictionary project. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:File}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing files. * [[commons:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in the Wikimedia Commons, our repository for free images, music, sound, and video. | {{ns:Template}} = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing templates. * [[wikimedia:Special:Search/{{FULLPAGENAME}}|Look for "{{FULLPAGENAME}}"]] in Wikimedia, the Free Templates, Categories, & More! | #default = * {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{PAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1&ns{{NAMESPACENUMBER}}=1|name=Search for "''{{#invoke:String|replace|source={{PAGENAME}}|­}}''"}} in existing {{#if: {{NAMESPACE}} | pages with the '''"[[{{ns:Project}}:Namespaces|{{NAMESPACE}}]]"''' namespace | pages without using [[{{ns:Project}}:Namespaces|namespaces]] }}. * [[{{ns:Special}}:WhatLinksHere/{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}|Look for pages within the Test Wikipedia that link to this title]]. }} * {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | You can go back to your [[:{{SUBJECTPAGENAME}}|subject page]] | Please discuss changes on your [[{{TALKPAGENAME}}|talk page]] }}. <div id="noarticletext_technical"> ---- '''Other reasons that may be displayed this message:''' * If {{ #ifeq: {{NAMESPACE}} | {{ns:File}} | a file | a page }} was recently edited here, it may be invisible. (Because of a delay in updating the database.) You can wait a few minutes to complete, you can [[Special:Purge/{{FULLPAGENAME}}|click it]] for purging, or you may want to [[Special:Protect/{{FULLPAGENAME}}|protect]] this page. You can also want to [[Special:Edit/{{FULLPAGENAME}}|edit this page]] or [[Special:Delete/{{FULLPAGENAME}}|delete this page]]. * Titles on the Test Wikipedia is in the '''[[w:en:Case sensitivity|case sensitivity]]''' except for this first character; please {{plain link|url=https://test.wikipedia.org/w/index.php?search={{urlencode:{{#titleparts:{{#invoke:String|replace|source={{FULLPAGENAME}}|­}}}}}}&title=Special%3ASearch&fulltext=1|name=check the Test Wikipedia's alternative capitalizations}} and consider adding some [[Wikipedia:Redirects|redirects]] here to the correct title. If the page you are looking for is not TestSite, see its corresponding [[w:en:Main Page|language Wikipedia]]. If the page is not a [[w:en:test wiki|test wiki]], check one of the [[wikimedia:Main Page|other Foundation wikis]]. * If this page has been deleted, try [{{fullurl:Special:Log/delete|page={{#invoke:String|replace|source={{FULLPAGENAMEE}}|­}}}} checking the '''deletion log'''], and see [[Wikipedia:Why was the page I created deleted?|the Why was the page I created deleted?]] for possible reasons. </div> }} <!--End fmbox --><!--Start book box-->{{#if:{{#invoke:String|match|{{FULLPAGENAME}}|^Book:|nomatch= }}{{#invoke:String|match|{{FULLPAGENAME}}|^Book talk:|nomatch= }}|{{fmbox|type=system|text=<strong>This page may have existed as part of the former book & talk namespace.</strong> If so it was likely moved to [[{{#invoke:String|replace|{{#invoke:String|replace|{{FULLPAGENAME}}|^Book talk:|Book:|plain=False}}|^Book:|Wikipedia:Books/archive/|plain=False}}]] before being deleted. See [[Wikipedia:Books]] for more information.}}}}<!--End book box--> </div> == {{ #switch: {{NAMESPACE}} | {{ns:0}} = Page | {{ns:Talk}} = [[{{ns:Help}}:Using talk pages|Discussion page]] | {{TALKSPACE}} = [[{{ns:Help}}:Using talk pages|{{SUBJECTSPACE}}'s Discussion page]] | {{ns:Category}} = [[{{ns:Project}}:Categorization|Category]] | {{ns:Help}} = [[{{ns:Help}}:Contents|Help page]] | {{ns:File}} = [[{{ns:Project}}:Files to upload|File]] | {{ns:Portal}} = [[{{ns:Project}}:Portal|Portal]] | {{ns:Template}} = [[{{ns:Project}}:Template messages|Template]] | {{ns:User}} = [[{{ns:Project}}:User pages|User page]] | {{ns:Project}} = [[{{ns:Project}}:Project namespace|Project page]] | {{ns:MediaWiki}} = [[Special:AllMessages|System message]] | #default = {{NAMESPACE}} page}} not showing up? == Reload the page you just created "'''{{FULLPAGENAME}}'''." <small>You may also [[Wikipedia:Administrators' noticeboard|contact an administrator]] before recreating this page.</small> If you can't find the page, you can [[Special:Search/{{FULLPAGENAME}}|search for an existing page]] or {{ #ifeq: {{{nopermission}}} | yes | <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{urlencode:{{FULLPAGENAME}}}}}} search the related logs]</span> | [[Special:Edit/{{FULLPAGENAME}}|create it]] }}. {{ #ifeq: {{{nopermission}}} | yes | You do not have permission to create this page. {{#if: {{PROTECTIONLEVEL:create}} | This page is protected from creation. <span class="group-user-show">If I think this page should be created here, please remove protection at the [[Wikipedia:Administrators' noticeboard|admin noticeboard]].</span> | You can't edit pages, or is protected. }} |<div class="mw-parser-output">{{fmbox | type = system | id = noarticletext | image = none | text = If you can't find {{FULLPAGENAME}}, you can also include the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|veaction=edit}} visual editor]</span>, {{ #ifeq: {{NAMESPACE}} | {{TALKSPACE}} | the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span>, or [[Special:NewSection/{{FULLPAGENAME}}|create a new section]] | or the <span class="planlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit&veswitched=1}} source editor]</span> }} for editing.}}</div>}} <noinclude> {{documentation}} </noinclude> iqmd2cdsyf1i6grjj9ufmued1fmjpwd World 0 85319 745211 744956 2026-06-04T22:11:50Z SherryYang-WMF 74266 testing whether depictions show in image carousel if I suppress for my account 745211 wikitext text/x-wiki {{Use British English}} [[File:The Earth seen from Apollo 17.jpg|alt=A view of the Earth taken from outer space|thumb|This photo of this world is called ''The Blue Marble''.|300x300px]] This '''world''' is a planet where a society of people has formed. Authors sometimes invent new worlds. The authors use these worlds as the setting for their stories. Some authors invent worlds that have magic. Nobody knows whether there are intelligent beings on other worlds. == World literature == World literature is literature that is read by many [[people]] all over this world. World literature is different from national literature.<blockquote>I am more and more convinced that poetry is the universal possession of mankind, revealing itself everywhere and at all times in hundreds and hundreds of men. . . . I therefore like to look about me in foreign nations, and advise everyone to do the same. National literature is now a rather unmeaning term; the epoch of world literature is at hand. – Johann Wolfgang von Goethe, 1827</blockquote> == Worlds in literature == Creating a different world is a literary device used by authors to illustrate ideas. By placing the story in the setting of a different world, the author can change the way that things happen in the world. For example, the author might imagine a world that has very little water or a world that has very little dry land. Deciding what the world looks like and how the world works is called ''world-building''. Thinking about the world helps the author make good choices about what happens to the characters in the story. Some authors think about many details, such as what [[languages]] the characters speak and what the [[architecture]] is on the world. === Worlds in science fiction === <div class="depiction">[[File:951020 STS73 Columbia launch.jpg|alt=A space shuttle in the sky|thumb|Characters in science fiction stories may travel to other worlds]]</div> Science fiction stories often use different worlds. Frank Herbert's famous ''Dune'' series focused on a world called Arrakis that produces a rare chemical substance. Often a science-fiction story will be involve multiple worlds. The ''Foundation'' series by Isaac Asimov was set in a galaxy with thousands of populated worlds. The Star Wars movies had a several important worlds, and characters traveled between them. Some authors of science fiction worlds try to make them scrupulously obey the laws of physics. === Fantasy worlds === Fantasy worlds are fictional worlds that use magic. This magic may involve saying [[magic words]], using magical objects, or performing [[backup|magical rituals]]. {| class="wikitable" |+Fantasy worlds !Author !World !Source !Description |- |J.R.R. Tolkien |Middle Earth |''The Lord of the Rings'' triology |Middle-earth has some qualities similar to Mediæval Europe. The author added magical creatures like elves and wizards. At the end of the story, some magical creatures leave the world. |- |C. S. Lewis  |Narnia |''The Chronicles of Narnia'' series |The whole world is named after the principal country, Narnia. It features a powerful lion, an evil witch, giants, dragons, and some magical devices. This world is flat. |- |Piers Anthony |Xanth |''The Magic of Xanth'' |This world has many magical things. It is connected to modern America. Each creature in Xanth has a unique magical talent. These talents are usually minor. Translating the book is difficult because of the many silly puns. These make sense in English but not necessarily in other languages. |} == World languages == Some languages spoken in many parts of the world. These are called ''world languages''. {{As of|2016}}, English is the most common world language. Previously, French was the most popular language in the West. Chinese was used by traders in all of East Asia for centuries. Arabic is common in the Middle East, Northern Africa, and other parts of the world. Once upon a time, Greek and Latin were spoken by most traders in the Western world. Before then, traders learned the languages of nearby cities. Ancient traders did not travel around the world. The Egyptians used pictures to write their language. The pictures are called hieroglyphics. This is what they look like:<!-- Hieroglyphics are shown as PNG images on wiki. Not all wikis support hieroglyphics. --><hiero> A1-B2-D4-E5 </hiero><hiero> F6 p*p*p:t*t*t </hiero> == World health == {{Sidebar|content1 = World Health Organisation|content2 = International Red Cross and Red Crescent Movement|content3 = Médecins Sans Frontières‎|title = International health organisations|image = [[File:Esclapius stick 1 coloured.svg|55px|class=noviewer]]}} The World Health Organisation (French: <span lang="fr" dir="ltr">Organisation mondiale de la santé</span>) is an international organisation for public health.<ref name=":0">Burci, Gian Luca; Vignes, Claude-Henri (2004-01-01). [https://books.google.com/books?id=Xou_nD9jJF0C ''World Health Organization'']. Kluwer Law International. [[w:International Standard Book Number|ISBN]]&nbsp;[[Special:BookSources/9789041122735|9789041122735]]. Pages 15–20.</ref> It is part of the United Nations. The World Health Organisation began in 1948.<ref name=":0" /> It wants people to be healthy and safe. It studies public health and tells governments and other organisations how to help people become healthy. The organisation counts the number of people with health problems. These health problems include influenza, HIV infection, and depression. It also counts the number of people who experience other problems. These problems include dirty water, violence, and hunger. Mental health is also important.<ref>{{Cite journal|title = Mortality in mental disorders and global disease burden implications: a systematic review and meta-analysis|url = http://www.ncbi.nlm.nih.gov/pubmed/25671328|journal = JAMA psychiatry|date = 2015-04-01|issn = 2168-6238|pmc = 4461039|pmid = 25671328|pages = 334-341|volume = 72|issue = 4|doi = 10.1001/jamapsychiatry.2014.2502|first = Elizabeth Reisinger|last = Walker|first2 = Robin E.|last2 = McGee|first3 = Benjamin G.|last3 = Druss}} {{Open access}}</ref> People with mental health problems such as depression often die ten years early. The World Bank is also interested in health.<ref>{{Cite journal|title = Essential surgery: key messages from Disease Control Priorities, 3rd edition|url = http://www.ncbi.nlm.nih.gov/pubmed/25662414|journal = Lancet (London, England)|date = 2015-05-30|issn = 1474-547X|pmid = 25662414|pages = 2209-2219|volume = 385|issue = 9983|doi = 10.1016/S0140-6736(15)60091-5|first = Charles N.|last = Mock|first2 = Peter|last2 = Donkor|first3 = Atul|last3 = Gawande|first4 = Dean T.|last4 = Jamison|first5 = Margaret E.|last5 = Kruk|first6 = Haile T.|last6 = Debas}}</ref> Health affects economic prospects. == Poetry == This poem by John Donne mentions the world: <poem> Her death hath taught us dearly that thou art Corrupt and mortal in thy purest part. Let no man say, the world itself being dead, 'Tis labour lost to have discovered The world's infirmities, since there is none Alive to study this dissection; For there's a kind of world remaining still, Though she which did inanimate and fill The world, be gone... </poem>The entire poem can be [[s:An Anatomy of the World—The First Anniversery|read at Wikisource]]. == Shape of this world == This world is not a perfect sphere. It is slightly flattened. This is the mathematical formula for measuring the flatness of a sphere: <math>\begin{align} f&=\frac{a-b}{a}. \end{align}</math> For this world, <math display="inline">f\,\!</math> is approximately 0.3%. The Moon is rounder. For the Moon, <math display="inline">f\,\!</math> is approximately 0.1%. Jupiter is flatter. For Jupiter, <math display="inline">f\,\!</math> is approximately 6.5%. == Local planets == A world is on a planet. There are different types of planets. There are several types of planets in this solar system: * Terrestrial planets * Giant planets ** Gas giants ** Ice giants These are the planets in this solar system: * Four terrestrial planets *# Mercury *# Venus *# Earth *#* One satellite, called the Moon *# Mars * Two gas giants *# Jupiter *#* Four large satellites *#* 63 other satellites *# Saturn *#* 62 satellites. Some are very small. The largest, called ''Titan'', is larger than the planet Mercury. *#* Seven are large. * Two ice giants *# Uranus *#* Five satellites *#* 22 other satellites *# Neptune *#* One satellite, called ''Triton'' == Gallery == <gallery caption="Comparison of local planets (not to scale)"> File:Reprocessed Mariner 10 image of Mercury.jpg|Mercury File:Venus globe.jpg|Venus File:North America from low orbiting satellite Suomi NPP.jpg|Earth File:Mars 23 aug 2003 hubble.jpg|Mars File:Jupiter New Horizons.jpg|Jupiter File:Saturn during Equinox.jpg|Saturn |Uranus File:Neptune Full.jpg|Neptune </gallery> == Duplicate image == For testing purposes: [[File:The Earth seen from Apollo 17.jpg|alt=|thumb||200x200px]] [[File:Saturn during Equinox.jpg|alt=|thumb||200x200px]] == Footnotes == <references /> == External links == * [https://www.example.com Example link] – an external link to example.com * [https://www.example.com] – an external link without a label n0v7pi3xmuyopnvqw2r5ecx9444h05m User:Oshwah/common.js 2 88094 745216 513513 2026-06-05T04:42:54Z Oshwah 26741 Import script. 745216 javascript text/javascript /*importScript('User:Oshwah/SockBlock.js');*/ /*importScript('User:Oshwah/EvasionBlock.js');*/ /*importScript('User:Oshwah/UPOLHardBlock.js');*/ /*importScript('User:Oshwah/UPOLSoftBlock.js');*/ /*importScript('User:Oshwah/AbusiveUsernameBlock.js')*/ /*importScript('User:Oshwah/PromoHardBlock.js');*/ /*importScript('User:Oshwah/PromoSoftBlock.js');*/ /*importScript('User:Oshwah/Test.js');*/ importScript('User:Oshwah/QuickBlockScript.js'); importScript('User:Oshwah/MarkBlockedGadgetTest.js'); importScript('User:Oshwah/History-Log-Links.js'); importScript('User:Oshwah/NewUserPatrol-TEST.js'); //importScript('User:Steven Crossin/DRW.js'); //Testing DRW 74zm3h3b4y7cndibkiloxau7kpxsnhk 745218 745216 2026-06-05T04:46:22Z Oshwah 26741 Add settings for NewUserPatrol-TEST.js. 745218 javascript text/javascript /*importScript('User:Oshwah/SockBlock.js');*/ /*importScript('User:Oshwah/EvasionBlock.js');*/ /*importScript('User:Oshwah/UPOLHardBlock.js');*/ /*importScript('User:Oshwah/UPOLSoftBlock.js');*/ /*importScript('User:Oshwah/AbusiveUsernameBlock.js')*/ /*importScript('User:Oshwah/PromoHardBlock.js');*/ /*importScript('User:Oshwah/PromoSoftBlock.js');*/ /*importScript('User:Oshwah/Test.js');*/ importScript('User:Oshwah/QuickBlockScript.js'); importScript('User:Oshwah/MarkBlockedGadgetTest.js'); importScript('User:Oshwah/History-Log-Links.js'); importScript('User:Oshwah/NewUserPatrol-TEST.js'); nup_log_list_enabled = true; nup_log_list_number_of_logs = 20; nup_log_list_refresh_rate = 5; //importScript('User:Steven Crossin/DRW.js'); //Testing DRW kl42lbmhejgm4zndfp3fdky7f5m2b61 745219 745218 2026-06-05T06:21:20Z Oshwah 26741 Import script and settings. 745219 javascript text/javascript /*importScript('User:Oshwah/SockBlock.js');*/ /*importScript('User:Oshwah/EvasionBlock.js');*/ /*importScript('User:Oshwah/UPOLHardBlock.js');*/ /*importScript('User:Oshwah/UPOLSoftBlock.js');*/ /*importScript('User:Oshwah/AbusiveUsernameBlock.js')*/ /*importScript('User:Oshwah/PromoHardBlock.js');*/ /*importScript('User:Oshwah/PromoSoftBlock.js');*/ /*importScript('User:Oshwah/Test.js');*/ importScript('User:Oshwah/QuickBlockScript.js'); importScript('User:Oshwah/MarkBlockedGadgetTest.js'); importScript('User:Oshwah/History-Log-Links.js'); importScript('User:Oshwah/NewUserPatrol-TEST.js'); nup_log_list_enabled = true; //Variables to set user-defined settings and values for NewUserPatrol-TEST.js. nup_log_list_number_of_logs = 20; nup_log_list_refresh_rate = 5; importScript('User:Oshwah/AbuseFilterPatrol-TEST.js'); afp_log_list_enabled = true; //Variables to set user-defined settings and values for AbuseFilterPatrol-TEST.js. afp_log_list_number_of_logs = 20; afp_log_list_refresh_rate = 5; //importScript('User:Steven Crossin/DRW.js'); //Testing DRW p943fxnw1qey0azfleshnmg2g5psple 745220 745219 2026-06-05T06:25:14Z Oshwah 26741 Undo script and settings insertion. There's no point; filters 51 and 53 would also have to be here as well, so I decided to scrap importing this script. 745220 javascript text/javascript /*importScript('User:Oshwah/SockBlock.js');*/ /*importScript('User:Oshwah/EvasionBlock.js');*/ /*importScript('User:Oshwah/UPOLHardBlock.js');*/ /*importScript('User:Oshwah/UPOLSoftBlock.js');*/ /*importScript('User:Oshwah/AbusiveUsernameBlock.js')*/ /*importScript('User:Oshwah/PromoHardBlock.js');*/ /*importScript('User:Oshwah/PromoSoftBlock.js');*/ /*importScript('User:Oshwah/Test.js');*/ importScript('User:Oshwah/QuickBlockScript.js'); importScript('User:Oshwah/MarkBlockedGadgetTest.js'); importScript('User:Oshwah/History-Log-Links.js'); importScript('User:Oshwah/NewUserPatrol-TEST.js'); nup_log_list_enabled = true; nup_log_list_number_of_logs = 20; nup_log_list_refresh_rate = 5; //importScript('User:Steven Crossin/DRW.js'); //Testing DRW kl42lbmhejgm4zndfp3fdky7f5m2b61 QuickCategories CI Test/2 0 103056 745231 414063 2026-06-05T08:08:49Z ~2026-33107-70 74304 745231 wikitext text/x-wiki Test page for the QuickCategories tool. af edit aao3wwcylwj798af7z7afzz03w024vi 745232 745231 2026-06-05T08:11:33Z ~2026-33107-70 74304 745232 wikitext text/x-wiki Test page for the QuickCategories tool. af edit af edit 9judfst1ea3jn0jmp1obw6w92rifb6x Poop 0 106057 745180 724000 2026-06-04T13:24:04Z ~2026-33225-26 74291 745180 wikitext text/x-wiki Right-o! captcha test captcha test poop poop! Rumps doo! 5i985skmoatwljbnz3oa4efhw9k8jdh 745181 745180 2026-06-04T13:31:32Z DBrant8 69202 745181 wikitext text/x-wiki Right-o! captcha test captcha test poop poop! captcha test Rumps doo! ivkpwo3zkwlndtstx2jqo2gw4i6ev7t 745182 745181 2026-06-04T13:32:48Z DBrant8 69202 745182 wikitext text/x-wiki Right-o! captcha test captcha test poop poop captcha test captcha test! Rumps doo! e000ndlecpjhtqhaux3n1jodj33yt5l 🌀︎ 0 116353 745196 452666 2026-06-04T16:37:39Z ~2026-33209-22 74289 /* */ test 745196 wikitext text/x-wiki U+1F300 CYCLONE with U+FE0E VARIATION SELECTOR-15 [[Category:Emoji]] test ftwgiigzhewj5j9vp7ontovofzd6l4s 745197 745196 2026-06-04T17:03:57Z ~2026-33209-22 74289 /* */ test 745197 wikitext text/x-wiki U+1F300 CYCLONE with U+FE0E VARIATION SELECTOR-15 [[Category:Emoji]] testtest 8esvjgsby398gfzbib99pk4p0m5vi49 User:Oshwah/History-Log-Links.js 2 126695 745221 679694 2026-06-05T06:28:13Z Oshwah 26741 Update to test and see if the changes I made on en-wiki, cloned here, work exactly the same without issue. 745221 javascript text/javascript //<nowiki> mw.loader.using("mediawiki.util", function () { const action = mw.config.get("wgAction"); //Grabs the current action (e.g. 'view', 'edit', 'history'). if (action === "history") { //Only proceed if this is an action page and if the current action page is 'history'. $("li:has(.mw-userlink):has(.mw-usertoollinks)").each(function () { //Loop through <li>s that have '.mw-userlink' AND '.mw-usertoollinks' inside them. const user_tool_links = $(this).find(".mw-usertoollinks").first().html(); //Grab the HTML of the user tool links for the current user. const relevant_user = $(this).find(".mw-userlink").first().text(); //Grab the username of the relevant user. const encoded_username = encodeURIComponent(relevant_user); //Create all the links we want to append. const usertalk_link = user_tool_links.match('^(.*)>talk</a>'); //Grab existing HTML of the user talk page link from the existing '.mw-usertoollinks' HTML. This also captures the opening <span> tag for the entire list of user links. const contribs_temp = user_tool_links.match('<a href="/wiki/Special:Contributions(.*)>contribs</a>'); const contribs_link = ((contribs_temp == null)?('<a href="/wiki/Special:Contributions/' + relevant_user + '" class="mw-usertoollinks-contribs" title="Special:Contributions/' + relevant_user + '">contribs</a>'):(contribs_temp[0])); //If the current user's tool links don't contain a contribs link (IP address), we must create our own instead. const publiclog_link = '<a href="/wiki/Special:Log?user=' + encoded_username + '">logs</a>'; const checkuser_link = '<a href="/w/index.php?title=Special:CheckUser&user=' + encoded_username + '">checkuser</a>'; const checklog_link = '<a href="/w/index.php?title=Special:CheckUserLog&cuSearchType=target&cuSearch=' + encoded_username + '">checks</a>'; const block_link = '<a href="/wiki/Special:Block/' + encoded_username + '" class="mw-usertoollinks-block" title="Special:Block/' + relevant_user + '">block</a>'; let new_links = [usertalk_link[0]]; //Create array of new tool links, set first element to user talk page link. new_links.push(contribs_link, publiclog_link); //Add contribs and public log links to the array. if (mw.util.isIPAddress(relevant_user)) { //If the current user is an IP address, we have IP user tool links to add as well. const whois_link = '<a href="https://tools.wmflabs.org/whois-referral/gateway.py?lookup=true&ip=' + encoded_username + '">WHOIS</a>'; //Create IP user tool links const geolocate_link = '<a href="https://whatismyipaddress.com/ip/' + relevant_user + '">geolocate</a>'; const proxy_check_link = '<a href="https://spur.us/context/' + relevant_user + '">proxy</a>'; const bullseye_check_link = '<a href="https://bullseye.toolforge.org/ip/' + relevant_user + '">bullseye</a>'; new_links.push(whois_link, geolocate_link, proxy_check_link, bullseye_check_link); //Add IP user tool links to the array. } new_links.push(checkuser_link, checklog_link, block_link); //Add ending user tool links to the array. const new_links_str = new_links.join('</span> <span>') + '</span>'; //Join each element of the new user tool links together, separated by '</span> <span>' (which automatically adds the " | " separators), add a closing </span> tag, and place into to a singular string. $(this).find(".mw-usertoollinks").first().html(new_links_str); //Write the new HTML string back to the user '.mw-usertoollinks' span object and replace the old HTML. }); } }); //</nowiki> jght3fw49cu1qmmfu7r28pcpf8wpesx Mwbot-rs/FileUsage 0 153352 745234 582214 2026-06-05T08:50:23Z ~2026-33107-70 74304 showcaptcha warning 745234 wikitext text/x-wiki [[File:A_keyboard.jpg|341x341px]] af edit j074jhw7yu6m30lk10chxd507v06wqs Projectwiki 0 153925 745204 741531 2026-06-04T21:54:57Z Solidest 54422 [[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/4 июня 2026#Projectwiki]] 745204 wikitext text/x-wiki <noinclude>{{к удалению|2026-06-04}}</noinclude> the sample ocr page Af edit Normal edit [https://djfdsjl.com] Normal edit [https://jsdkfldsjl.com] Af edit normal edit t6fvuwrg3emxieoqgxllm4joonyld5v 745208 745204 2026-06-04T21:55:31Z Solidest 54422 [[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/4 июня 2026#Projectwiki]] — Снято с удаления 745208 wikitext text/x-wiki the sample ocr page Af edit Normal edit [https://djfdsjl.com] Normal edit [https://jsdkfldsjl.com] Af edit normal edit i2fbvljtkfd0d9vujfdcj3gw9jbda7y Test 0 155073 745223 745174 2026-06-05T07:44:29Z Pginer-WMF 19605 /* Testing */ 745223 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test == Testing == cross-origin edit == Testing == cross-origin edit 92o21bhxxcpdk03yc4u623xpe3dilvg 745224 745223 2026-06-05T07:44:39Z Pginer-WMF 19605 /* Testing */ 745224 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test == Testing == cross-origin edit == Testing == cross-origin edit dhu77dcx07n80mwpyjq06j1mqg92r02 745225 745224 2026-06-05T07:44:49Z Pginer-WMF 19605 /* Testing */ 745225 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test Test == Testing == cross-origin edit == Testing == cross-origin edit hhm1laaifdjany88n4neysokw7etzpl 745226 745225 2026-06-05T07:46:01Z Pginer-WMF 19605 /* Testing */ 745226 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test Test Test == Testing == cross-origin edit == Testing == cross-origin edit quo0frwng4cbcmh9wehg0u068qr3biv 745227 745226 2026-06-05T07:46:10Z Pginer-WMF 19605 /* Testing */ 745227 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test Test Test Test == Testing == cross-origin edit == Testing == cross-origin edit 0wbxvesp2u7pc0djjntxno14o88tye9 745228 745227 2026-06-05T07:46:17Z Pginer-WMF 19605 /* Testing */ 745228 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test Test Test Test Test == Testing == cross-origin edit == Testing == cross-origin edit j1k0hvo3azja6js9k9dvvivvdadr969 745229 745228 2026-06-05T07:46:26Z Pginer-WMF 19605 /* Testing */ 745229 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit Test Test Test Test Test TestTestTest == Testing == cross-origin edit == Testing == cross-origin edit rsg5bgvdpnk4cjdil56ohidzxlz0pgi 745230 745229 2026-06-05T07:46:34Z Pginer-WMF 19605 /* Testing */ 745230 wikitext text/x-wiki <noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test........ https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>. == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit 3435mjtl119qk866w42wft8re0t2a4y খসড়া:Testmore' 0 168565 745233 677760 2026-06-05T08:47:13Z ~2026-33107-70 74304 warning showcaptcha 745233 wikitext text/x-wiki {{AFC draft}} foobar e3ibltg9wpo9xjdlg9wmp4ziguukoyt User:Solidest/remover-core.js 2 174846 745203 745121 2026-06-04T21:54:44Z Solidest 54422 745203 javascript text/javascript /** * Remover — ядро (core). * Загружается лениво при первом клике по пункту меню. * Ожидает, что window.RemoverState уже задан remover-loader.js. * * Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты). * Логика выполнения сосредоточена в универсальных обработчиках. * Экспортирует: window.RemoverCore.handleMenuClick(item, event) */ (function () { 'use strict'; var state = window.RemoverState; if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; } var mwCfg = state.mwCfg; var cfg = applyCoreConfigDefaults(state.cfg || {}); var isCategory = state.isCategory; var isVector22 = state.isVector22; var scriptLink = cfg.scriptLink; var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings'; var settingsVersion = 1; var settingsMenuMeta = collectSettingsMenuMeta(); var settingsArticleItemLabels = settingsMenuMeta.articleLabels; var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels; var settingsItemLabelById = settingsMenuMeta.idToLabel; var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm; var settingsItemLabelOrder = settingsMenuMeta.labelOrder; var settingsDefaults = getDefaultSettings(); var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__'; var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__'; var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__'; var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {})); var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor; var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic; var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string') ? state.signatureSeparator.trim() : initialSettings.signatureSeparator; initialSettings.notifyAuthor = setAlert; initialSettings.subscribeTopic = setSubscribe; initialSettings.signatureSeparator = signatureSeparator; state.cfg = cfg; state.settings = clonePlainObject(initialSettings); state.setAlert = setAlert; state.setSubscribe = setSubscribe; // ─── Константы ────────────────────────────────────────────────────────── var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь']; var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря']; var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); }); var T_OPEN = '{' + '{'; var T_CLOSE = '}' + '}'; var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления'; var CATEGORY_NOMINATION_META = { discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true }, deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true }, rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true }, merge: { title: 'Номинация: к объединению', actionText: 'к объединению' } }; var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g; var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i; var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i; var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i; var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i; var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds'; var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i'); var KUL_PATTERN_STR = 'к\\s*улучшению'; var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i'); var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on'; var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i'); var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/; var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/; var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/; var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/; var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/; var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i; var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i; // ─── Глобальные переменные сессии ──────────────────────────────────────── var isError = false; var logStatusSeq = 0; var resizeObservers = []; var modalLayoutSyncHandlers = []; var tplAliasCache = {}; // ─── Стили ─────────────────────────────────────────────────────────────── var stStyles = cfg.modalStyles; var tk = { cBase: 'var(--color-base, #202122)', cSub: 'var(--color-subtle, #72777d)', cSubM: 'var(--color-subtle, #54595d)', cInv: 'var(--color-inverted-fixed, #fff)', cProg: 'var(--color-progressive, #3366cc)', cProgH: 'var(--color-progressive--hover, #2a4b8d)', cDang: 'var(--color-destructive, #d73333)', cDis: 'var(--color-disabled, var(--color-subtle, #72777d))', bgBase: 'var(--background-color-base, #fff)', bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)', bgN: 'var(--background-color-neutral, #eaecf0)', bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))', bgProg: 'var(--background-color-progressive, #3366cc)', bgProgH:'var(--background-color-progressive--hover, #2a4d8f)', bgSucc: 'var(--background-color-success, #14866d)', bgSuccH:'var(--background-color-success--hover, #0f6d57)', bSub: 'var(--border-color-subtle, #a2a9b1)', bSubS: 'var(--border-color-subtle, #ddd)', bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))', bProg: 'var(--border-color-progressive, #3366cc)', bProgH: 'var(--border-color-progressive--hover, #2a4d8f)', bSucc: 'var(--border-color-success, #14866d)', bSuccH: 'var(--border-color-success--hover, #0f6d57)' }; var sz = { taH: '180px', taMinH: '100px', taMinW: '180px', mobileBp: 720, modalRatio: 0.4, modalMinWide: 420, modalDefaultWide: 720, viewportGap: 24, touchDesktopGap: 120 }; var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;'; var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;'; var stCancel = neutralVis + btnBase; var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase; var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase; var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;'; var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;'; var stRow = 'display:flex;margin-bottom:6px;'; var inlineControlGap = 4; var squareControlSize = 32; var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px'; var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;'; var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;'; var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;'; var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;'; var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;'; var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;'; var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;'; var multiNominationGap = '6px'; var RESIZE_CLASS = 'rm-resizable'; // ═══════════════════════════════════════════════════════════════════════════ // РЕЕСТР ОПЕРАЦИЙ // Каждая запись описывает одну кнопку меню. Поля: // id — идентификатор (совпадает с item.id из loader) // handler — имя метода-обработчика в объекте handlers // handlerArg — аргумент, передаваемый в handler (опционально) // ═══════════════════════════════════════════════════════════════════════════ var OPERATIONS = [ // ── Статьи ────────────────────────────────────────────────────────── { id: 'fRm', label: 'КБУ', handler: 'showKbu', // Параметры номинации: заполняются при submit nomination: { pageTitle: function (pg) { return normTitle(pg); }, // шаблон встраивается в статью, номинационная страница отсутствует inArticle: true } }, { id: 'tRm', label: 'КУ', handler: 'showNomination', nomination: { comment: 'к удалению', template: 'к удалению', navTemplate: 'КУ', nomPage: function (date) { return 'Википедия:К удалению/' + date; }, supportsMulti: true, multiOpId: 'mRm', supportsTransfer: true, // шаблон встраивается в статью через <noinclude> inArticle: true, articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); } } }, { id: 'rnm', label: 'КПМ', handler: 'showNomination', nomination: { comment: 'к переименованию', template: 'к переименованию', navTemplate: 'КПМ', nomPage: function (date) { return 'Википедия:К переименованию/' + date; }, supportsMulti: true, multiOpId: 'mRnm', inArticle: true, articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'rename', firstId: 'rmRenameFirst', inputClass: 'rmRenameInput', firstPh: 'Новое название', addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант', containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' } } }, { id: 'imp', label: 'КУЛ', handler: 'showNomination', nomination: { comment: 'к срочному улучшению', template: 'к улучшению', navTemplate: 'КУЛ', nomPage: function (date) { return 'Википедия:К улучшению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); } } }, { id: 'merge', label: 'КОБ', handler: 'showNomination', nomination: { comment: 'к объединению с другой', template: 'к объединению', navTemplate: 'КОБ', nomPage: function (date) { return 'Википедия:К объединению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'merge', firstId: 'rmMergeFirst', inputClass: 'rmMergeInput', firstPh: 'Объединить с…', addBtnId: 'rmAddMerge', addBtnLabel: '+', containerId: 'rmMergeContainer', addPh: 'Дополнительная статья', maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.' } } }, { id: 'split', label: 'КРАЗД', handler: 'showNomination', nomination: { comment: 'к разделению', template: 'к разделению', navTemplate: 'КР', nomPage: function (date) { return 'Википедия:К разделению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'split', firstId: 'rmSplitFirst', inputClass: 'rmSplitInput', firstPh: 'Разделить на…', addBtnId: 'rmAddSplit', addBtnLabel: '+', containerId: 'rmSplitContainer', addPh: 'Дополнительная статья' } } }, { id: 'recov', label: 'ВУС', handler: 'showNomination', nomination: { comment: '', template: 'к восстановлению', navTemplate: 'ВУС', nomPage: function (date) { return 'Википедия:К восстановлению/' + date; }, inArticle: false // шаблон не ставится в (удалённую) статью } }, { id: 'close', label: 'Снятие', handler: 'showArticleClose' }, // ── Запросы ───────────────────────────────────────────────────────── { id: 'protect', label: 'Защита', handler: 'showReport', reportMode: 'protect' }, { id: 'request', label: 'Запрос', handler: 'showReport', reportMode: 'request' }, // ── Категории ──────────────────────────────────────────────────────── { id: 'cat-fRm', label: 'КБУ', handler: 'showKbu', forCategory: true }, { id: 'cat-discuss', label: 'Обсудить', handler: 'showCatNomination', catType: 'discuss' }, { id: 'cat-delete', label: 'Удалить', handler: 'showCatNomination', catType: 'deletion' }, { id: 'cat-rename', label: 'Переименовать', handler: 'showCatNomination', catType: 'rename' }, { id: 'cat-merge', label: 'Объединить', handler: 'showCatNomination', catType: 'merge' }, { id: 'cat-done', label: 'Снятие', handler: 'showCatClose' } ]; // Быстрый поиск по id var OPERATIONS_MAP = {}; OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; }); // ─── Тексты переноса (КБУ → КУ) ───────────────────────────────────────── var transferTexts = { kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' }, kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' }, both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' } }; // ═══════════════════════════════════════════════════════════════════════════ // УТИЛИТЫ // ═══════════════════════════════════════════════════════════════════════════ function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); } function escapeHtml(s) { return String(s || '') .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function getTalkPage(pageName) { var match = /([^:]*:)?(.*)/.exec(pageName); if (match[1]) { var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')]; if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2]; } return 'Обсуждение:' + pageName; } function normTitle(s) { return (s || '').replace(/_/g, ' '); } function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); } function getCategoryNamespaceLabel() { return mwCfg.wgFormattedNamespaces[14] || 'Категория'; } function normalizeCategoryPageName(value) { var title = normTitle(value).trim(); var nsMatch, nsKey, ns; if (!title) return ''; nsMatch = title.match(/^([^:]+):(.+)$/); if (nsMatch) { nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_'); ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey]; if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim(); return getCategoryNamespaceLabel() + ':' + title; } return getCategoryNamespaceLabel() + ':' + title; } function makeSummary(text) { return scriptLink + ': ' + text; } function appendNominationSignature(text) { var body = String(text || ''); return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~'; } function extractDisplayedText(s) { return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1'); } function collectInputValues(selector) { return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean); } function applyCoreConfigDefaults(config) { var defaults = { scriptLink: '[[Участник:Solidest/Remover|Remover]]', fastRemoveReasons: { general: [ ['уд-бессвязно', 'О1 Бессвязный текст'], ['уд-тест', 'О2 Тестовая страница'], ['уд-ванд', 'О3.1 Вандальная страница'], ['уд-мист', 'О3.2 Мистификация'], ['уд-повторно', 'О4 Уже удалялось'], ['уд-автор', 'О5 По просьбе автора'], ['уд-под', 'О6 Ненужная подстраница'], ['уд-переим', 'О7 Для переименования'], ['уд-дубль', 'О8 Дубликат'], ['уд-реклама', 'О9 Реклама или спам'], ['уд-нецелевая', 'О10 Нецелевая СО'], ['уд-копивио', 'О11 Нарушение АП'] ], articles: [ ['подст:ds', 'ds Отсроченное пусто или коротко', 'С'], ['уд-пусто', 'С1 Пусто или коротко'], ['уд-иностр', 'С2 Не на русском'], ['уд-ссылки', 'С3 Лишь ссылки'], ['уд-нз', 'С5 Явно незначимо'], ['уд-бям', 'С7 Создано нейросетью'] ], redirects: [ ['уд-в никуда', 'П1 Перенапр. в никуда'], ['уд-мпр', 'П2 Межпростр. перенапр.'], ['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'], ['уд-падеж', 'П4 Не именительный падеж'], ['уд-смысл', 'П5 Неверное перенапр.'], ['уд-перобс', 'П6 Перенапр. на СО'] ], files: [ ['db-duplicate', 'Ф1 Копия файла'], ['db-badimage', 'Ф2 Повреждённый файл'], ['подст:nld', 'Ф3 Нет данных о лицензии'], ['подст:nsd', 'Ф3 Нет данных о источнике'], ['подст:nad', 'Ф3 Нет данных о авторе'], ['подст:dd', 'Ф3 Сомнительные данные файла'], ['подст:npd', 'Ф3 Не подтверждена лицензия'], ['подст:ofud', 'Ф4 Неиспользуемый КДИ'], ['подст:dfud', 'Ф5 Нет КДИ'], ['db-badfairuse', 'Ф6 Неоправданное КДИ'], ['подст:rfu', 'Ф7 Заменяемый КДИ'], ['NCT', 'Ф8 Есть на Складе'], ['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ'] ], categories: [ ['уд-пусткат', 'К1.1 Пустая категория'], ['уд-служебная', 'К1.2 Разобранная служебная кат.'], ['уд-перекат', 'К2 Переименованная кат.'] ], users: [ ['уд-владелец', 'У1 По желанию владельца'], ['уд-анон', 'У2 Устаревшая СО анонима'], ['уд-несущ', 'У3 Несуществующий участник'], ['уд-нецелевое', 'У4 Нецелевое использ. ЛП'], ['уд-неактив', 'У5 Подстраница неактивного'] ], special: [ ['db', 'Особый случай'] ] }, fastRemoveCriteriaAnchors: { 'подст:ds': 'С1', deleteslow: 'С1', ds: 'С1' }, requiredParamTemplates: { 'уд-переим': 'страницу, которую нужно переименовать', 'уд-дубль': 'страницу-дубликат', 'уд-копивио': 'URL источника нарушения АП', 'db-duplicate': 'имя файла-оригинала', 'подст:rfu': 'имя заменяемого файла', 'NCT': 'имя файла на Викискладе', 'уд-перекат': 'новое название категории', 'db': '!причину удаления' }, categoryTemplates: { discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению', rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat', merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm', discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось' }, modalStyles: { border: '1px solid var(--border-color-progressive, #3366bb)', background: 'var(--background-color-base, #f8f9fa)', borderRadius: '6px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', headerColor: 'var(--color-progressive, #3366bb)' } }; config.scriptLink = config.scriptLink || defaults.scriptLink; config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {}); config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {}); config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {}); config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {}); config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {}); return config; } function clonePlainObject(obj) { return JSON.parse(JSON.stringify(obj || {})); } function normalizeMenuLabel(value) { return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); } function readSettingsOptionState(fallback) { var base = clonePlainObject(fallback || {}); var stored; var raw = null; if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base; stored = mw.user.options.get(settingsOptionName); if (typeof stored === 'string' && stored.trim()) { try { raw = JSON.parse(stored); } catch (e) { console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e); } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base; return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {}); } function normalizeQuickPhraseValue(value) { return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim(); } function normalizeQuickPhrasesList(list, fallback) { var source = Array.isArray(list) ? list : fallback; var normalized = []; (source || []).forEach(function (value) { var phrase = normalizeQuickPhraseValue(value); if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase); }); return normalized; } function collectSettingsMenuMeta() { var labels = []; var articleLabels = []; var categoryLabels = []; var idToLabel = {}; var labelByNorm = {}; var labelOrder = {}; function collect(items, targetLabels) { items.forEach(function (item) { var id; var label; var normLabel; if (!item || item.type === 'separator') return; id = String(item.id || '').trim(); label = String(item.label || '').trim(); normLabel = normalizeMenuLabel(label); if (id && label && !(id in idToLabel)) idToLabel[id] = label; if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label); if (normLabel && !(normLabel in labelByNorm)) { labelByNorm[normLabel] = label; labelOrder[label] = labels.length; labels.push(label); } }); } collect(cfg.articleMenuItems, articleLabels); collect(cfg.categoryMenuItems, categoryLabels); return { labels: labels, articleLabels: articleLabels, categoryLabels: categoryLabels, idToLabel: idToLabel, labelByNorm: labelByNorm, labelOrder: labelOrder }; } function buildSettingsMenuItemsHint() { var parts = []; if (settingsArticleItemLabels.length) { parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>'); } if (settingsCategoryItemLabels.length) { parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>'); } return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : ''; } function getDefaultSettings() { return { version: settingsVersion, excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []), notifyAuthor: !!cfg.defaultNotifyAuthor, subscribeTopic: !!cfg.defaultSubscribeTopic, menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover', disabledItems: [], quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []), showMenuIcons: !!cfg.showMenuIcons, signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : '' }; } function normalizeNumberList(list, fallback) { var source = Array.isArray(list) ? list : fallback; return source .map(function (value) { return parseInt(value, 10); }) .filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; }) .sort(function (a, b) { return a - b; }); } function normalizeDisabledItemValue(value) { var token = String(value || '').trim(); if (!token) return null; if (settingsItemLabelById[token]) return settingsItemLabelById[token]; return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null; } function compareSettingsMenuLabels(a, b) { var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER; var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER; if (ai !== bi) return ai - bi; return a.localeCompare(b, 'ru'); } function normalizeDisabledItemsList(list, fallback) { var source = Array.isArray(list) ? list : fallback; return source .map(normalizeDisabledItemValue) .filter(function (value, index, arr) { return value && arr.indexOf(value) === index; }) .sort(compareSettingsMenuLabels); } function normalizeMenuTitleSetting(value, fallback) { var menuTitle = String(value || '').trim(); if (!menuTitle) return fallback; if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS; if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE; if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS; return menuTitle; } function normalizeRemoverSettings(raw) { var defaults = clonePlainObject(settingsDefaults); var source = (raw && typeof raw === 'object') ? raw : {}; return { version: settingsVersion, excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []), notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor, subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic, menuTitle: normalizeMenuTitleSetting( (typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '', typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover' ), disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []), quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []), showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons, signatureSeparator: (typeof source.signatureSeparator === 'string') ? source.signatureSeparator.trim() : (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '') }; } function areRemoverSettingsEqual(a, b) { return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b)); } function updateStoredSettingsState(settings, skipUserOptionsSync) { var normalized = normalizeRemoverSettings(settings); state.settings = clonePlainObject(normalized); setAlert = normalized.notifyAuthor; setSubscribe = normalized.subscribeTopic; signatureSeparator = normalized.signatureSeparator; state.setAlert = setAlert; state.setSubscribe = setSubscribe; if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') { mw.user.options.set(settingsOptionName, JSON.stringify(normalized)); } return normalized; } function splitSettingsInput(value) { return String(value || '') .split(/[\s,;]+/) .map(function (part) { return part.trim(); }) .filter(Boolean); } function splitSettingsListInput(value) { return String(value || '') .split(/[\n,;]+/) .map(function (part) { return part.trim(); }) .filter(Boolean); } function parseNamespaceInput(value) { var tokens = splitSettingsInput(value); var invalid = []; var values = []; tokens.forEach(function (token) { var parsed = parseInt(token, 10); if (String(parsed) !== token) invalid.push(token); else if (values.indexOf(parsed) === -1) values.push(parsed); }); values.sort(function (a, b) { return a - b; }); return { values: values, invalid: invalid }; } function parseDisabledItemsInput(value) { var tokens = splitSettingsListInput(value); var invalid = []; var values = []; tokens.forEach(function (token) { var normalized = normalizeDisabledItemValue(token); if (!normalized) invalid.push(token); else if (values.indexOf(normalized) === -1) values.push(normalized); }); values.sort(compareSettingsMenuLabels); return { values: values, invalid: invalid }; } function formatItemsWithAnd(items) { var list = (items || []).filter(Boolean); if (!list.length) return ''; if (list.length === 1) return list[0]; return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1]; } function formatPagesWithAnd(names, prefix) { var p = prefix || ':'; var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; }); return formatItemsWithAnd(links); } function asNonEmptyArray(value) { return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean); } function getCategoryNominationMeta(type) { return CATEGORY_NOMINATION_META[type] || CATEGORY_NOMINATION_META.discuss; } function buildRenameTemplateParam(targetNames) { var list = asNonEmptyArray(targetNames); if (!list.length) return ''; return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : ''); } function collectRenameTargetsFromTemplateParams(params) { return (params || []).map(function (value) { return String(value || '').trim(); }).filter(Boolean); } function formatRenameItemLabel(pageName, targetName) { var targets = asNonEmptyArray(targetName); return '[[:' + pageName + ']]' + (targets.length ? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ') : ''); } function buildRenameItemLabelFormatter(targetsByPage) { var targets = targetsByPage || {}; return function (pageName) { return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || ''); }; } function formatRenameItemsWithAnd(pages, targetsByPage) { var formatItem = buildRenameItemLabelFormatter(targetsByPage); var links = (pages || []).map(function (pageName) { return formatItem(pageName); }); return formatItemsWithAnd(links); } function normalizeCategoryTargetName(value) { return normTitle(stripCatPrefix(value)).trim(); } function normalizeCategoryTargetPageName(value) { var title = normalizeCategoryTargetName(value); return title ? normalizeCategoryPageName(title) : ''; } function buildMultiRenameTargetMap(pairs, key) { var map = {}; (pairs || []).forEach(function (pair) { var value; if (!pair || !pair.pageName) return; value = pair[key || 'targetName']; map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || ''); }); return map; } function getMultiRenameTarget(job, pageName, key) { var map = job && job[key || 'multiRenameTargets']; return map ? (map[normTitle(pageName)] || '') : ''; } function collectMultiRenamePairs(options) { var opts = options || {}; var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle; var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle; var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget; var pairs = []; $('.rmMultiPageBlock').each(function () { var $block = $(this); var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim(); var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) { return { targetName: normalizeTarget(targetRaw), templateTargetName: normalizeTemplateTarget(targetRaw) }; }).filter(function (item) { return item.targetName || item.templateTargetName; }); var pageName = normalizePage(pageRaw); var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean); var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean); if (!pageName && !targetNames.length && !templateTargetNames.length) return; pairs.push({ pageName: pageName, targetName: targetNames[0] || '', templateTargetName: templateTargetNames[0] || '', targetNames: targetNames, templateTargetNames: templateTargetNames }); }); return pairs; } function validateMultiRenamePairs(pairs, pageLabel, targetLabel) { var seen = {}; var pages = pairs || []; var pageWord = pageLabel || 'страницу'; var targetWord = targetLabel || 'новое название'; if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; } for (var i = 0; i < pages.length; i++) { var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName); var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName); if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; } if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; } if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; } if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; } seen[normTitle(pages[i].pageName)] = true; } return true; } function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) { return $.extend({}, extraOptions || {}, { formatItemLabel: buildRenameItemLabelFormatter(targetsByPage) }); } function formatCatLink(name) { return '[[:Категория:' + name + ']]'; } function formatMergeStatus(status) { return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status; } function applyGeneratedText($el, generated) { var cur = $el.val(); var prev = $el.data('rmGenerated') || ''; if (!prev || cur.indexOf(prev) === 0) { $el.val(generated + cur.slice(prev.length)); } else { $el.val(generated + (cur ? '\n' + cur : '')); } $el.data('rmGenerated', generated); } function getCurrentQuickPhrases() { return normalizeQuickPhrasesList( state.settings && state.settings.quickPhrases, settingsDefaults.quickPhrases || [] ); } function insertTextIntoTextarea($el, text) { var el = $el && $el[0]; var value; var start; var end; var updatedValue; var caretPos; if (!el) return; text = String(text || ''); if (!text) return; value = $el.val() || ''; start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length; end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start; updatedValue = value.slice(0, start) + text + value.slice(end); caretPos = start + text.length; $el.val(updatedValue).trigger('input').trigger('change').focus(); if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos); } function buildQuickPhrasesPanelHtml(textareaId) { var phrases = getCurrentQuickPhrases(); if (!phrases.length) return ''; return joinHtml([ '<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">', phrases.map(function (phrase) { return joinHtml([ '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId, '" data-rm-phrase="', escapeHtml(phrase), '">', escapeHtml(phrase), '</button>' ]); }).join(''), '</div>' ]); } function getMultiNominationCommentText(commentsByPage, pageTitle) { var key = normTitle(pageTitle); if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return ''; return normalizeQuickPhraseValue(commentsByPage[key]); } function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) { var list = Array.isArray(pages) ? pages.filter(Boolean) : []; return list.length > 0 && list.every(function (pageName) { return !!getMultiNominationCommentText(commentsByPage, pageName); }); } function hasMultiNominationText(pages, bodyText, commentsByPage) { return !!normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage); } function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) { if (hasMultiNominationText(pages, bodyText, commentsByPage)) return true; alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.'); return false; } function buildMultiNominationText(pages, bodyText, commentsByPage, options) { var opts = options || {}; var list = Array.isArray(pages) ? pages.filter(Boolean) : []; var body = normalizeQuickPhraseValue(bodyText); var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage); var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3); var headingMarks = new Array(headingLevel + 1).join('='); var formatItemLabel = typeof opts.formatItemLabel === 'function' ? opts.formatItemLabel : function (pageName) { return '[[:' + pageName + ']]'; }; var pageSections = list.map(function (pageName, index) { var comment = getMultiNominationCommentText(commentsByPage, pageName); var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n'; return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : ''); }).join(''); var commonSectionText = body ? appendNominationSignature(body) : (hasAllPageComments ? '' : appendNominationSignature('')); return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : ''); } function buildMultiNominationListText(pages, bodyText, commentsByPage, options) { var opts = options || {}; var list = Array.isArray(pages) ? pages.filter(Boolean) : []; var body = normalizeQuickPhraseValue(bodyText); var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage); var formatItemLabel = typeof opts.formatItemLabel === 'function' ? opts.formatItemLabel : function (pageName) { return '[[:' + pageName + ']]'; }; var pageLines = list.map(function (pageName) { var comment = getMultiNominationCommentText(commentsByPage, pageName); return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : ''); }).join('\n'); var commonText = body ? appendNominationSignature(body) : (hasAllPageComments ? '' : appendNominationSignature('')); return pageLines + (pageLines && commonText ? '\n' : '') + commonText; } function collectMultiNominationComments(normalizePageName) { var comments = {}; var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle; $('.rmMultiPageBlock').each(function () { var $block = $(this); var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim()); var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val()); if (!pageName) return; comments[pageName] = comment; }); return comments; } function getNominationPublishText(job) { if (job && job.isMulti) return String(job.msg || ''); return appendNominationSignature(job && job.msg); } function getNominationConflictRule(job) { if (!job || job.mode !== 'nominate') return null; if (job.opId === 'tRm' || job.opId === 'mRm') { return { id: 'ku', label: 'КУ', namePattern: '(?:к\\s*удалению|ку)', detect: function (articleText) { var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i); if (!match) return null; var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim()); return { label: 'КУ', templateName: templateName || 'КУ', templateDisplay: '{{' + (templateName || 'КУ') + '}}' }; } }; } if (job.opId === 'rnm' || job.opId === 'mRnm') { return { id: 'kpm', label: 'КПМ', namePattern: '(?:к\\s*переименованию|кпм|rename)', detect: function (articleText) { var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i); if (!match) return null; var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim()); return { label: 'КПМ', templateName: templateName || 'КПМ', templateDisplay: '{{' + (templateName || 'КПМ') + '}}' }; } }; } return null; } function detectNominationConflict(articleText, job) { var rule = getNominationConflictRule(job); if (!rule || typeof rule.detect !== 'function') return null; return rule.detect(articleText); } function getConflictDecisionForPage(job, pageName) { var decisions = job && job.conflictDecisions; var key = normTitle(pageName); return decisions && decisions[key] ? decisions[key] : null; } function getCategoryMergeRe() { return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i'); } function eachSequential(targets, iteratee, done) { var i = 0; (function next(err) { if (err || i >= targets.length) { done(err || null); return; } iteratee(targets[i++], next); }(null)); } function normalizeSectionForLink(sectionTitle) { return (sectionTitle || '').trim() .replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) { var v = (label || target || '').trim(); return v.charAt(0) === ':' ? v.slice(1) : v; }) .replace(/''+/g, '').replace(/\s+/g, ' ').trim(); } function getViewportWidth() { return Math.floor(Math.max( (document.documentElement && document.documentElement.clientWidth) || 0, (typeof window.innerWidth === 'number' && window.innerWidth) || 0, $(window).width() || 0 )); } function getVisualViewportWidth() { var widths = []; if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width); if (window.screen && window.screen.width > 0) widths.push(window.screen.width); return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth(); } function isTouchModalDevice() { return !!( (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0) ); } function getModalLayout() { var minWidth = parseInt(sz.taMinW, 10) || 180; var layoutWidth = getViewportWidth(); var visualWidth = getVisualViewportWidth(); var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth)); var isMobile = layoutWidth <= sz.mobileBp; var isTouchDesktop = !isMobile && isTouchModalDevice() && visualWidth > 0 && visualWidth <= sz.mobileBp && layoutWidth >= visualWidth + sz.touchDesktopGap; var useFullWidth = isMobile || isTouchDesktop; var maxOuterWidth; var defaultOuterWidth; var desktopWidth; if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap); else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32); else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth))); desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio)); if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth; else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide; else defaultOuterWidth = desktopWidth; return { minWidth: minWidth, isMobile: isMobile, isTouchDesktop: isTouchDesktop, useFullWidth: useFullWidth, shouldCenter: useFullWidth || mwCfg.skin === 'minerva', maxOuterWidth: maxOuterWidth, defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth) }; } function getDefaultResizableWidth(frameWidth) { var layout = getModalLayout(); return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0)); } function getBoxFrameWidth($el) { function px(prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width'); } // ═══════════════════════════════════════════════════════════════════════════ // API // ═══════════════════════════════════════════════════════════════════════════ function getApiUrl() { return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php'; } function getCsrfTokenValue() { return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function') ? mw.user.tokens.get('csrfToken') : null; } function storeCsrfToken(token) { if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return; mw.user.tokens.set({ csrfToken: token }); } function isValidCsrfToken(token) { return typeof token === 'string' && !!token && token !== '+\\'; } function fetchCsrfToken(forceRefresh, callback) { var cachedToken = getCsrfTokenValue(); if (!forceRefresh && isValidCsrfToken(cachedToken)) { callback(cachedToken); return; } $.ajax({ url: getApiUrl(), method: 'GET', dataType: 'json', data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' } }) .done(function (data) { var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken; if (isValidCsrfToken(token)) { storeCsrfToken(token); callback(token); return; } callback(null); }) .fail(function () { callback(null); }); } function apiReq(params, mode, callback) { var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options'; function sendRequest(retryWithFreshToken) { var reqParams = $.extend({}, params, { format: 'json', action: mode }); if (!isWrite) { $.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' }) .done(function (data) { if (callback) callback(data); }) .fail(function (jqXHR, status) { console.error('Ошибка API: ' + status); if (callback) callback({ error: { code: 'network', info: status } }); }); return; } fetchCsrfToken(!!retryWithFreshToken, function (token) { if (!isValidCsrfToken(token)) { if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } }); return; } reqParams.token = token; $.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' }) .done(function (data) { var err = data && data.error; var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || ''))); if (isBadToken && !retryWithFreshToken) { sendRequest(true); return; } if (callback) callback(data); }) .fail(function (jqXHR, status) { console.error('Ошибка API: ' + status); if (callback) callback({ error: { code: 'network', info: status } }); }); }); } sendRequest(false); } function saveSettingsToServer(settings, callback) { var normalized = normalizeRemoverSettings(settings); if (!mwCfg.wgUserName) { callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' }); return; } apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) { if (resp && resp.options === 'success') { callback(null, updateStoredSettingsState(normalized)); return; } callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' }); }); } function resetSettingsOnServer(callback) { if (!mwCfg.wgUserName) { callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' }); return; } apiReq({ change: settingsOptionName }, 'options', function (resp) { if (resp && resp.options === 'success') { callback(null, updateStoredSettingsState(settingsDefaults, true)); return; } callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' }); }); } function getFirstQueryPage(data) { var pages = data && data.query && data.query.pages; if (!pages) return null; return pages[Object.keys(pages)[0]] || null; } function extractRevisionContent(rev) { if (!rev) return null; if (typeof rev['*'] === 'string') return rev['*']; if (rev.slots && rev.slots.main) { if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*']; if (typeof rev.slots.main.content === 'string') return rev.slots.main.content; } return null; } function makeReadError(apiError, fallbackCode, fallbackInfo) { var err = apiError || {}; return { code: err.code || fallbackCode || 'read_failed', info: err.info || fallbackInfo || 'Не удалось получить содержимое.' }; } function getTextWithTimestamp(pageName, callback) { apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) { var content; var page; if (data && data.error) { callback(null, null, data.error); return; } if (!data || !data.query || !data.query.pages) { callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' }); return; } page = getFirstQueryPage(data); if (!page) { callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' }); return; } if (page.invalid !== undefined) { callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' }); return; } if (page.missing !== undefined) { callback(null, null, null); return; } if (!page.revisions || !page.revisions.length) { callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' }); return; } content = extractRevisionContent(page.revisions[0]); if (content === null) { callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' }); return; } callback(content, page.revisions[0].timestamp || null, null); }); } function getText(pageName, callback) { getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); }); } function editPageContent(pageTitle, options, buildFn, callback) { var opts = options || {}; var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1); (function attempt(retry) { getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».')); return; } if (sourceText === null) { callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' }); return; } var done = (function () { var called = false; return function (result) { if (called) return; called = true; if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; } if (result.skip) { callback(null, result.meta || null); return; } if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' }; if (opts.watchlist) ep.watchlist = opts.watchlist; if (opts.assertuser) ep.assertuser = opts.assertuser; if (opts.createonly) ep.createonly = opts.createonly; if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { var err = resp && resp.error ? resp.error : null; if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; } callback(err, result.meta || null); }); }; }()); var maybe = buildFn(sourceText, done); if (maybe !== undefined) done(maybe); }); }(0)); } // ═══════════════════════════════════════════════════════════════════════════ // ШАБЛОНЫ: удаление и вставка // ═══════════════════════════════════════════════════════════════════════════ function findBalancedTemplateEnd(text, start) { var depth = 0; var i = start; var len = text.length; while (i < len - 1) { if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; continue; } if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; if (depth === 0) return i; continue; } i++; } return -1; } function splitTemplateTopLevelParts(innerText) { var parts = []; var start = 0; var templateDepth = 0; var linkDepth = 0; var i = 0; var text = String(innerText || ''); while (i < text.length) { if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { templateDepth++; i += 2; continue; } if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) { templateDepth--; i += 2; continue; } if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') { linkDepth++; i += 2; continue; } if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) { linkDepth--; i += 2; continue; } if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) { parts.push(text.slice(start, i)); start = i + 1; } i++; } parts.push(text.slice(start)); return parts; } function getTemplateMatchAt(text, start, nameRe) { var end = findBalancedTemplateEnd(text, start); var parts; var rawName; var name; if (end < 0) return null; parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2)); rawName = String(parts.shift() || '').trim(); name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); if (!nameRe.test(name)) return null; return { start: start, end: end, text: text.slice(start, end), name: rawName, params: parts.map(function (part) { return part.trim(); }) }; } function findTemplateByPattern(text, namePattern) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var i = 0; var end; var match; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) return match; end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } return null; } function hasTemplateWithDateByPattern(text, namePattern, dateIso) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var targetDate = convertToStandardDate(dateIso); var i = 0; var end; var match; var templateDate; if (!targetDate) return false; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) { templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, '')); if (templateDate === targetDate) return true; i = match.end; continue; } end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } return false; } function getTemplateRemovalRange(text, match) { var start = match.start; var end = match.end; var before = text.slice(0, start).match(/<noinclude>\s*$/i); var after; if (before) { after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i); if (after) { return { start: before.index, end: end + after[0].length }; } } after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/); if (after) end += after[0].length; return { start: start, end: end }; } function stripTemplatesByPattern(text, namePattern) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var ranges = []; var out = []; var pos = 0; var i = 0; var end; var match; var range; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) { range = getTemplateRemovalRange(source, match); ranges.push(range); i = Math.max(match.end, range.end); continue; } end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } if (!ranges.length) return { text: source, removed: false }; ranges.forEach(function (r) { if (r.start < pos) r.start = pos; out.push(source.slice(pos, r.start)); pos = r.end; }); out.push(source.slice(pos)); return { text: out.join(''), removed: true }; } function removeTemplatesByAliases(text, aliases) { var seen = {}, patterns = []; aliases.forEach(function (alias) { var tpl = alias.replace(RE_TEMPLATE_NS, '').trim(); var key = tpl.toLowerCase(); if (!tpl || seen[key]) return; seen[key] = true; patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+')); }); if (!patterns.length) return { text: text, removed: false }; return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')'); } function removeTransferTemplatesLocal(articleText, transferMode) { var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false }; if (transferMode === 'none') return result; if (transferMode === 'kbu' || transferMode === 'both') { var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')'); result.text = kbu.text; result.removedKbu = kbu.removed; } if (transferMode === 'kul' || transferMode === 'both') { var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR); result.text = kul.text; result.removedKul = kul.removed; } var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR); result.text = hangon.text; result.removedHangon = hangon.removed; return result; } function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) { var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu; var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul; var needHangon = transferMode !== 'none' && !localResult.removedHangon; if (!needKbu && !needKul && !needHangon) { callback(localResult); return; } var titleMap = {}; if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; }); if (needKul) titleMap['Шаблон:К улучшению'] = true; if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; } apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) { var page = getFirstQueryPage(data); if (page && page.templates) { page.templates.forEach(function (tpl) { var norm = normalizeTemplateName(tpl.title); if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) || (needKul && RE_KUL_PATTERN.test(norm)) || (needHangon && RE_HANGON.test(norm))) { titleMap[tpl.title] = true; } }); } var titles = Object.keys(titleMap); if (!titles.length) { callback(localResult); return; } function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); } function collectAndApplyAliases() { var allAliases = []; titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); }); var dedup = {}, aliases = []; allAliases.forEach(function (alias) { var key = normalizeAliasKey(alias); if (!key || dedup[key]) return; dedup[key] = true; aliases.push(alias); }); var updated = $.extend({}, localResult); var r = removeTemplatesByAliases(updated.text, aliases); updated.text = r.text; if (r.removed) { if (needKbu) updated.removedKbu = true; if (needKul) updated.removedKul = true; if (needHangon) updated.removedHangon = true; } callback(updated); } var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; }); if (!missingTitles.length) { collectAndApplyAliases(); return; } (function resolveChunk(offset) { if (offset >= missingTitles.length) { collectAndApplyAliases(); return; } var chunk = missingTitles.slice(offset, offset + 20); var chunkByKey = {}; chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; }); apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) { var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {}; Object.keys(pages).forEach(function (pid) { var p = pages[pid]; if (!p || !p.title) return; var sourceTitle = chunkByKey[normalizeAliasKey(p.title)]; if (!sourceTitle) return; var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; })); var seen = {}, unique = []; found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); }); tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle]; }); chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; }); resolveChunk(offset + 20); }); }(0)); }); } // ─── Вставка шаблонов ──────────────────────────────────────────────────── function findInsertPositionAfterProjectTemplates(text) { var pos = 0, len = text.length; while (pos < len) { var wsMatch = text.slice(pos).match(/^[\t ]*\n/); if (wsMatch) { pos += wsMatch[0].length; continue; } if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break; var afterOpen = text.slice(pos + 2); var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/); var templateEnd; if (!nameMatch) break; var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim(); if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break; templateEnd = findBalancedTemplateEnd(text, pos); if (templateEnd < 0) break; pos = templateEnd; if (pos < len && text.charAt(pos) === '\n') pos++; } return pos; } function insertTplOnTalkPage(text, tplText, sep) { var s = (sep === undefined) ? '\n' : sep; var insertPos = findInsertPositionAfterProjectTemplates(text); if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : ''); var before = text.slice(0, insertPos).replace(/\n+$/, ''); var after = text.slice(insertPos).replace(/^\n+/, ''); return before + '\n' + tplText + (after.length ? s + after : ''); } function wrapInNoinclude(text, templateText) { var match = text.match(RE_NOINCLUDE); if (match) { // Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху var before = text.slice(0, text.indexOf(match[0])); if (/\S/.test(before)) { return '<noinclude>' + templateText + '</noinclude>\n' + text; } var content = match[2]; if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n'; return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>'); } return '<noinclude>' + templateText + '</noinclude>\n' + text; } function buildDateSectionTalkTemplateText(templateName, dateIso, sectionTitle, sectionIndex) { var normalizedSection = String(sectionTitle || '').trim(); var index = parseInt(sectionIndex, 10); var tpl = templateName + '|' + dateIso; if (isNaN(index) || index < 1) index = 1; if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection; return T_OPEN + tpl + T_CLOSE; } function upsertDateSectionTemplateOnTalkPage(text, templateName, aliases, dateIso, sectionTitle) { var source = text || ''; var normalizedSection = String(sectionTitle || '').trim(); var names = []; asNonEmptyArray(aliases).concat(templateName).forEach(function (name) { var normalized = String(name || '').trim(); if (normalized && names.indexOf(normalized) === -1) names.push(normalized); }); var namePattern = names.map(function (name) { return escapeRegExp(String(name).trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')\\s*([^}]*)\\}\\}', 'i'); var tplMatch = source.match(tplRe); if (!tplMatch) { return { text: insertTplOnTalkPage(source, buildDateSectionTalkTemplateText(templateName, dateIso, normalizedSection, 1), '\n'), status: 'created' }; } var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean); var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); }); var stdDate = convertToStandardDate(dateIso); if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' }; var nextIdx = existingDates.length + 1; var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : ''); return { text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }), status: 'updated' }; } function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) { return upsertDateSectionTemplateOnTalkPage(text, 'оставлено', ['оставлено'], dateIso, sectionTitle); } function upsertRemovedFromDeletionTemplateOnTalkPage(text, dateIso, sectionTitle) { return upsertDateSectionTemplateOnTalkPage(text, 'Снято с удаления', ['Снято с удаления'], dateIso, sectionTitle); } function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) { var normalizedSection = String(sectionTitle || '').trim(); var normalizedReason = normalizeQuickPhraseValue(reasonText); var normalizedDeadline = String(deadlineText || '').trim(); var index = parseInt(sectionIndex, 10); var tpl = 'условно оставлено|' + dateIso; if (isNaN(index) || index < 1) index = 1; if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection; if (normalizedReason) tpl += '|пояснение=' + normalizedReason; if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline; return T_OPEN + tpl + T_CLOSE; } function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) { var source = text || ''; var normalizedSection = String(sectionTitle || '').trim(); var normalizedReason = normalizeQuickPhraseValue(reasonText); var normalizedDeadline = String(deadlineText || '').trim(); var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i; var tplMatch = source.match(tplRe); if (!tplMatch) { return { text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'), status: 'created' }; } var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean); var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); }); var stdDate = convertToStandardDate(dateIso); if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' }; var nextIdx = existingDates.length + 1; var suffix = '|' + dateIso; if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection; if (normalizedReason) suffix += '|пояснение=' + normalizedReason; if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline; return { text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }), status: 'updated' }; } // ═══════════════════════════════════════════════════════════════════════════ // ПАЙПЛАЙН НОМИНАЦИИ // ═══════════════════════════════════════════════════════════════════════════ function runNominationPipeline(steps) { var s = steps; var ctx = { templateMeta: null, nominationInfo: null }; var stages = [ { name: 'шаблон', fn: function (next) { s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); }); } }, { name: 'номинация', pendingText: 'Публикуется номинация...', successText: 'Номинация опубликована.', errorText: 'Публикация номинации.', fn: function (next) { s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); }); } }, { name: 'подписка', shouldRun: function () { var info = ctx.nominationInfo; return !!(setSubscribe && info && info.pageTitle && info.sectionTitle); }, fn: function (next) { subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); }); } }, { name: 'оповещение', shouldRun: function () { return !!(setAlert && !s.skipNotify); }, fn: function (next) { s.notifyStep(ctx.nominationInfo, next); } } ]; (function run(i) { if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; } var stage = stages[i]; if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; } var statusId = stage.pendingText ? logStatus(stage.pendingText, null, { pending: true, trackError: false }) : null; try { stage.fn(function (err) { if (err) { if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId }); if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx); else markSubmitError(); return; } if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false }); run(i + 1); }); } catch (ex) { var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) }; if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId }); if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx); else markSubmitError(); } }(0)); } // ─── Публикация номинации ──────────────────────────────────────────────── function publishNomination(opts, callback) { var cb = callback || function () {}; var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1); function doPublish() { apiReq({ title: opts.pageTitle, section: 'new', sectiontitle: opts.sectionTitle, summary: opts.summary, text: opts.text, assertuser: mwCfg.wgUserName }, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null); }); } if (opts.sectionTitle) { if (!opts.navTemplate) { doPublish(); return; } apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName }, 'edit', function (resp) { if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; } doPublish(); }); return; } // Вставка в существующую страницу if (opts.createText !== undefined) { (function attempt(retry) { getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) { var result; var ep; if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; } result = pageText === null ? (typeof opts.createText === 'function' ? opts.createText() : opts.createText) : (opts.buildText ? opts.buildText(pageText) : null); if (typeof result === 'string') result = { text: result }; if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } if (result.skip) { cb(null); return; } if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } ep = { title: opts.pageTitle, text: result.text, summary: result.summary || opts.summary, assertuser: mwCfg.wgUserName }; if (pageText === null) ep.createonly = true; else if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { var err = resp && resp.error ? resp.error : null; if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) { attempt(retry + 1); return; } cb(err); }); }); }(0)); return; } editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' }, function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; }, function (err) { cb(err || null); } ); } // ─── Оповещение авторов ────────────────────────────────────────────────── function notifyAuthor(pg, options, callback) { var opts = options || {}; var cb = callback || function () {}; var actionText = (typeof opts.actionText === 'string') ? opts.actionText : ''; var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : ''; var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : ''); var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true; var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена'; var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : ''; apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) { var page = getFirstQueryPage(queryResp); if (!page) { cb({ code: 'network', info: 'Network error' }); return; } if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; } if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; } var rv = page.revisions[0]; if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; } apiReq({ title: 'User talk:' + rv.user, section: 'new', sectiontitle: 'Remover: [[:' + pg + ']]', summary: opts.summary || makeSummary('уведомление автора'), text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' + discussionText + '~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>', assertuser: mwCfg.wgUserName }, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); }); }); } function notifyAuthorsForPages(pages, notifyOptions, callback) { var cb = callback || function () {}; var opts = notifyOptions || {}; var list = []; (pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); }); if (!list.length) { cb(); return; } eachSequential(list, function (pg, next) { var pageLink = buildQuotedStatusPageLink(pg); var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false }); notifyAuthor(pg, opts, function (err) { logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err, { statusId: statusId, trackError: opts.trackError !== false }); next(); }); }, cb); } // ─── Подписка на раздел ────────────────────────────────────────────────── function subscribeToTopic(pageTitle, sectionTitle, callback) { var cb = callback || function () {}; if (!setSubscribe || !sectionTitle) { cb(); return; } var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false }); var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase(); function finish(err, st) { if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; } logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false }); cb(); } function trySubscribe(attemptsLeft) { apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) { var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null; if (!items || !items.length) { if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; } finish(null, 'not_found'); return; } var commentname = null; for (var ti = items.length - 1; ti >= 0; ti--) { var t = items[ti]; if (t.type === 'heading') { var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim(); if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) { commentname = t.name; break; } } } if (!commentname) { if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); else finish(null, 'not_found'); return; } apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) { finish(res && res.error ? res.error : null, 'subscribed'); }); }); } setTimeout(function () { trySubscribe(2); }, 1500); } // ═══════════════════════════════════════════════════════════════════════════ // UI: модальные окна // ═══════════════════════════════════════════════════════════════════════════ function syncModalLayout() { var syncFn = $('#removerModal').data('rmSyncLayout'); if (typeof syncFn === 'function') syncFn(); } function clearModalLayoutSyncHandlers() { modalLayoutSyncHandlers = []; $('#removerModal').removeData('rmSyncLayout'); } function registerModalLayoutSync(handler) { if (typeof handler !== 'function') return; if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler); $('#removerModal').data('rmSyncLayout', function () { modalLayoutSyncHandlers.slice().forEach(function (fn) { if (typeof fn === 'function') fn(); }); }); } function registerResizeObserver(observer) { if (observer) resizeObservers.push(observer); } function resetModalObservers() { resizeObservers.forEach(function (observer) { if (observer && typeof observer.disconnect === 'function') observer.disconnect(); }); resizeObservers = []; clearModalLayoutSyncHandlers(); $(window).off('resize.removerModal'); $(window).off('.rmTaResize'); } function closeModal() { resetModalObservers(); $(window).off('keydown.remover'); if (isVector22) $('#content').css('min-width', ''); $('#removerModal').remove(); } function ensureModalStyles() { if (document.getElementById('removerModalDynamicStyles')) return; var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;'; var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;'; var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;'; var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)'; var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)'; var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)'; var css = [ '#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}', '#removerModal{color:inherit}', '#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}', '#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}', '#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}', '#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}', '#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}', '#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}', '#removerModal button:not(:disabled):hover{filter:brightness(.97)}', '#removerModal button:not(:disabled):active{transform:translateY(1px)}', '#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}', '#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}', '#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}', '#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}', '#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}', '#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}', '#removerModal .rmRenameVariantAddBtn{font-size:15px!important}', '#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}', '#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}', '#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}', '#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}', '#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}', '#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}', '#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}', '#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}', '#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}', '#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}', '#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}', '#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}', '#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}', '#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}', '#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}', '#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}', '#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}', '#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}', '#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}', '#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}', '#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}', '#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}', '#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}', '#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}', '#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}', '#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}', '#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}', '#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}', '#removerModal .rmActionItem:active{transform:translateY(1px)}', '#removerModal .rmActionMain{display:flex;align-items:center}', '#removerModal .rmActionMain input[type="radio"]{margin-right:8px}', '#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}', '#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}', '#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}', '#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}', '#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}', '#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}', '#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmConflictGroup{margin-top:10px}', '#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}', '#removerModal .rmConflictChoice{padding:5px 10px}', '#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}', '#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}', '#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}', '#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}', '#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}', '#removerModal .rmSettingsSectionHeader{margin:0 0 12px}', '#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}', '#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}', '#removerModal .rmSettingsField:last-child{margin-bottom:0}', '#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}', '#removerModal .rmSettingsFieldControl{display:block;min-width:0}', '#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}', '#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}', '#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}', '#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}', '#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}', '#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}', '#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}', '#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}', '#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}', '#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}', '#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}', '#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}', '#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}', '#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}', '#removerModal.rmModalSettings #rmSettingsForm{gap:18px}', '#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}', '#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}', '#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}', '#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}', '#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}', '#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}', '#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}', '#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}', '#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}', '#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}', '#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}', '#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}', '#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}', '#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}', '#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}', '#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}', '#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}', '#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}', '#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}', '#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}', '#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}', '#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}', '#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}', '#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}', '#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}', '#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}', '#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}', '#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}', '#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}', '#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}', '#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}', '#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}', '#removerModal #rmTransferModeGroup{gap:0}', '#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}', '#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}', '#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}', '#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}', '#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}', '#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}', '#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}', '#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}', '#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}', '#removerModal #rmProtectTextBlock{margin-top:14px}', '#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}', '#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}', '#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}', '#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}', '#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}', '#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}', '#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}', '#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}', '#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}', '#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}', '#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}', '#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}', '#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}', '#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}', '#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}', '#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}', '#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmQuickPhrasesPanel{margin-top:8px}', '#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}', '#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}', '@media (max-width:' + sz.mobileBp + 'px){', '#removerModal button{white-space:normal!important}', '#removerModal #rmFooterButtons{align-items:flex-start!important}', '#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}', '#removerModal .rmSettingsSection{padding:12px 13px}', '#removerModal .rmSettingsField{padding:10px}', '#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}', '#removerModal .rmTransferPanel{padding:0}', '#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}', '#removerModal .rmTransferHintRow{grid-column:auto}', '#removerModal .rmQuickPhraseChip{max-width:100%}', '}' ].join(''); var style = document.createElement('style'); style.id = 'removerModalDynamicStyles'; style.textContent = css; document.head.appendChild(style); } function applyV2022Layout($modal, explicitWidth) { if (!isVector22) return; var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' }; if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px'; $modal.css(css); } function getPageUrl(pageTitle) { return (mw.util && typeof mw.util.getUrl === 'function') ? mw.util.getUrl(pageTitle) : '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_')); } function getPageUrlWithFragment(pageTitle, fragment) { var url = getPageUrl(pageTitle); var frag = normalizeSectionForLink(fragment); if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_')); return url; } function buildStatusPageLink(pageName) { var title = normTitle(pageName); return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(title) + '</a>'; } function buildQuotedStatusPageLink(pageName) { return '«' + buildStatusPageLink(pageName) + '»'; } function buildHeaderIconButtonHtml(id, title, label, text) { return joinHtml([ '<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ', 'style="', stHeaderIconBtn, '">', text || '', '</button>' ]); } function createModal(opts) { if (typeof opts === 'string') opts = { title: opts }; var layout = getModalLayout(); resetModalObservers(); ensureModalStyles(); if (isVector22) $('#content').css('min-width', ''); $('#removerModal').remove(); var subtitleHtml = ''; var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;'; var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;'; if (opts.subtitleHtml) { subtitleHtml = joinHtml([ '<div id="removerModalSubtitle" style="', subtitleStyle, '">', opts.subtitleHtml, '</div>' ]); } else if (opts.subtitlePage) { subtitleHtml = joinHtml([ '<div id="removerModalSubtitle" style="', subtitleStyle, '">', opts.subtitleLabel || 'Текущий день', ': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">', normTitle(opts.subtitlePage), '</a></div>' ]); } var settingsButtonHtml = opts.showSettingsButton === false ? '' : buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙'); var display = opts.inline ? 'inline-block' : 'block'; var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0'); var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : ''; var modalStyle = joinHtml([ 'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';', 'border:', stStyles.border, ';background:', stStyles.background, ';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow, ';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle ]); var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';'; var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;'; $('#content').prepend(joinHtml([ '<div id="removerModal" style="', modalStyle, '">', '<div id="removerModalHeaderBar" style="', headerStyle, '">', '<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>', settingsButtonHtml, '</div>', subtitleHtml, '<div id="removerModalContent"></div>', '<div id="removerModalFooter" style="margin-top:15px;"></div>', '</div>' ])); var $modal = $('#removerModal'); if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); else applyV2022Layout($modal); $('#removerSettingsTrigger').off('click').on('click', function () { openSettings(); }); } function buildFooterCheckboxHtml(name, checked, label) { return joinHtml([ '<label style="', stFooterCheckLabel, '">', '<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ', checked ? 'checked' : '', '>', label, '</label>' ]); } function buildFooterActionsHtml(buttonsHtml) { return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>'; } function renderModalFooter(mode, options) { var opts = options || {}; $('#removerModalFooter').css('width', ''); if (mode === 'submit') { var showCb = opts.showCheckbox !== false; var showSub = opts.showSubscribe === true; var ns = mwCfg.wgNamespaceNumber; var notifyLabel = ns === 0 ? 'Оповестить создателя статьи' : (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона' : (ns === 14 || ns === 15) ? 'Оповестить создателя категории' : 'Оповестить создателя страницы'; var cbInlineHtml = ''; if (showSub || showCb) { cbInlineHtml = joinHtml([ '<div id="rmFooterCheckboxes" style="', stFooterChecks, '">', showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '', showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '', '</div>' ]); } $('#removerModalFooter').html(joinHtml([ '<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">', cbInlineHtml, buildFooterActionsHtml(joinHtml([ '<button id="removerCancel" style="', stCancel, '">Отмена</button>', '<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>' ])), '</div>' ])); $('#removerCancel').click(function () { closeModal(); }); $('#removerSubmit').data('rmSubmitInProgress', false).click(function () { if ($(this).data('rmSubmitInProgress')) return; $(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' }); isError = false; if (!opts.preserveLogOnSubmit) { $('#rmLogBox').empty(); logStatusSeq = 0; } if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; } if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; } $(this).data('rmSubmitInProgress', true).prop('disabled', true); var submitResult; try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; } if (submitResult === false) unlockModalSubmit(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click(); }); } else if (mode === 'reload') { var newBtns = buildFooterActionsHtml(joinHtml([ '<button id="removerCancel" style="', stCancel, '">Закрыть</button>', '<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>' ])); $('#rmFooterCheckboxes').remove(); var $btns = $('#rmFooterButtons'); if ($btns.length) { $btns.css({ 'justify-content': 'flex-end' }).html(newBtns); } else { $('#removerModalFooter').append(joinHtml([ '<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">', newBtns, '</div>' ])); } $('#removerCancel').click(function () { closeModal(); }); $('#removerReload').click(function () { location.reload(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.keyCode === 27) $('#removerCancel').click(); if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click(); }); } else { // 'close' $('#removerModalFooter').html(joinHtml([ '<div style="display:flex;justify-content:flex-end;align-items:center;">', '<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>', '</div>' ])); $('#removerCancel').click(function () { closeModal(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.keyCode === 27) $('#removerCancel').click(); }); } } function unlockModalSubmit() { $('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false); } function markSubmitError() { isError = true; var errColor = '#d73333'; $('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false) .addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' }); } // ─── UI: статус и ссылки ───────────────────────────────────────────────── function startProcessing() { if ($('#rmLogBox').length) return; $('#removerModal').append( '<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>' ); syncLinkWidths(); } function logStatus(message, error, opts) { var o = opts || {}; if (o.trackError !== false && error && error.code) isError = true; var $box = $('#rmLogBox'); if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); } var statusId = o.statusId || ('rm-status-' + (++logStatusSeq)); var $row = $box.find('[data-rm-status-id="' + statusId + '"]'); if (!$row.length) { $row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>'); $box.append($row); } var html; if (error) { var errText = error.code ? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>' : escapeHtml(String(error)); html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText; } else if (o.pending) { html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>'; } else { html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>'; } $row.html(html); return statusId; } function formatLogErrorCode(code) { var value = String(code || ''); return value.toLowerCase() === 'error' ? 'Ошибка' : value; } function logPageEdit(pageName, error, opts) { logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts); } function syncLinkWidths() { var $box = $('#rmLogBox'); if (!$box.length) return; var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0; $box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' }); } function appendNominationLink(pageTitle, sectionTitle) { if (!pageTitle) return; var url = getPageUrl(pageTitle); var frag = normalizeSectionForLink(sectionTitle); if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_')); var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle); var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $target.append( '<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' + '<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' + '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>' ); } // ─── UI-строители ──────────────────────────────────────────────────────── function buildInfoBoxHtml(mainText, detailsText, isErr) { var cls = isErr ? ' class="error"' : ''; return joinHtml([ '<div class="rmInfoBox">', '<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>', detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '', '</div>' ]); } function buildActionsHtml(actions, inputName, listId) { var actionItemsHtml = actions.map(function (a, i) { var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : ''); var tagHtml = a.tag ? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>' : ''; return joinHtml([ '<label class="rmActionItem">', '<span class="rmActionMain">', '<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>', tagHtml, '<span>', a.label, '</span>', '</span>', meta ? '<span class="rmActionMeta">' + meta + '</span>' : '', '</label>' ]); }).join(''); return joinHtml([ '<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>', '<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">', actionItemsHtml, '</div>' ]); } function buildNestedCommentFieldsHtml(opts) { var options = opts || {}; var wrapId = options.wrapId || ''; var textareaId = options.textareaId || ''; var textareaClass = options.textareaClass ? ' ' + options.textareaClass : ''; var textareaStyleExtra = options.textareaStyleExtra || ''; var wrapStyleExtra = options.wrapStyleExtra || ''; var placeholder = options.placeholder || 'Комментарий (необязательно)'; var beforeHtml = options.beforeHtml || ''; var marginTop = options.marginTop || '6px'; var minHeight = parseInt(options.minHeight, 10) || 90; var isEmbedded = !!options.embedded; var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"'); var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;'; if (isEmbedded) { wrapStyle += 'padding:0;border:0;background:transparent;'; } else { wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';'; } wrapStyle += wrapStyleExtra; return joinHtml([ '<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">', beforeHtml, '<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass, '" placeholder="', escapeHtml(placeholder), '" style="', stInputFull, 'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>', buildQuickPhrasesPanelHtml(textareaId), '</div>' ]); } function buildConditionalRetFieldsHtml() { return buildNestedCommentFieldsHtml({ wrapId: 'rmCloseConditionalWrap', textareaId: 'rmCloseConditionalReason', placeholder: 'Условие / пояснение (необязательно)', marginTop: '8px', minHeight: 90, beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">' }); } function buildAddMultiPageButtonHtml(options) { var opts = options || {}; var title = opts.addTitle || 'Мультиноминация: добавить страницу'; return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn'); } function buildSquareAddButtonHtml(id, title, className, symbol) { var idAttr = id ? ' id="' + id + '"' : ''; var clsAttr = className ? ' class="' + className + '"' : ''; var label = title || 'Добавить'; return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>'; } function leftControlStyle(style) { return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;'); } function buildLeftSquareAddButtonHtml(id, title, className, symbol) { return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn)); } function buildLeftRemoveButtonHtml(className, title) { var cls = className || 'rmRemoveInput'; var label = title || 'Удалить'; return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>'; } function buildMultiRenameVariantAddButtonHtml() { return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷'); } function buildStartMultiPageButtonHtml(title) { var label = title || 'Мультиноминация: добавить страницу'; return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn'); } function buildMultiPageButtonsHtml(commentWrapId, commentId, options) { var opts = options || {}; var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;'); var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице'; var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице'; if (opts.showAdd) return buildAddMultiPageButtonHtml(opts); return joinHtml([ '<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId, '" data-rm-comment-textarea="', commentId, '" data-rm-comment-title="', escapeHtml(commentTitle), '" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle), '" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>', '<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>' ]); } function buildMultiRenameVariantRowHtml(value, options) { var opts = options || {}; return joinHtml([ '<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">', buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'), '<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'), '" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>', '</div>' ]); } function buildMultiPageRowHtml(index, options) { var opts = options || {}; var pageInputId = 'rmMultiPage' + index; var commentWrapId = 'rmMultiPageCommentWrap' + index; var commentId = 'rmMultiPageComment' + index; var pageValue = opts.pageValue || ''; var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : ''; var inputPlaceholder = opts.inputPlaceholder || 'Страница'; var targetInputClass = opts.targetInputClass || ''; var targetInputHtml = ''; var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)'; var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0'; var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'); var blockStyle = 'max-width:100%;box-sizing:border-box;'; var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, { showAdd: !!opts.showAdd, showComment: !!opts.showComment, addTitle: opts.addTitle, removeTitle: opts.removeTitle, commentTitle: opts.commentTitle, commentExpandedTitle: opts.commentExpandedTitle }); if (opts.targetInput) { targetInputHtml = joinHtml([ '<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'), '" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">' ]); } return joinHtml([ '<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">', '<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">', '<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>', opts.targetVariants ? '' : targetInputHtml, buttonsHtml, '</div>', opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '', buildNestedCommentFieldsHtml({ wrapId: commentWrapId, textareaId: commentId, textareaClass: 'rmMultiPageCommentInput', placeholder: commentPlaceholder, marginTop: multiNominationGap, minHeight: 90, embedded: true, wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;', textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';' }), '</div>' ]); } function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) { var addLabel = 'Добавить вариант нового заголовка'; var currentValue = currentPageName || ''; return joinHtml([ '<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">', '<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">', buildStartMultiPageButtonHtml(startMultiTitle), '</div>', '<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">', buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'), '<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">', '</div>', '<div id="', config.containerId, '"></div>', '</div>' ]); } function showInfoAndClose(mainText, detailsText, isErr) { $('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false)); renderModalFooter('close'); } function getSelectedAction(inputName, actionMap) { var id = $('[name="' + inputName + '"]:checked').val(); var sel = actionMap[id]; if (!sel) alert('Выберите действие.'); return sel || null; } function prependTemplateToNoinclude(text, templateText) { var source = String(text || ''); var tpl = String(templateText || '').trim(); if (!tpl) return source; var match = source.match(RE_NOINCLUDE); if (match) { var before = source.slice(0, source.indexOf(match[0])); if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source; var content = String(match[2] || '').replace(/^\n+/, ''); return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>'); } return '<noinclude>' + tpl + '</noinclude>\n' + source; } function buildGeneratedNominationTemplateText(job, pg) { var tplStr = ''; if (!job) return ''; if (job.opId === 'fRm') { tplStr = job.kbuTemplate || ''; if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo; if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment; return tplStr ? (T_OPEN + tplStr + T_CLOSE) : ''; } if (typeof job.articleTpl !== 'function') return ''; tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]); if (job.opId === 'merge' && job.tplpar) { tplStr = (job.op.nomination.articleTpl)( ('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1), job.date[0] ); } return tplStr ? (T_OPEN + tplStr + T_CLOSE) : ''; } function applyConflictTemplateResolution(articleText, job, pg, decision) { var rule = getNominationConflictRule(job); var generatedTemplate = buildGeneratedNominationTemplateText(job, pg); var source = String(articleText || ''); if (!generatedTemplate || !decision) return source; if (decision.templateAction === 'overwrite') { var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source; return prependTemplateToNoinclude(cleaned, generatedTemplate); } if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate); return source; } function inspectMultiNominationConflicts(job, callback) { var cb = callback || function () {}; var pages = (job && job.multiArticles) ? job.multiArticles.slice() : []; var conflicts = []; var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false }); if (!pages.length) { logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false }); cb(null, conflicts); return; } eachSequential(pages, function (pg, next) { getText(pg, function (articleText, readErr) { var conflict; if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; } if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; } conflict = detectNominationConflict(articleText, job); if (conflict) { conflicts.push($.extend({ pageName: pg }, conflict)); logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false }); } next(); }); }, function (err) { if (err) { logStatus('Проверка статей.', err, { statusId: statusId }); cb(err); return; } logStatus( conflicts.length ? 'Проверка завершена: найдены статьи с уже установленными шаблонами.' : 'Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false } ); cb(null, conflicts); }); } function buildNominationConflictResolutionHtml(conflicts) { return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' + '<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' + '<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' + '<div id="rmConflictList" class="rmConflictList">' + conflicts.map(function (conflict, index) { var pageLink = buildStatusPageLink(conflict.pageName); return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' + '<input type="hidden" class="rmConflictPageAction" value="skip">' + '<input type="hidden" class="rmConflictTemplateAction" value="keep">' + '<div class="rmConflictTitle">' + pageLink + '</div>' + '<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' + '<div class="rmConflictGroup">' + '<div class="rmConflictGroupTitle">Действие со статьёй</div>' + '<div class="rmConflictButtons" data-rm-conflict-group="page">' + '<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' + '</div></div>' + '<div class="rmConflictGroup">' + '<div class="rmConflictGroupTitle">Действие с шаблоном</div>' + '<div class="rmConflictButtons" data-rm-conflict-group="template">' + '<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' + '</div>' + '<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' + '</div></div>'; }).join('') + '</div>'; } function updateNominationConflictCardState($card) { var pageAction = $card.find('.rmConflictPageAction').val() || 'skip'; var disableTemplate = pageAction !== 'keep'; var $templateButtons = $card.find('[data-rm-choice-type="template"]'); $card.toggleClass('is-skip', disableTemplate); $card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate); $templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate); } function bindNominationConflictResolutionUi() { var $content = $('#removerModalContent'); function setChoice($card, type, value) { var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction'; $card.find(inputClass).val(value); $card.find('[data-rm-choice-type="' + type + '"]').each(function () { var isActive = $(this).data('rmChoice') === value; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); updateNominationConflictCardState($card); } $content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () { var $btn = $(this); var $card = $btn.closest('.rmConflictCard'); if ($btn.prop('disabled')) return; setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice')); }); $('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); }); } function collectNominationConflictResolution(conflicts) { var decisions = {}; (conflicts || []).forEach(function (conflict, index) { var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]'); decisions[normTitle(conflict.pageName)] = { pageAction: $card.find('.rmConflictPageAction').val() || 'skip', templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep' }; }); return decisions; } function applyNominationConflictResolutionToJob(job, decisions) { var resultArticles; var headerText; if (!job || !job.isMulti) { job.conflictDecisions = decisions || {}; return { value: job }; } resultArticles = (job.multiArticles || []).filter(function (pageName) { var decision = decisions && decisions[normTitle(pageName)]; return !decision || decision.pageAction !== 'skip'; }); if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' }; job.conflictDecisions = decisions || {}; job.multiArticles = resultArticles.slice(); job.pages = resultArticles.slice().reverse(); if (job.multiRenamePairs && job.multiRenamePairs.length) { job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) { return pair && resultArticles.indexOf(pair.pageName) !== -1; }); } if (job.multiRenameTargets) { job.multiRenameTargets = resultArticles.reduce(function (map, pageName) { map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets'); return map; }, {}); } if (job.multiRenameTemplateTargets) { job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) { map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets'); return map; }, {}); } headerText = String(job.multiHeaderText || '').trim(); job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]')); job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, ''); job.msg = job.multiNominationFormat === 'list' ? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null) : buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false }) : { leadingBlankLine: false }); job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]'); return { value: job }; } function showNominationConflictResolution(job, conflicts, onContinue) { resetModalObservers(); $('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts)); bindNominationConflictResolutionUi(); syncLinkWidths(); renderModalFooter('submit', { submitText: 'Продолжить номинирование', showSubscribe: true, preserveLogOnSubmit: true, onSubmit: function () { var decisions = collectNominationConflictResolution(conflicts); var applied = applyNominationConflictResolutionToJob(job, decisions); if (applied.error) { alert(applied.error); return false; } if (typeof onContinue === 'function') onContinue(applied.value); return true; } }); } function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) { var opts = options || {}; var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180; var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100; var allowWidthResize = opts.allowWidth !== false; var syncFn = typeof sync === 'function' ? sync : function () {}; var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; }; var usePointerEvents = typeof window.PointerEvent === 'function'; var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 }; var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;'; if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';'; var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>'); function getCoord(evt, key) { var e = evt.originalEvent || evt; if (e.touches && e.touches.length) return e.touches[0][key]; if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key]; return e[key]; } function stopDrag() { dragState.active = false; $(window).off('.rmTaResize'); } function onDragMove(evt) { var clientX; var clientY; if (!dragState.active) return; clientX = getCoord(evt, 'clientX'); clientY = getCoord(evt, 'clientY'); if (typeof clientY !== 'number') return; if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px'); $ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px'); syncFn(); if (evt.preventDefault) evt.preventDefault(); } function startDrag(evt) { var clientY = getCoord(evt, 'clientY'); if (typeof clientY !== 'number') return; dragState.active = true; dragState.startX = getCoord(evt, 'clientX') || 0; dragState.startY = clientY; dragState.startWidth = $ta.outerWidth(); dragState.startHeight = $ta.outerHeight(); if (evt.preventDefault) evt.preventDefault(); $(window).off('.rmTaResize'); if (usePointerEvents) { $(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag); } else { $(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag); } } $ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' }); $ta.next('[data-rm-textarea-grip]').remove(); $ta.after($grip); if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag); else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag); } function applyModalContentWidth($modal, contentWidth, options) { var opts = options || {}; var layout = getModalLayout(); var modalFrame = getBoxFrameWidth($modal); var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame)); var modalWidth = safeContentWidth + modalFrame; var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0; $modal.css({ width: modalWidth + 'px', 'max-width': layout.maxOuterWidth + 'px', 'box-sizing': 'border-box', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' }).toggleClass('rmCompactContent', safeContentWidth < 520); $('.' + RESIZE_CLASS).css({ width: safeContentWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); $('#rmMsg,#nominationReason,#rmReportText').each(function () { var $textarea = $(this); var textareaId = this.id; if (!$textarea.length) return; $textarea.css('width', safeContentWidth + 'px'); $textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px'); $('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px'); }); $('.rmNestedCommentInput').each(function () { var $textarea = $(this); var $wrap = $textarea.parent(); var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0; var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0; var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0; var wrapOuterWidth; var textareaWidth; if (!$wrap.length || !$wrap.is(':visible')) return; wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame); textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame); $wrap.css({ width: wrapOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); $textarea.css({ width: textareaWidth + 'px', 'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px' }); $textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px'); $('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px'); }); syncLinkWidths(); if (isVector22) { var $content = $('#content'); if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' }); else if ($content.length) $content.css({ 'min-width': '' }); } else { $('#content').css({ 'min-width': '' }); } if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth); return safeContentWidth; } function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) { var $ta = $('#' + textareaId); var $wrap = $('#' + wrapId); var $modal = $('#removerModal'); var $container = $wrap.parent(); var layout = getModalLayout(); var safeMinWidth = parseInt(minWidth, 10) || 280; var safeMinHeight = parseInt(minHeight, 10) || 90; var initialWidth; var modalFrame; var containerFrame; var wrapFrame; function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } function getMaxTextareaWidth(currentLayout) { return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame); } function getEffectiveMinWidth(currentLayout) { return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout)); } function sync() { var currentLayout = getModalLayout(); var maxTextareaWidth = getMaxTextareaWidth(currentLayout); var textareaWidth = $ta.outerWidth(); var contentWidth; if (!$wrap.is(':visible')) return; if (textareaWidth > maxTextareaWidth) { $ta.css('width', maxTextareaWidth + 'px'); textareaWidth = $ta.outerWidth(); } contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame); applyModalContentWidth($modal, contentWidth); } if (!$ta.length || !$wrap.length || !$modal.length) return; modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width'); containerFrame = $container.length ? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width') : 0; wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width'); initialWidth = Math.min( Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)), getMaxTextareaWidth(layout) ); $ta.css({ width: initialWidth + 'px', 'min-width': getEffectiveMinWidth(layout) + 'px', 'min-height': safeMinHeight + 'px', 'box-sizing': 'border-box', resize: layout.useFullWidth ? 'none' : 'both', 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' }); $ta.data('rmNestedContainerFrame', containerFrame); $ta.data('rmNestedWrapFrame', wrapFrame); $ta.data('rmNestedMinWidth', safeMinWidth); $ta.next('[data-rm-textarea-grip]').remove(); if (layout.useFullWidth) { bindTouchTextareaGrip($ta, sync, function () { return getMaxTextareaWidth(getModalLayout()); }, { minWidth: getEffectiveMinWidth(layout), minHeight: safeMinHeight }); } registerModalLayoutSync(sync); $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); if (typeof ResizeObserver === 'function') { var observer = new ResizeObserver(sync); observer.observe($ta[0]); registerResizeObserver(observer); } sync(); } function setupResizableModal(textareaId) { var $ta = $('#' + textareaId); var $modal = $('#removerModal'); var layout = getModalLayout(); function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } applyV2022Layout($modal); $modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' }); var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box'; var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width'); var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0; var minWidth = layout.minWidth; $modal.data('rmInitialContentW', initialContentW); $ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box', border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase, color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW }); $(window).off('.rmTaResize'); function sync() { var currentLayout = getModalLayout(); var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame)); var w = $ta.outerWidth(); if (w > maxTextareaWidth) { $ta.css('width', maxTextareaWidth + 'px'); w = $ta.outerWidth(); } applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w)); } if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () { return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame)); }); else { $ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' }); $ta.next('[data-rm-textarea-grip]').remove(); } registerModalLayoutSync(sync); $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); if (typeof ResizeObserver === 'function') { var observer = new ResizeObserver(sync); observer.observe($ta[0]); registerResizeObserver(observer); } sync(); } function addInputRow(opts) { var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0; var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0); var rowClass = opts.rowClass ? ' ' + opts.rowClass : ''; var widthStyle = opts.fitParentWidth ? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;' : (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : '')); $('#' + opts.containerId).append(joinHtml([ '<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">', opts.prefixHtml || '', '<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">', opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>', '</div>' ])); syncModalLayout(); } $(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () { $(this).closest('.rmInputRow').remove(); syncModalLayout(); }); $(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) { var targetId; var phrase; e.preventDefault(); targetId = $(this).data('rmTarget'); phrase = $(this).attr('data-rm-phrase') || ''; if (!targetId) return; insertTextIntoTextarea($('#' + targetId), phrase); }); function buildMultiInputHtml(c) { var addLabel = c.addBtnLabel || '+ Добавить'; var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : ''; var addSymbol = c.type === 'rename' ? '⤷' : '+'; return joinHtml([ '<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">', buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol), '</div>', '<div id="', c.containerId, '"></div>' ]); } function wireMultiInput(c) { $('#' + c.addBtnId).click(function () { var count = $('#' + c.containerId + ' .rmInputRow').length; if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; } addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass, rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '', indentLeft: 0, fitParentWidth: c.type === 'rename', prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '', removeBeforeInput: c.type === 'rename' }); }); } function buildSettingsFieldHtml(label, controlHtml, helpText, options) { var opts = options || {}; var helpHtml = helpText ? '<div class="rmSettingsFieldHint">' + helpText + '</div>' : ''; var labelHtml = opts.forId ? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>' : '<div class="rmSettingsFieldLabel">' + label + '</div>'; return joinHtml([ '<div class="rmSettingsField">', labelHtml, '<div class="rmSettingsFieldControl">', controlHtml, '</div>', helpHtml, '</div>' ]); } function buildSettingsSectionHtml(title, bodyHtml, helpText, options) { var opts = options || {}; var headerHtml = ''; var description = []; if (opts.titleNote) description.push(opts.titleNote); if (helpText) description.push(helpText); if (title || description.length) { headerHtml = joinHtml([ '<div class="rmSettingsSectionHeader">', title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '', description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '', '</div>' ]); } return joinHtml([ '<div class="rmSettingsSection">', headerHtml, bodyHtml, '</div>' ]); } function buildSettingsSimpleCheckboxHtml(id, text) { return joinHtml([ '<label class="rmSettingsCheck">', '<input id="', id, '" type="checkbox">', '<span>', text, '</span>', '</label>' ]); } function buildQuickPhrasesSettingsEditorHtml() { return joinHtml([ '<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">', '<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>', '<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">', '<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>', '</div>' ]); } function getQuickPhraseEditor() { return $('#rmSettingsQuickPhrasesEditor'); } function getQuickPhraseEditorState() { var $editor = getQuickPhraseEditor(); var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []); var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10); if (isNaN(editingIndex)) editingIndex = -1; return { editor: $editor, phrases: phrases, editingIndex: editingIndex }; } function setQuickPhraseEditorState(phrases, editingIndex) { var $editor = getQuickPhraseEditor(); var normalized = normalizeQuickPhrasesList(phrases, []); var safeEditingIndex = parseInt(editingIndex, 10); if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1; if (!$editor.length) return; $editor.data('rmQuickPhrases', normalized); $editor.data('rmQuickPhraseEditingIndex', safeEditingIndex); renderQuickPhraseEditor(); } function clearQuickPhraseDropState() { var $editor = getQuickPhraseEditor(); $editor.removeData('rmQuickPhraseDragIndex'); $editor.removeData('rmQuickPhraseDropIndex'); $editor.removeData('rmQuickPhraseDropAfter'); $editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after'); } function renderQuickPhraseEditor() { var state = getQuickPhraseEditorState(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); var $meta = $('#rmSettingsQuickPhraseMeta'); if (!state.editor.length || !$list.length || !$input.length) return; if (state.phrases.length) { $list.html(state.phrases.map(function (phrase, index) { var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : ''); return joinHtml([ '<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">', '<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>', '<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons); $('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || ''); $('#rmSettingsSignatureSeparator').val(data.signatureSeparator || ''); $('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', ')); $('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', ')); setQuickPhraseEditorState(data.quickPhrases || [], -1); $('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing'); clearQuickPhraseDropState(); applyMenuTitlePresetControls(menuTitleValue); updateSignatureSeparatorPreview(data.signatureSeparator || ''); } function collectSettingsFormValues(options) { var opts = options || {}; var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val()); var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val()); var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle'); if (namespaces.invalid.length) { return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' }; } if (disabledItems.invalid.length) { return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' }; } if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput(); return { value: normalizeRemoverSettings({ notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'), subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'), showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'), menuTitle: forcePresetOnly ? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string' ? storedMenuTitle : (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset())) : (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()), signatureSeparator: $('#rmSettingsSignatureSeparator').val(), excludedNamespaces: namespaces.values, disabledItems: disabledItems.values, quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues() }) }; } function updateSettingsSubmitReadyState(baselineSettings) { var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true }); var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings))); $('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError')); $('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none'); } function bindSettingsSubmitReadyState(baselineSettings) { var update = function () { setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0); }; $('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update); $('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on( 'click.rmSettingsReady keyup.rmSettingsReady', '.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput', update ); $('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update); updateSettingsSubmitReadyState(baselineSettings); } function buildSettingsFormHtml(menuLabelsHint) { var menuFields = buildSettingsFieldHtml('Заголовок отдельного меню', '<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(), getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) + buildSettingsFieldHtml('Визуальное оформление меню', '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>'); var messageFields = buildSettingsFieldHtml('Префикс перед подписью', '<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>', { forId: 'rmSettingsSignatureSeparator' }) + buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(), 'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' }); var defaultFields = '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') + buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>'; var disableFields = buildSettingsFieldHtml('Скрыть пункты меню', '<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint, { forId: 'rmSettingsDisabledItems' }) + buildSettingsFieldHtml('Не показывать в пространствах имён', '<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.', { forId: 'rmSettingsExcludedNamespaces' }); return joinHtml([ '<div id="rmSettingsForm" style="max-width:100%;">', '<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>', buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'), buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'), buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'), buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'), '</div>' ]); } function buildSettingsFooterLeftHtml() { return joinHtml([ '<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">', '<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>', '<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ', 'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ', 'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>', '</div>' ]); } function openSettings() { var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults); var menuLabelsHint = buildSettingsMenuItemsHint(); var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $(); var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice(); function restorePreviousModal() { closeModal(); if ($previousModal.length) { $('#content').prepend($previousModal); modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice(); if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); syncModalLayout(); syncLinkWidths(); } } createModal({ title: 'Конфигурация', width: 'compact', showSettingsButton: false }); $('#removerModal').addClass('rmModalSettings'); $('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←')); $('#rmSettingsBack').on('click', restorePreviousModal); $('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint)); fillSettingsFormValues(currentSettings); bindMenuTitlePresetControls(); bindSignatureSeparatorPreview(); bindQuickPhrasesEditor(); renderModalFooter('submit', { showCheckbox: false, submitText: 'Сохранить', onSubmit: function () { var collected = collectSettingsFormValues(); var shouldReset; var saveFn; if (collected.error) { alert(collected.error); return false; } shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults); saveFn = shouldReset ? function (callback) { resetSettingsOnServer(callback); } : function (callback) { saveSettingsToServer(collected.value, callback); }; saveFn(function (err) { if (err) { alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.'); unlockModalSubmit(); return; } location.reload(); }); } }); var $settingsActions = $('#rmFooterActionButtons'); $settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>'); $settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>'); bindSettingsSubmitReadyState(currentSettings); $('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml()); $('#rmSettingsResetFooter').on('click', function () { fillSettingsFormValues(settingsDefaults); updateSettingsSubmitReadyState(currentSettings); $('#removerSubmit').trigger('focus'); }); } // ─── Завершение обработки ──────────────────────────────────────────────── function finalizeSuccess(nominationInfo, usePageReload) { if (isError) { var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>'); markSubmitError(); return; } renderModalFooter('reload'); if (nominationInfo && nominationInfo.pageTitle) { appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle); } if (!usePageReload && !nominationInfo) location.reload(); } function finalizeFastRemoval(notifiedPages, summary) { if (isError || !setAlert || !notifiedPages || !notifiedPages.length) { finalizeSuccess(null, false); return; } notifyAuthorsForPages(notifiedPages, { summary: summary, actionText: 'к быстрому удалению' }, function () { finalizeSuccess(null, false); }); } // ─── Общий runner ──────────────────────────────────────────────────────── /** * Универсальный запуск полного пайплайна номинации. * @param {Object} o * templateStep — функция (next) → обработка шаблонов на статьях * nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle}) * notifyStep — функция (nominationInfo, next) * skipNotify — boolean * skipLink — boolean, не показывать ссылку на номинацию */ function runFlow(o) { runNominationPipeline({ templateStep: o.templateStep, nominationStep: o.nominationStep, notifyStep: o.notifyStep || function (info, next) { next(); }, skipNotify: o.skipNotify, onSuccess: function (ctx) { if (isError) { markSubmitError(); return; } renderModalFooter('reload'); if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) { appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle); } }, onFailure: function () { markSubmitError(); } }); } // ═══════════════════════════════════════════════════════════════════════════ // ЯДРО: обработка статей (apply template + nomination page) // ═══════════════════════════════════════════════════════════════════════════ /** * Применяет шаблон к одной статье/категории. * Понимает режим inArticle (вставка через <noinclude>), * режим closeAction (снятие шаблона + запись на СО), * режим cleanupAction (снятие КБУ/КУЛ). * * @param {string} pg — название страницы * @param {Object} job — параметры задания (см. buildJob) * @param {function} callback(err, meta) */ function applyTemplateToPage(pg, job, callback) { var mode = job.mode; // ── Снятие КБУ/КУЛ ────────────────────────────────────────────────── if (mode === 'cleanup') { var tm = job.transferMode || 'none'; if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; } editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var local = removeTransferTemplatesLocal(article, tm); removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) { if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; } done({ text: updated.text }); }); }, function (err) { callback(err); }); return; } // ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ─────────────── if (mode === 'denom') { getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; } if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; } if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; } var tplPattern = job.sourceTemplate.split('|').map(function (alias) { return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tpl = findTemplateByPattern(article, tplPattern); if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; } var normalizedTplDate = convertToStandardDate(tpl.params[0]); var tplExtraParams = tpl.params.slice(1); var tplExtra = tplExtraParams.join('|').trim(); var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams); if (!RE_DATE_ISO.test(normalizedTplDate)) { callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' }); return; } var date = getDate(normalizedTplDate); var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1]; var retTalkSection = ''; var sectionNW, tplpar, newTalkTpl; if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; } if (job.closeType === 'noRnm') { if (!renameTargets.length) { callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' }); return; } sectionNW = pg + ' → ' + renameTargets.join(', '); tplpar = pg + '|' + renameTargets.join('|'); } if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion') { retTalkSection = tplExtra; sectionNW = retTalkSection || pg; tplpar = retTalkSection ? ('l1=' + retTalkSection) : ''; } var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate); var talkTitle = getTalkPage(pg); newTalkTpl = (job.closeType === 'retConditional') ? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1) : (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE); getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) { if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; } var sourceTalkText = talkText || ''; var talkPageMissing = talkText === null; var talkResult = (job.closeType === 'ret') ? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection) : (job.closeType === 'withdrawnDeletion') ? upsertRemovedFromDeletionTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection) : (job.closeType === 'retConditional') ? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline) : { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' }; function saveArticle() { var cleaned = stripTemplatesByPattern(article, tplPattern).text; var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName }; if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (t) { var editErr = t && t.error ? t.error : null; callback(editErr, editErr ? null : { discussionPage: nomPlace, discussionSection: sectionNW, summary: editSummary }); }); } if (talkResult.text === sourceTalkText) { saveArticle(); return; } var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName }; if (talkPageMissing) talkEp.createonly = true; else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp; apiReq(talkEp, 'edit', function (talkResp) { if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; } saveArticle(); }); }); }); return; } // ── Обычная номинация: вставка шаблона в статью ───────────────────── // mode === 'nominate' var isKu = job.opId === 'tRm' || job.opId === 'mRm'; var conflictRule = getNominationConflictRule(job); var conflictLabel = conflictRule ? conflictRule.label : 'номинации'; editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var hasExistingNomination = conflictRule && conflictRule.detect(article); var conflictDecision = getConflictDecisionForPage(job, pg); function buildResult(finalText) { var generatedTpl = buildGeneratedNominationTemplateText(job, pg); return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText }; } function finishConflictResolution(sourceText) { var resolvedText; var pageLink = buildQuotedStatusPageLink(pg); if (conflictDecision.templateAction === 'keep') { if (sourceText !== article) { return { text: sourceText, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' } }; } return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } }; } resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision); return { text: resolvedText, meta: { successMessage: conflictDecision.templateAction === 'overwrite' ? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.' : 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.' } }; } if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) { return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } }; } if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') { if (job.transferMode && job.transferMode !== 'none') { var localConflict = removeTransferTemplatesLocal(article, job.transferMode); removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) { done(finishConflictResolution(updated.text)); }); return; } return finishConflictResolution(article); } if (isKu && job.transferMode && job.transferMode !== 'none') { var local = removeTransferTemplatesLocal(article, job.transferMode); removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); }); return; } return buildResult(article); }, function (err) { callback(err); } ); } /** * Обрабатывает список страниц последовательно. * @param {string[]} pages * @param {Object} job * @param {function} onDone(notifiedPages, err, pageMeta) */ function processPageList(pages, job, onDone) { var notifiedPages = []; var pageMeta = {}; eachSequential(pages.slice().reverse(), function (pg, nextPage) { var pageLink = buildQuotedStatusPageLink(pg); var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false }); applyTemplateToPage(pg, job, function (err, meta) { var normPg = normTitle(pg); var isClose = job.mode === 'cleanup' || job.mode === 'denom'; if (!isClose) { if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false }); else logPageEdit(pg, err, { statusId: statusId }); } else { if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); } else { logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false }); if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false }); } } if (!err) { notifiedPages.push(pg); if (meta) pageMeta[normPg] = meta; } nextPage(err || null); }); }, function (err) { onDone(notifiedPages, err, pageMeta); }); } // ═══════════════════════════════════════════════════════════════════════════ // ПОСТРОЕНИЕ JOB из формы // ═══════════════════════════════════════════════════════════════════════════ /** * Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov). * @param {Object} op — запись из OPERATIONS * @param {string} pg — целевая страница (уже разрешённая) * @param {boolean} isMulti — режим мультиноминации * @returns {Object|false} — job или false при ошибке ввода */ function buildNominationJob(op, pg, isMulti) { var nom = op.nomination; var date = getDate(); var msg = normalizeQuickPhraseValue($('#rmMsg').val()); var rawMsg = msg; var opId = isMulti ? (nom.multiOpId || op.id) : op.id; var tplpar = ''; var section, sectionNW, extraPages, multiArticles = []; var multiHeaderText = ''; var multiArticleComments = {}; var multiFormat = 'sections'; var multiRenamePairs = []; var multiRenameTargets = {}; var multiRenameTemplateTargets = {}; var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length; // Вычислить section и tplpar в зависимости от типа дополнительного ввода if (nom.extraInput) { var ei = nom.extraInput; if (ei.type === 'rename') { if (isRenameWithRowTargets) { multiRenamePairs = collectMultiRenamePairs(); if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false; multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames'); multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames'); tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames); section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames); } else { var rn = collectInputValues('.rmRenameInput'); if (!rn.length) { alert('Укажите новое название.'); return false; } tplpar = buildRenameTemplateParam(rn); section = formatRenameItemLabel(pg, rn); } } else if (ei.type === 'merge') { var mn = collectInputValues('.rmMergeInput'); if (!mn.length) { alert('Укажите статью для объединения.'); return false; } tplpar = pg + '|' + mn.join('|'); extraPages = mn; section = formatPagesWithAnd([pg].concat(mn)); } else if (ei.type === 'split') { var sn = collectInputValues('.rmSplitInput'); if (!sn.length) { alert('Укажите статьи для разделения.'); return false; } tplpar = formatPagesWithAnd(sn); section = '[[:' + pg + ']] → ' + tplpar; } } if (isMulti) { var ttl = $('#rmHeader').val() || ''; var articles = isRenameWithRowTargets ? multiRenamePairs.map(function (pair) { return pair.pageName; }) : collectInputValues('.rmMultiPageInput'); multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections'; multiArticleComments = collectMultiNominationComments(); multiHeaderText = ttl; multiArticles = articles.slice(); if (!articles.length) { alert('Укажите страницу.'); return false; } if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false; section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : ''); msg = multiFormat === 'list' ? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null) : buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false }) : { leadingBlankLine: false }); } if (!section) section = '[[:' + pg + ']]'; sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, ''); var nomPageDate = date[1]; var nomPage = nom.nomPage(nomPageDate); var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]'); return { mode: 'nominate', opId: opId, op: op, date: date, tplpar: tplpar, articleTpl: nom.articleTpl || function () { return ''; }, inArticle: nom.inArticle !== false, transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'), summary: summary, msg: msg, nomPage: nomPage, navTemplate: nom.navTemplate, section: section, sectionNW: sectionNW, comment: nom.comment || '', extraPages: extraPages || [], isMulti: !!isMulti, multiHeaderText: multiHeaderText, multiNominationBody: rawMsg, multiArticleComments: multiArticleComments, multiNominationFormat: multiFormat || 'sections', multiRenamePairs: multiRenamePairs, multiRenameTargets: multiRenameTargets, multiRenameTemplateTargets: multiRenameTemplateTargets, multiArticles: multiArticles, pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || [])) }; } function getTransferModeFromButtons() { var kbu = $('#rmTransferBtnKbu').hasClass('is-active'); var kul = $('#rmTransferBtnKul').hasClass('is-active'); if (kbu && kul) return 'both'; if (kbu) return 'kbu'; if (kul) return 'kul'; return 'none'; } function buildKbuFormHtml(reasons) { return joinHtml([ '<select id="rmSel" style="', stInputFull, '">', reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''), '</select>', '<input id="fiRm" type="hidden" style="', stInputFull, '">', '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">', buildQuickPhrasesPanelHtml('fiRmComment') ]); } function buildNominationMultiHeaderHtml(pg, options) { var opts = options || {}; var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'); return joinHtml([ '<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">', '<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">', buildAddMultiPageButtonHtml(opts), '</div>', '</div>', '<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">', buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })), '</div>' ]); } function buildTransferBoxHtml() { return joinHtml([ '<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">', '<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>', '<div id="rmTransferModeGroup" class="rmSegmentedBar">', '<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>', '<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>', '</div>', '<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>', '</div></div>' ]); } function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) { return joinHtml([ '<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">', '<div class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>', '</div>', '</div>' ]); } function getMultiNominationUiOptions(kind, options) { var opts = options || {}; var isArticle = kind === 'article'; var isRename = !!opts.renameMulti; var actionText = typeof opts.actionText === 'string' ? opts.actionText : (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : '')); var itemAcc = isArticle ? 'статью' : 'категорию'; var itemDat = isArticle ? 'статье' : 'категории'; var itemGen = isArticle ? 'статьи' : 'категории'; var result = { inputPlaceholder: isArticle ? 'Статья' : 'Категория', addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''), removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''), commentTitle: 'Добавить комментарий к этой ' + itemDat, commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat, commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)' }; if (isRename) { $.extend(result, { targetInput: true, targetInputClass: 'rmMultiRenameTargetInput', targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:', targetVariants: true }); } if (opts.setup) { $.extend(result, { defaultPage: opts.defaultPage || '', multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap' }); if (isRename) $.extend(result, { singleOnlySelector: '#rmSingleRenameBlock', hideContainerWhenSingle: true, singleCurrentPageSelector: '#rmSingleRenameCurrent', singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput', singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput', singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer', singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия', singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined }); } return result; } function buildNominationFormHtml(nom, pg, multiMode) { var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename'; var multiOptions = getMultiNominationUiOptions('article', { renameMulti: isRenameMulti, deletionMulti: multiMode && nom.template === 'к удалению' }); return joinHtml([ isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '', multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '', nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '', '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>', buildQuickPhrasesPanelHtml('rmMsg'), nom.supportsTransfer ? buildTransferBoxHtml() : '', multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : '' ]); } function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) { var opts = options || {}; var multiOptions = getMultiNominationUiOptions('category', opts); return joinHtml([ opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '', multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '', variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '', '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>', buildQuickPhrasesPanelHtml('nominationReason'), multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : '' ]); } function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) { var $textarea = $('#' + textareaId); if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return; setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90); $textarea.data('rmNestedResizerReady', true); } function setMultiPageCommentExpanded($btn, expanded) { var wrapId = $btn.data('rmCommentWrap'); var textareaId = $btn.data('rmCommentTextarea'); var $wrap = $('#' + wrapId); var title = expanded ? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице') : ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице'); if (!$btn.length || !$wrap.length) return; $btn.attr('aria-expanded', expanded ? 'true' : 'false') .attr('aria-label', title) .attr('title', title) .toggleClass('is-active', expanded) .text('✎'); $wrap.toggle(expanded); if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId); } function getMultiPageCommentTargets($block) { var $textarea = $block.find('.rmMultiPageCommentInput').first(); var $wrap = $textarea.parent(); return { wrapId: $wrap.attr('id') || '', textareaId: $textarea.attr('id') || '' }; } function collectMultiRenameTargetValues($block) { return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () { return ($(this).val() || '').trim(); }).get().filter(Boolean); } function setMultiPageRowControls($block, showAdd, showComment, options) { var opts = options || {}; var $row = $block.find('.rmMultiPageRow').first(); var ids = getMultiPageCommentTargets($block); var $commentBtn = $row.find('.rmMultiPageCommentToggle'); if (!$row.length) return; if (showAdd) { if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false); if (ids.wrapId) $('#' + ids.wrapId).hide(); $block.find('.rmMultiRenameVariantsContainer').hide(); $row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove(); if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts)); return; } $block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants); $row.find('.rmAddMultiPage').remove(); if (!$row.find('.rmMultiPageCommentToggle').length) { $row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment }))); return; } $row.find('.rmMultiPageCommentToggle').toggle(showComment); } function setupMultiPageNominationUi(options) { var opts = options || {}; var containerSelector = opts.containerSelector || '#rmMultiPagesContainer'; var pageCounter = parseInt(opts.nextIndex, 10) || 1; var wasMultiModeExpanded = false; function restoreEmptySinglePageInput() { var $pageInput = $(containerSelector + ' .rmMultiPageInput').first(); if (!$pageInput.length || String($pageInput.val() || '').trim()) return; $pageInput.val(opts.defaultPage || ''); } function copySingleCurrentPageToFirstRow() { var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $(); var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first(); var value = String(($source.val && $source.val()) || '').trim(); if (!$source.length || !$target.length || !value) return; $target.val(value); } function copyFirstRowPageToSingleCurrent() { var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $(); var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first(); var value = String(($source.val && $source.val()) || '').trim(); if (!$source.length || !$target.length) return; $target.val(value || opts.defaultPage || ''); } function getSingleRenameTargets() { var targets = []; var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $(); if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim()); if (opts.singleRenameVariantSelector) { $(opts.singleRenameVariantSelector).each(function () { var value = String($(this).val() || '').trim(); if (value) targets.push(value); }); } return targets.slice(0, 3); } function setMultiBlockRenameTargets($block, targets) { var list = asNonEmptyArray(targets).slice(0, 3); var $target = $block.find('.rmMultiRenameTargetInput').first(); var $container = $block.find('.rmMultiRenameVariantsContainer').first(); if (!$target.length) return; $target.val(list[0] || ''); if (!$container.length) return; $container.find('.rmMultiRenameVariantRow').remove(); list.slice(1).forEach(function (value) { $container.append(buildMultiRenameVariantRowHtml(value, opts)); }); } function copySingleRenameTargetsToFirstRow() { var $block = $(containerSelector + ' .rmMultiPageBlock').first(); if (!$block.length) return; setMultiBlockRenameTargets($block, getSingleRenameTargets()); } function copyFirstRowRenameTargetsToSingle() { var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $(); var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first(); var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3); var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $(); if (!$sourceBlock.length || !$target.length) return; $target.val(targets[0] || ''); if (!$variantContainer.length) return; $variantContainer.empty(); targets.slice(1).forEach(function (value) { addInputRow({ containerId: opts.singleRenameVariantContainerId, placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант', inputClass: opts.singleRenameInputClass, rowClass: 'rmRenameVariantRow', indentLeft: 0, fitParentWidth: true, prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'), removeBeforeInput: true }); $variantContainer.find('input').last().val(value); }); } function updateMultiMode() { var $blocks = $(containerSelector + ' .rmMultiPageBlock'); var hasExtra = $blocks.length > 1; var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap; $(containerSelector).css({ gap: pageGap, marginBottom: pageGap }); $('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra); if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra); if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra); if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra); if (!hasExtra && wasMultiModeExpanded) { restoreEmptySinglePageInput(); copyFirstRowPageToSingleCurrent(); copyFirstRowRenameTargetsToSingle(); } $blocks.each(function (index) { setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts); }); wasMultiModeExpanded = hasExtra; syncModalLayout(); } $(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () { var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1; if (wasSingle) { copySingleCurrentPageToFirstRow(); copySingleRenameTargetsToFirstRow(); } $(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts)); updateMultiMode(); }); $(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () { var $block = $(this).closest('.rmMultiPageBlock'); var $container = $block.find('.rmMultiRenameVariantsContainer').first(); var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length; if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; } $container.append(buildMultiRenameVariantRowHtml('', opts)).show(); syncModalLayout(); }); $(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () { $(this).closest('.rmMultiRenameVariantRow').remove(); syncModalLayout(); }); $(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () { var $btn = $(this); if (!$btn.is(':visible')) return; setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true'); syncModalLayout(); }); $(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () { $(this).closest('.rmMultiPageBlock').remove(); updateMultiMode(); }); updateMultiMode(); return { update: updateMultiMode, isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; } }; } function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) { var $root = $(rootSelector); $root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () { var $btn = $(this); $root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false'); $btn.addClass('is-active').attr('aria-pressed', 'true'); }); } function buildProtectAddButtonHtml() { return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage'); } function buildProtectPageRowHtml(id, pageName, isFirstRow) { return joinHtml([ '<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"', pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>', isFirstRow ? buildProtectAddButtonHtml() : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>', '</div>' ]); } function buildReportFormHtml(ctx, isZka) { var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.'; if (isZka) { return joinHtml([ '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">', '<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>', buildQuickPhrasesPanelHtml('rmReportText') ]); } return joinHtml([ '<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">', '<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>', '<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>', '</div></div>', '<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">', '<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>', buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true), '<div id="rmProtectPagesContainer"></div>', '</div>', '<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>', '</div></div>', '<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>', '</div></div>', '<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>', '</div></div>', '<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>', buildQuickPhrasesPanelHtml('rmReportText'), '</div>' ]); } // ═══════════════════════════════════════════════════════════════════════════ // ОБРАБОТЧИКИ ОПЕРАЦИЙ // ═══════════════════════════════════════════════════════════════════════════ var handlers = { // ── КБУ ───────────────────────────────────────────────────────────── showKbu: function (op) { var forCategory = !!(op && op.forCategory); var reasons = getFastRemoveReasons(); createModal({ title: 'Быстрое удаление', width: 'compact', subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>' }); $('#removerModalContent').html(buildKbuFormHtml(reasons)); function updateKbuReasonControls() { var reason = reasons[$('#rmSel').val()] || reasons[0]; var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null; var showComment = true; $('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason)); if (paramCfg) { var noComment = paramCfg.charAt(0) === '!'; $('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show(); showComment = !noComment; } else { $('#fiRm').attr('type', 'hidden').hide(); } $('#fiRmComment').toggle(showComment); $('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment); } $('#rmSel').change(updateKbuReasonControls); $('#rmSel').trigger('change'); renderModalFooter('submit', { submitText: 'Номинировать', onSubmit: function () { var idx = $('#rmSel').val(); var addInfo = $('#fiRm').val(); var comment = $('#fiRmComment').val(); startProcessing(); if (forCategory) { var tpl = reasons[idx][0]; var categorySummary = makeSummary('номинация категории на быстрое удаление'); if (addInfo) tpl += '|1=' + addInfo; if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment; editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' }, function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; }, function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else { logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false }); finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary); } }); } else { var job = { mode: 'nominate', opId: 'fRm', kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment, summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'), inArticle: true }; processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) { finalizeFastRemoval(notifiedPages, job.summary); }); } return true; } }); }, // ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ──────── showNomination: function (op) { var nom = op.nomination; var pg = normTitle(mwCfg.wgPageName); var date = getDate()[1]; var nomPage = nom.nomPage(date); var multiMode = nom.supportsMulti; function updateTransferUi() { var mode = getTransferModeFromButtons(); var isNone = mode === 'none'; var isKbu = mode === 'kbu' || mode === 'both'; var isKul = mode === 'kul' || mode === 'both'; $('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false'); $('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false'); $('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false'); var t = transferTexts[mode]; if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); } applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : ''); } createModal({ title: 'Номинация: ' + nom.template, subtitlePage: nomPage, subtitleLabel: 'Текущий день' }); $('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode)); setupResizableModal('rmMsg'); // Логика переноса if (nom.supportsTransfer) { $(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () { if (this.id === 'rmTransferBtnNone') { $('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active'); $('#rmTransferBtnNone').addClass('is-active'); } else { $(this).toggleClass('is-active'); var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active'); $('#rmTransferBtnNone').toggleClass('is-active', !anyOn); } updateTransferUi(); }); updateTransferUi(); } // Многостраничный режим if (multiMode) { setupMultiPageNominationUi(getMultiNominationUiOptions('article', { setup: true, defaultPage: pg, renameMulti: nom.extraInput && nom.extraInput.type === 'rename', deletionMulti: nom.template === 'к удалению' })); bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn'); } if (nom.extraInput) wireMultiInput(nom.extraInput); renderModalFooter('submit', { submitText: 'Номинировать', showSubscribe: true, onSubmit: function () { var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1; var singleCurrentInput = $('#rmSingleRenameCurrent').val() || ''; var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : ''; var changed = inputVal && inputVal !== pg; function executeJob(job) { startProcessing(); runFlow({ templateStep: function (next) { if (!job.inArticle) { next(); return; } processPageList(job.pages, job, function (notifiedPages, err) { job._notifiedPages = notifiedPages; next(err); }); }, nominationStep: function (done) { publishNomination({ pageTitle: job.nomPage, navTemplate: job.navTemplate, sectionTitle: job.section, summary: job.summary, text: getNominationPublishText(job) }, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); }); }, notifyStep: function (nominationInfo, next) { var pages = job._notifiedPages || []; if (!setAlert || !pages.length) { next(); return; } notifyAuthorsForPages(pages, { summary: job.summary, actionText: job.comment, discussionPage: nominationInfo && nominationInfo.pageTitle, discussionSection: nominationInfo && nominationInfo.sectionTitle }, next); }, skipLink: op.id === 'fRm' }); } function run(targetPg) { var job = buildNominationJob(op, targetPg, isMulti); if (!job) { unlockModalSubmit(); return; } if (job.isMulti && job.inArticle && getNominationConflictRule(job)) { startProcessing(); inspectMultiNominationConflicts(job, function (err, conflicts) { if (err) { markSubmitError(); return; } if (!conflicts.length) { executeJob(job); return; } showNominationConflictResolution(job, conflicts, function (resolvedJob) { executeJob(resolvedJob); }); }); return; } executeJob(job); } if (changed) { apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) { if (data && data.error) { unlockModalSubmit(); alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.'); return; } var page = getFirstQueryPage(data); if (!page) { unlockModalSubmit(); alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.'); return; } if (page.missing !== undefined) { unlockModalSubmit(); alert('Страница «' + inputVal + '» не существует.'); return; } run(normTitle(page.title || inputVal)); }); } else { run(pg); } return true; } }); }, // ── Снятие номинации (статья) ──────────────────────────────────────── showArticleClose: function () { showCloseActionsModal({ inputName: 'rmCloseAction', listId: 'rmCloseActions', emptyText: 'Не найдено подходящих шаблонов для закрытия.', emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.', getActions: function (articleText) { var actions = []; if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true }); if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true }); if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снятие', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true }); if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true }); if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true }); var hasKbu = RE_KBU_ON_PAGE.test(articleText); var hasKul = RE_KUL_ON_PAGE.test(articleText); if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' }); if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' }); if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' }); return actions; }, afterRender: function (actions) { var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; }); var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; }); if (hasDoneRnm) { $('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append( '<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>' ); } if (hasConditionalRet) { $('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml()); } }, afterFooterRender: function (_, actionMap) { function ensureConditionalTextareaResizer() { var $textarea = $('#rmCloseConditionalReason'); if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return; setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90); $textarea.data('rmConditionalResizerReady', true); } function updateUi() { var sel = actionMap[$('[name="rmCloseAction"]:checked').val()]; $('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle)); $('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields)); if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer(); var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu'); var $cb = $('[name="rmUAlert"]'); var $cbLabel = $('[name="rmUAlert"]').closest('label'); if ($cb.length) $cb.prop('disabled', disableNotify); if ($cbLabel.length) $cbLabel.css({ visibility: disableNotify ? 'hidden' : 'visible', pointerEvents: disableNotify ? 'none' : '' }); syncModalLayout(); } $(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi); updateUi(); }, onSubmit: function (sel, pageName) { var job; if (sel.mode === 'denom') { var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : ''; var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : ''; var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : ''; if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; } if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) { alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.'); return false; } job = { mode: 'denom', closeType: sel.closeType, resultTemplate: sel.resultTemplate, sourceTemplate: sel.sourceTemplate, oldTitle: oldTitle, conditionalReason: conditionalReason, conditionalDeadline: conditionalDeadline, notifyActionText: sel.comment, skipNotify: false }; } else { job = { mode: 'cleanup', transferMode: sel.transferMode, summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel), notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both') ? 'больше не номинирована к срочному улучшению' : '', skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both') }; } processPageList([pageName], job, function (notifiedPages, err, pageMeta) { function finishClose() { if (isError) { markSubmitError(); } else { renderModalFooter('reload'); } } if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; } var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {}; notifyAuthorsForPages(notifiedPages, { summary: meta.summary || job.summary, actionText: job.notifyActionText, discussionPage: meta.discussionPage, discussionSection: meta.discussionSection, includeProposedPrefix: false }, finishClose); }); return true; } }); }, // ── ОБКАТ: номинация категории ─────────────────────────────────────── showCatNomination: function (op) { var catType = op.catType; var catMeta = getCategoryNominationMeta(catType); var now = new Date(); var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear(); var pageName = normalizeCategoryPageName(mwCfg.wgPageName); var multiMode = !!catMeta.supportsMulti; var renameMultiMode = !!catMeta.renameMulti && multiMode; createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' }); var variantCfgs = { rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' }, merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' } }; var vCfg = variantCfgs[catType]; var categoryMultiOptions = { renameMulti: renameMultiMode, actionText: catMeta.actionText, skipVariantInput: renameMultiMode }; $('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions)); setupResizableModal('nominationReason'); if (multiMode) { setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({ setup: true, defaultPage: pageName }, categoryMultiOptions))); bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn'); } if (vCfg) wireMultiInput(vCfg); renderModalFooter('submit', { submitText: 'Номинировать', showSubscribe: true, onSubmit: function () { var reason = normalizeQuickPhraseValue($('#nominationReason').val()); var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1; var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({ normalizePageName: normalizeCategoryPageName, normalizeTargetName: normalizeCategoryTargetPageName, normalizeTemplateTargetName: normalizeCategoryTargetName }) : []; var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames'); var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames'); var singleRenameCategory = renameMultiMode && !hasMultiRenameRows ? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName) : ''; var targetPages = hasMultiRenameRows ? renamePairs.map(function (pair) { return pair.pageName; }) : (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]); if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName]; var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1); var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections'; var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {}; var notifiedPages = []; if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false; if (!targetPages.length) { alert('Укажите категорию.'); return false; } if (isMulti) { if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false; } else if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; } var discussionTarget = isMulti ? targetPages : targetPages[0]; var discussionReason = isMulti ? (multiFormat === 'list' ? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null) : buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 }) : { headingLevel: 4 })) : reason; var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true, renameTargetsByPage: renameTargetsByCategory } : null; var mainName = null, additionalNames = []; if (vCfg) { if (hasMultiRenameRows) { mainName = renamePairs[0] && renamePairs[0].templateTargetName; additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : []; } else { mainName = $('#' + vCfg.firstId).val().trim(); if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; } additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput'); } } startProcessing(); runFlow({ templateStep: function (next) { addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) { notifiedPages = processedPages || []; next(err); }, hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : null); }, nominationStep: function (done) { createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions); }, notifyStep: function (nominationInfo, next) { if (!setAlert || !nominationInfo) { next(); return; } var section = normalizeSectionForLink(nominationInfo.sectionTitle || ''); notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, { summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'), actionText: catMeta.actionText, discussionPage: nominationInfo.pageTitle, discussionSection: nominationInfo.sectionTitle }, next); } }); return true; } }); }, // ── Снятие номинации (категория) ───────────────────────────────────── showCatClose: function () { showCloseActionsModal({ inputName: 'rmCategoryCloseAction', showCheckbox: false, emptyText: 'Не найдено подходящих шаблонов для завершения.', emptyDetails: 'Проверяются ОБКАТ и КУ.', getActions: function (catText) { var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|'); var actions = []; if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) { actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true }); } if (RE_KU_ON_PAGE.test(catText)) { actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' }); } return actions; }, onSubmit: function (sel, pageName) { if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName); if (sel.mode === 'cleanup') removeKuFromCategory(pageName); return true; } }); }, // ── Защита / Запрос к администраторам ─────────────────────────────── showReport: function (op) { var mode = op.reportMode || 'protect'; var ctx = getReporterContext(mode); var isZka = mode === 'request'; var protectMode = 'install'; var pageCounter = 1; function buildProtectText(pm) { if (pm === 'remove') { var removeLevels = []; $('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); }); return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : ''; } var levels = [], reasons = []; $('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); }); $('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); }); if (!levels.length && !reasons.length) return ''; var text = 'Просьба установить'; if (levels.length) text += ' ' + levels.join(' и/или '); if (reasons.length) text += ' по причине: ' + reasons.join(', '); return text + '.'; } function applyProtectMode(m) { protectMode = m; var isInstall = m === 'install'; $('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false'); $('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false'); $('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты'); var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты'; $('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>'); $('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall); $('#rmRemoveLevelsWrap').toggle(!isInstall); $('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active'); $('#rmReportText').val('').removeData('rmGenerated'); } function updateProtectMultiUi() { var $rows = $('#rmProtectMultiWrap .rmProtectPageRow'); var hasExtra = $rows.length > 1; if (!$rows.length) { $('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true)); $rows = $('#rmProtectMultiWrap .rmProtectPageRow'); hasExtra = false; } $('#rmProtectHeaderWrap').toggle(hasExtra); if (!hasExtra) $('#rmProtectHeader').val(''); $rows.each(function () { var $row = $(this); $row.find('.rmProtectAddPage,.rmRemoveInput').remove(); $row.append(hasExtra ? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>' : buildProtectAddButtonHtml() ); }); syncModalLayout(); } createModal({ title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы', width: 'compact', subtitleHtml: isZka ? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 'Категория:' + target; addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) { var url = getPageUrl(targetPage); var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>'; if (success) { var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : ''; logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false }); } else { logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false }); } next(); }); }, cb); } function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) { editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' }, function (text) { var existing = text.match(getCategoryMergeRe()); if (existing) { var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; }); var norm = sourceCatName.replace(/\s+/g, ' ').trim(); if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; } return { text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }), summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'), meta: { status: 'updated' } }; } return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } }; }, function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); } ); } // ── Категории: обсуждение ──────────────────────────────────────────────── function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) { var opts = options || {}; var pages = Array.isArray(pageName) ? pageName : null; var titleText; if (pages && pages.length) { titleText = String(opts.headerText || '').trim(); if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage); if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''); return '=== ' + titleText + ' ==='; } var title = '=== [[:' + pageName + ']]'; if (type === 'rename' || type === 'merge') { title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName); if (additionalNames && additionalNames.length) { var conj = type === 'rename' ? ' или ' : ' и '; var head = additionalNames.slice(0, -1).map(formatCatLink).join(', '); title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]); } } else if (type === 'deletion') { title += ' → удалить'; } return title + ' ==='; } function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) { var cb = callback || function () {}; var opts = options || {}; var now = new Date(); var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate(); var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' =='; var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year; var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts); var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, ''); var discText = discTitle + '\n' + discBody + '\n'; var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim(); var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]'; var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget; publishNomination({ pageTitle: discPage, readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.', summary: makeSummary(summaryText), createText: function () { return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText; }, buildText: function (text) { var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text); if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) { return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } }; } if (todayMatch) { var dayContentStart = todayMatch.index + todayMatch[0].length; var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart)); var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length; return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') }; } return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) }; } }, function (err) { if (err) { cb(err); return; } cb(null, { pageTitle: discPage, sectionTitle: sectionTitle }); }); } // ── Категории: завершение ОБКАТ ─────────────────────────────────────────── function markCategoryDiscussionAsDone(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон обсуждения...', errorText: 'Снятие шаблона обсуждения.', successText: 'Шаблон обсуждения снят.', editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' }, buildFn: function (text) { var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|'); var patterns = [ new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'), new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i') ]; var match = null; for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; } if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } }; return { text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'), meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) } }; }, onSuccess: function (meta) { var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false }); updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) { if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; } logStatus( (info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.', null, { statusId: talkStatusId, trackError: false } ); renderModalFooter('reload'); }); } }); } function updateCategoryTalkPage(categoryName, templateDate, callback) { var cb = callback || function () {}; var talkPage = getTalkPage(categoryName); var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE; getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) { if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; } if (text === null) { apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true }, 'edit', function (resp) { if (resp && resp.error) { if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000); else cb(resp.error); } else cb(null, { status: 'created' }); }); return; } var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i'); var tplMatch = text.match(discussedRe); var newText = text; if (tplMatch) { var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean); if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; } newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); }); } else { newText = insertTplOnTalkPage(text, newTpl); } if (newText === text) { cb(null, { status: 'no_changes' }); return; } var ep = { title: talkPage, text: newText, summary: tplMatch ? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate) : makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate) }; if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); }); }); } // ── Быстрое объединение (Ctrl+клик КОБ) ───────────────────────────────── function buildQuickMergeHtml(tplDate, targets, currentCatName) { return joinHtml([ '<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>', '<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">', targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'), '</pre>', '<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>', '<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>' ]); } function showQuickMergeModal() { getText(mwCfg.wgPageName, function (text, readErr) { if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; } if (!text) { alert('Не удалось получить содержимое.'); return; } var mergeRe = getCategoryMergeRe(); var match = text.match(mergeRe); if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; } var params = match[1].split('|').map(function (p) { return p.trim(); }); var tplDate = params[0]; var targets = params.slice(1); if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; } createModal({ title: 'Быстрое добавление шаблона объединения' }); var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName)); $('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName)); renderModalFooter('submit', { showCheckbox: false, submitText: 'Добавить шаблоны', onSubmit: function () { startProcessing(); $('#removerSubmit').prop('disabled', true); eachSequential(targets, function (target, next) { addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) { if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false }); else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false }); next(); }); }, function () { if (isError) markSubmitError(); else renderModalFooter('close'); }); return true; } }); }); } // ── ЗКА/Защита: публикация ─────────────────────────────────────────────── function getReporterContext(mode) { var rawPage = mwCfg.wgPageName; var pageName = normTitle(rawPage) .replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:') .replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:'); var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage); var displayName = normTitle(rawPage) .replace(/(Special|Служебная):(Вклад|Contributions)\//i, '') .replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '') .replace(/(user|участни(к|ца)):/i, ''); var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]'); var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты'; return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage }; } function getTopDiscussionInsertIndex(pageText) { var introEnd = getIntroBlockEndIndex(pageText); var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd)); return firstHeading ? introEnd + firstHeading.index : introEnd; } function getIntroBlockEndIndex(pageText) { var pos = 0; while (pos < pageText.length) { var next = skipWhitespace(pageText, pos); var end; if (pageText.slice(next, next + 4) === '<!--') { end = pageText.indexOf('-->', next + 4); if (end === -1) return next; pos = end + 3; continue; } end = skipTemplate(pageText, next); if (end !== next) { pos = end; continue; } return next; } return pos; } function skipWhitespace(text, pos) { while (pos < text.length && /\s/.test(text.charAt(pos))) pos++; return pos; } function skipTemplate(text, pos) { var depth = 0; var i = pos; if (text.slice(pos, pos + 2) !== '{{') return pos; while (i < text.length) { if (text.slice(i, i + 4) === '<!--') { var commentEnd = text.indexOf('-->', i + 4); if (commentEnd === -1) return pos; i = commentEnd + 3; continue; } if (text.slice(i, i + 2) === '{{') { depth++; i += 2; continue; } if (text.slice(i, i + 2) === '}}') { depth--; i += 2; if (depth === 0) return i; continue; } i++; } return pos; } function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) { var sep = separator || '\n\n'; var before = pageText.slice(0, insertAt).replace(/\s+$/, ''); var block = String(blockText || '').replace(/\s+$/, ''); var after = pageText.slice(insertAt).replace(/^\s+/, ''); return (before ? before + sep : '') + block + (after ? sep + after : '\n'); } function insertTopDiscussionSection(pageText, sectionText) { return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n'); } function doReport(ctx, fast, protectMode) { var header = $('#rmReportHeader').val() || ctx.pageLink; var text = $('#rmReportText').val() || ''; var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам'; var isRemoveProtect = !isZka && protectMode === 'remove'; startProcessing(); var targetPage, editParams, sectionForLink; if (fast) { targetPage = 'Википедия:Запросы к администраторам/Быстрые'; sectionForLink = null; editParams = { appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName + '| страница = | пояснение = ' + text + T_CLOSE + '\n', summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]') }; } else if (isZka) { targetPage = ctx.reportPage; sectionForLink = extractDisplayedText(header); var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName); editParams = { text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~', summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос') }; } else { targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты'; var pages = collectInputValues('.rmProtectPageInput'); if (!pages.length) pages = [ctx.pageName]; var sectionTitle, pageLines; if (pages.length === 1) { sectionTitle = '[[' + pages[0] + ']]'; pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE; } else { sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', '); pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n'); } sectionForLink = extractDisplayedText(sectionTitle); editParams = { section: 'new', sectiontitle: sectionTitle, text: pageLines + '\n' + text + ' ~~' + '~~', summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос') }; } var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false }); if (isZka && !fast) { editPageContent(targetPage, { summary: editParams.summary, assertuser: mwCfg.wgUserName, readError: 'Не удалось получить содержимое страницы «' + targetPage + '».' }, function (pageText) { return { text: insertTopDiscussionSection(pageText, editParams.text) }; }, function (err) { if (err) { logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId }); markSubmitError(); $('#rmReportFast').prop('disabled', false); return; } logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false }); appendNominationLink(targetPage, sectionForLink); if (sectionForLink) subscribeToTopic(targetPage, sectionForLink); renderModalFooter('reload'); }); return; } apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) { if (resp && resp.error) { logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId }); markSubmitError(); if (isZka) $('#rmReportFast').prop('disabled', false); return; } logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false }); appendNominationLink(targetPage, sectionForLink); if (sectionForLink) subscribeToTopic(targetPage, sectionForLink); renderModalFooter('reload'); }); } // ═══════════════════════════════════════════════════════════════════════════ // ДИСПЕТЧЕР // ═══════════════════════════════════════════════════════════════════════════ function handleMenuClick(item, event) { isError = false; var op = OPERATIONS_MAP[item.id]; // Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона if (item.id === 'cat-merge' && event && event.ctrlKey) { showQuickMergeModal(); return; } if (!op) { console.error('RemoverCore: неизвестная операция', item.id); return; } var handlerFn = handlers[op.handler]; if (typeof handlerFn !== 'function') { console.error('RemoverCore: обработчик не найден', op.handler); return; } handlerFn(op, event); } // ─── Экспорт ───────────────────────────────────────────────────────────── window.RemoverCore = { handleMenuClick: handleMenuClick }; }()); qgg9s5r1m76is93ol93qjdr1qt4deh6 745212 745203 2026-06-04T22:21:51Z Solidest 54422 745212 javascript text/javascript /** * Remover — ядро (core). * Загружается лениво при первом клике по пункту меню. * Ожидает, что window.RemoverState уже задан remover-loader.js. * * Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты). * Логика выполнения сосредоточена в универсальных обработчиках. * Экспортирует: window.RemoverCore.handleMenuClick(item, event) */ (function () { 'use strict'; var state = window.RemoverState; if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; } var mwCfg = state.mwCfg; var cfg = applyCoreConfigDefaults(state.cfg || {}); var isCategory = state.isCategory; var isVector22 = state.isVector22; var scriptLink = cfg.scriptLink; var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings'; var settingsVersion = 1; var settingsMenuMeta = collectSettingsMenuMeta(); var settingsArticleItemLabels = settingsMenuMeta.articleLabels; var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels; var settingsItemLabelById = settingsMenuMeta.idToLabel; var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm; var settingsItemLabelOrder = settingsMenuMeta.labelOrder; var settingsDefaults = getDefaultSettings(); var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__'; var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__'; var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__'; var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {})); var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor; var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic; var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string') ? state.signatureSeparator.trim() : initialSettings.signatureSeparator; initialSettings.notifyAuthor = setAlert; initialSettings.subscribeTopic = setSubscribe; initialSettings.signatureSeparator = signatureSeparator; state.cfg = cfg; state.settings = clonePlainObject(initialSettings); state.setAlert = setAlert; state.setSubscribe = setSubscribe; // ─── Константы ────────────────────────────────────────────────────────── var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь']; var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря']; var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); }); var T_OPEN = '{' + '{'; var T_CLOSE = '}' + '}'; var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления'; var CATEGORY_NOMINATION_META = { discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true }, deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true }, rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true }, merge: { title: 'Номинация: к объединению', actionText: 'к объединению' } }; var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g; var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i; var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i; var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i; var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i; var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds'; var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i'); var KUL_PATTERN_STR = 'к\\s*улучшению'; var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i'); var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on'; var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i'); var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/; var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/; var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/; var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/; var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/; var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i; var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i; // ─── Глобальные переменные сессии ──────────────────────────────────────── var isError = false; var logStatusSeq = 0; var resizeObservers = []; var modalLayoutSyncHandlers = []; var tplAliasCache = {}; // ─── Стили ─────────────────────────────────────────────────────────────── var stStyles = cfg.modalStyles; var tk = { cBase: 'var(--color-base, #202122)', cSub: 'var(--color-subtle, #72777d)', cSubM: 'var(--color-subtle, #54595d)', cInv: 'var(--color-inverted-fixed, #fff)', cProg: 'var(--color-progressive, #3366cc)', cProgH: 'var(--color-progressive--hover, #2a4b8d)', cDang: 'var(--color-destructive, #d73333)', cDis: 'var(--color-disabled, var(--color-subtle, #72777d))', bgBase: 'var(--background-color-base, #fff)', bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)', bgN: 'var(--background-color-neutral, #eaecf0)', bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))', bgProg: 'var(--background-color-progressive, #3366cc)', bgProgH:'var(--background-color-progressive--hover, #2a4d8f)', bgSucc: 'var(--background-color-success, #14866d)', bgSuccH:'var(--background-color-success--hover, #0f6d57)', bSub: 'var(--border-color-subtle, #a2a9b1)', bSubS: 'var(--border-color-subtle, #ddd)', bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))', bProg: 'var(--border-color-progressive, #3366cc)', bProgH: 'var(--border-color-progressive--hover, #2a4d8f)', bSucc: 'var(--border-color-success, #14866d)', bSuccH: 'var(--border-color-success--hover, #0f6d57)' }; var sz = { taH: '180px', taMinH: '100px', taMinW: '180px', mobileBp: 720, modalRatio: 0.4, modalMinWide: 420, modalDefaultWide: 720, viewportGap: 24, touchDesktopGap: 120 }; var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;'; var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;'; var stCancel = neutralVis + btnBase; var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase; var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase; var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;'; var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;'; var stRow = 'display:flex;margin-bottom:6px;'; var inlineControlGap = 4; var squareControlSize = 32; var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px'; var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;'; var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;'; var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;'; var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;'; var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;'; var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;'; var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;'; var multiNominationGap = '6px'; var RESIZE_CLASS = 'rm-resizable'; // ═══════════════════════════════════════════════════════════════════════════ // РЕЕСТР ОПЕРАЦИЙ // Каждая запись описывает одну кнопку меню. Поля: // id — идентификатор (совпадает с item.id из loader) // handler — имя метода-обработчика в объекте handlers // handlerArg — аргумент, передаваемый в handler (опционально) // ═══════════════════════════════════════════════════════════════════════════ var OPERATIONS = [ // ── Статьи ────────────────────────────────────────────────────────── { id: 'fRm', label: 'КБУ', handler: 'showKbu', // Параметры номинации: заполняются при submit nomination: { pageTitle: function (pg) { return normTitle(pg); }, // шаблон встраивается в статью, номинационная страница отсутствует inArticle: true } }, { id: 'tRm', label: 'КУ', handler: 'showNomination', nomination: { comment: 'к удалению', template: 'к удалению', navTemplate: 'КУ', nomPage: function (date) { return 'Википедия:К удалению/' + date; }, supportsMulti: true, multiOpId: 'mRm', supportsTransfer: true, // шаблон встраивается в статью через <noinclude> inArticle: true, articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); } } }, { id: 'rnm', label: 'КПМ', handler: 'showNomination', nomination: { comment: 'к переименованию', template: 'к переименованию', navTemplate: 'КПМ', nomPage: function (date) { return 'Википедия:К переименованию/' + date; }, supportsMulti: true, multiOpId: 'mRnm', inArticle: true, articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'rename', firstId: 'rmRenameFirst', inputClass: 'rmRenameInput', firstPh: 'Новое название', addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант', containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' } } }, { id: 'imp', label: 'КУЛ', handler: 'showNomination', nomination: { comment: 'к срочному улучшению', template: 'к улучшению', navTemplate: 'КУЛ', nomPage: function (date) { return 'Википедия:К улучшению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); } } }, { id: 'merge', label: 'КОБ', handler: 'showNomination', nomination: { comment: 'к объединению с другой', template: 'к объединению', navTemplate: 'КОБ', nomPage: function (date) { return 'Википедия:К объединению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'merge', firstId: 'rmMergeFirst', inputClass: 'rmMergeInput', firstPh: 'Объединить с…', addBtnId: 'rmAddMerge', addBtnLabel: '+', containerId: 'rmMergeContainer', addPh: 'Дополнительная статья', maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.' } } }, { id: 'split', label: 'КРАЗД', handler: 'showNomination', nomination: { comment: 'к разделению', template: 'к разделению', navTemplate: 'КР', nomPage: function (date) { return 'Википедия:К разделению/' + date; }, inArticle: true, articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); }, extraInput: { type: 'split', firstId: 'rmSplitFirst', inputClass: 'rmSplitInput', firstPh: 'Разделить на…', addBtnId: 'rmAddSplit', addBtnLabel: '+', containerId: 'rmSplitContainer', addPh: 'Дополнительная статья' } } }, { id: 'recov', label: 'ВУС', handler: 'showNomination', nomination: { comment: '', template: 'к восстановлению', navTemplate: 'ВУС', nomPage: function (date) { return 'Википедия:К восстановлению/' + date; }, inArticle: false // шаблон не ставится в (удалённую) статью } }, { id: 'close', label: 'Снятие', handler: 'showArticleClose' }, // ── Запросы ───────────────────────────────────────────────────────── { id: 'protect', label: 'Защита', handler: 'showReport', reportMode: 'protect' }, { id: 'request', label: 'Запрос', handler: 'showReport', reportMode: 'request' }, // ── Категории ──────────────────────────────────────────────────────── { id: 'cat-fRm', label: 'КБУ', handler: 'showKbu', forCategory: true }, { id: 'cat-discuss', label: 'Обсудить', handler: 'showCatNomination', catType: 'discuss' }, { id: 'cat-delete', label: 'Удалить', handler: 'showCatNomination', catType: 'deletion' }, { id: 'cat-rename', label: 'Переименовать', handler: 'showCatNomination', catType: 'rename' }, { id: 'cat-merge', label: 'Объединить', handler: 'showCatNomination', catType: 'merge' }, { id: 'cat-done', label: 'Снятие', handler: 'showCatClose' } ]; // Быстрый поиск по id var OPERATIONS_MAP = {}; OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; }); // ─── Тексты переноса (КБУ → КУ) ───────────────────────────────────────── var transferTexts = { kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' }, kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' }, both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' } }; // ═══════════════════════════════════════════════════════════════════════════ // УТИЛИТЫ // ═══════════════════════════════════════════════════════════════════════════ function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); } function escapeHtml(s) { return String(s || '') .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function getTalkPage(pageName) { var match = /([^:]*:)?(.*)/.exec(pageName); if (match[1]) { var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')]; if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2]; } return 'Обсуждение:' + pageName; } function normTitle(s) { return (s || '').replace(/_/g, ' '); } function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); } function getCategoryNamespaceLabel() { return mwCfg.wgFormattedNamespaces[14] || 'Категория'; } function normalizeCategoryPageName(value) { var title = normTitle(value).trim(); var nsMatch, nsKey, ns; if (!title) return ''; nsMatch = title.match(/^([^:]+):(.+)$/); if (nsMatch) { nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_'); ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey]; if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim(); return getCategoryNamespaceLabel() + ':' + title; } return getCategoryNamespaceLabel() + ':' + title; } function makeSummary(text) { return scriptLink + ': ' + text; } function appendNominationSignature(text) { var body = String(text || ''); return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~'; } function extractDisplayedText(s) { return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1'); } function collectInputValues(selector) { return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean); } function applyCoreConfigDefaults(config) { var defaults = { scriptLink: '[[Участник:Solidest/Remover|Remover]]', fastRemoveReasons: { general: [ ['уд-бессвязно', 'О1 Бессвязный текст'], ['уд-тест', 'О2 Тестовая страница'], ['уд-ванд', 'О3.1 Вандальная страница'], ['уд-мист', 'О3.2 Мистификация'], ['уд-повторно', 'О4 Уже удалялось'], ['уд-автор', 'О5 По просьбе автора'], ['уд-под', 'О6 Ненужная подстраница'], ['уд-переим', 'О7 Для переименования'], ['уд-дубль', 'О8 Дубликат'], ['уд-реклама', 'О9 Реклама или спам'], ['уд-нецелевая', 'О10 Нецелевая СО'], ['уд-копивио', 'О11 Нарушение АП'] ], articles: [ ['подст:ds', 'ds Отсроченное пусто или коротко', 'С'], ['уд-пусто', 'С1 Пусто или коротко'], ['уд-иностр', 'С2 Не на русском'], ['уд-ссылки', 'С3 Лишь ссылки'], ['уд-нз', 'С5 Явно незначимо'], ['уд-бям', 'С7 Создано нейросетью'] ], redirects: [ ['уд-в никуда', 'П1 Перенапр. в никуда'], ['уд-мпр', 'П2 Межпростр. перенапр.'], ['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'], ['уд-падеж', 'П4 Не именительный падеж'], ['уд-смысл', 'П5 Неверное перенапр.'], ['уд-перобс', 'П6 Перенапр. на СО'] ], files: [ ['db-duplicate', 'Ф1 Копия файла'], ['db-badimage', 'Ф2 Повреждённый файл'], ['подст:nld', 'Ф3 Нет данных о лицензии'], ['подст:nsd', 'Ф3 Нет данных о источнике'], ['подст:nad', 'Ф3 Нет данных о авторе'], ['подст:dd', 'Ф3 Сомнительные данные файла'], ['подст:npd', 'Ф3 Не подтверждена лицензия'], ['подст:ofud', 'Ф4 Неиспользуемый КДИ'], ['подст:dfud', 'Ф5 Нет КДИ'], ['db-badfairuse', 'Ф6 Неоправданное КДИ'], ['подст:rfu', 'Ф7 Заменяемый КДИ'], ['NCT', 'Ф8 Есть на Складе'], ['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ'] ], categories: [ ['уд-пусткат', 'К1.1 Пустая категория'], ['уд-служебная', 'К1.2 Разобранная служебная кат.'], ['уд-перекат', 'К2 Переименованная кат.'] ], users: [ ['уд-владелец', 'У1 По желанию владельца'], ['уд-анон', 'У2 Устаревшая СО анонима'], ['уд-несущ', 'У3 Несуществующий участник'], ['уд-нецелевое', 'У4 Нецелевое использ. ЛП'], ['уд-неактив', 'У5 Подстраница неактивного'] ], special: [ ['db', 'Особый случай'] ] }, fastRemoveCriteriaAnchors: { 'подст:ds': 'С1', deleteslow: 'С1', ds: 'С1' }, requiredParamTemplates: { 'уд-переим': 'страницу, которую нужно переименовать', 'уд-дубль': 'страницу-дубликат', 'уд-копивио': 'URL источника нарушения АП', 'db-duplicate': 'имя файла-оригинала', 'подст:rfu': 'имя заменяемого файла', 'NCT': 'имя файла на Викискладе', 'уд-перекат': 'новое название категории', 'db': '!причину удаления' }, categoryTemplates: { discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению', rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat', merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm', discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось' }, modalStyles: { border: '1px solid var(--border-color-progressive, #3366bb)', background: 'var(--background-color-base, #f8f9fa)', borderRadius: '6px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', headerColor: 'var(--color-progressive, #3366bb)' } }; config.scriptLink = config.scriptLink || defaults.scriptLink; config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {}); config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {}); config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {}); config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {}); config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {}); return config; } function clonePlainObject(obj) { return JSON.parse(JSON.stringify(obj || {})); } function normalizeMenuLabel(value) { return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); } function readSettingsOptionState(fallback) { var base = clonePlainObject(fallback || {}); var stored; var raw = null; if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base; stored = mw.user.options.get(settingsOptionName); if (typeof stored === 'string' && stored.trim()) { try { raw = JSON.parse(stored); } catch (e) { console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e); } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base; return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {}); } function normalizeQuickPhraseValue(value) { return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim(); } function normalizeQuickPhrasesList(list, fallback) { var source = Array.isArray(list) ? list : fallback; var normalized = []; (source || []).forEach(function (value) { var phrase = normalizeQuickPhraseValue(value); if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase); }); return normalized; } function collectSettingsMenuMeta() { var labels = []; var articleLabels = []; var categoryLabels = []; var idToLabel = {}; var labelByNorm = {}; var labelOrder = {}; function collect(items, targetLabels) { items.forEach(function (item) { var id; var label; var normLabel; if (!item || item.type === 'separator') return; id = String(item.id || '').trim(); label = String(item.label || '').trim(); normLabel = normalizeMenuLabel(label); if (id && label && !(id in idToLabel)) idToLabel[id] = label; if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label); if (normLabel && !(normLabel in labelByNorm)) { labelByNorm[normLabel] = label; labelOrder[label] = labels.length; labels.push(label); } }); } collect(cfg.articleMenuItems, articleLabels); collect(cfg.categoryMenuItems, categoryLabels); return { labels: labels, articleLabels: articleLabels, categoryLabels: categoryLabels, idToLabel: idToLabel, labelByNorm: labelByNorm, labelOrder: labelOrder }; } function buildSettingsMenuItemsHint() { var parts = []; if (settingsArticleItemLabels.length) { parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>'); } if (settingsCategoryItemLabels.length) { parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>'); } return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : ''; } function getDefaultSettings() { return { version: settingsVersion, excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []), notifyAuthor: !!cfg.defaultNotifyAuthor, subscribeTopic: !!cfg.defaultSubscribeTopic, menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover', disabledItems: [], quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []), showMenuIcons: !!cfg.showMenuIcons, signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : '' }; } function normalizeNumberList(list, fallback) { var source = Array.isArray(list) ? list : fallback; return source .map(function (value) { return parseInt(value, 10); }) .filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; }) .sort(function (a, b) { return a - b; }); } function normalizeDisabledItemValue(value) { var token = String(value || '').trim(); if (!token) return null; if (settingsItemLabelById[token]) return settingsItemLabelById[token]; return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null; } function compareSettingsMenuLabels(a, b) { var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER; var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER; if (ai !== bi) return ai - bi; return a.localeCompare(b, 'ru'); } function normalizeDisabledItemsList(list, fallback) { var source = Array.isArray(list) ? list : fallback; return source .map(normalizeDisabledItemValue) .filter(function (value, index, arr) { return value && arr.indexOf(value) === index; }) .sort(compareSettingsMenuLabels); } function normalizeMenuTitleSetting(value, fallback) { var menuTitle = String(value || '').trim(); if (!menuTitle) return fallback; if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS; if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE; if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS; return menuTitle; } function normalizeRemoverSettings(raw) { var defaults = clonePlainObject(settingsDefaults); var source = (raw && typeof raw === 'object') ? raw : {}; return { version: settingsVersion, excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []), notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor, subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic, menuTitle: normalizeMenuTitleSetting( (typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '', typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover' ), disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []), quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []), showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons, signatureSeparator: (typeof source.signatureSeparator === 'string') ? source.signatureSeparator.trim() : (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '') }; } function areRemoverSettingsEqual(a, b) { return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b)); } function updateStoredSettingsState(settings, skipUserOptionsSync) { var normalized = normalizeRemoverSettings(settings); state.settings = clonePlainObject(normalized); setAlert = normalized.notifyAuthor; setSubscribe = normalized.subscribeTopic; signatureSeparator = normalized.signatureSeparator; state.setAlert = setAlert; state.setSubscribe = setSubscribe; if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') { mw.user.options.set(settingsOptionName, JSON.stringify(normalized)); } return normalized; } function splitSettingsInput(value) { return String(value || '') .split(/[\s,;]+/) .map(function (part) { return part.trim(); }) .filter(Boolean); } function splitSettingsListInput(value) { return String(value || '') .split(/[\n,;]+/) .map(function (part) { return part.trim(); }) .filter(Boolean); } function parseNamespaceInput(value) { var tokens = splitSettingsInput(value); var invalid = []; var values = []; tokens.forEach(function (token) { var parsed = parseInt(token, 10); if (String(parsed) !== token) invalid.push(token); else if (values.indexOf(parsed) === -1) values.push(parsed); }); values.sort(function (a, b) { return a - b; }); return { values: values, invalid: invalid }; } function parseDisabledItemsInput(value) { var tokens = splitSettingsListInput(value); var invalid = []; var values = []; tokens.forEach(function (token) { var normalized = normalizeDisabledItemValue(token); if (!normalized) invalid.push(token); else if (values.indexOf(normalized) === -1) values.push(normalized); }); values.sort(compareSettingsMenuLabels); return { values: values, invalid: invalid }; } function formatItemsWithAnd(items) { var list = (items || []).filter(Boolean); if (!list.length) return ''; if (list.length === 1) return list[0]; return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1]; } function formatPagesWithAnd(names, prefix) { var p = prefix || ':'; var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; }); return formatItemsWithAnd(links); } function asNonEmptyArray(value) { return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean); } function getCategoryNominationMeta(type) { return CATEGORY_NOMINATION_META[type] || CATEGORY_NOMINATION_META.discuss; } function buildRenameTemplateParam(targetNames) { var list = asNonEmptyArray(targetNames); if (!list.length) return ''; return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : ''); } function collectRenameTargetsFromTemplateParams(params) { return (params || []).map(function (value) { return String(value || '').trim(); }).filter(Boolean); } function formatRenameItemLabel(pageName, targetName) { var targets = asNonEmptyArray(targetName); return '[[:' + pageName + ']]' + (targets.length ? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ') : ''); } function buildRenameItemLabelFormatter(targetsByPage) { var targets = targetsByPage || {}; return function (pageName) { return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || ''); }; } function formatRenameItemsWithAnd(pages, targetsByPage) { var formatItem = buildRenameItemLabelFormatter(targetsByPage); var links = (pages || []).map(function (pageName) { return formatItem(pageName); }); return formatItemsWithAnd(links); } function normalizeCategoryTargetName(value) { return normTitle(stripCatPrefix(value)).trim(); } function normalizeCategoryTargetPageName(value) { var title = normalizeCategoryTargetName(value); return title ? normalizeCategoryPageName(title) : ''; } function buildMultiRenameTargetMap(pairs, key) { var map = {}; (pairs || []).forEach(function (pair) { var value; if (!pair || !pair.pageName) return; value = pair[key || 'targetName']; map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || ''); }); return map; } function getMultiRenameTarget(job, pageName, key) { var map = job && job[key || 'multiRenameTargets']; return map ? (map[normTitle(pageName)] || '') : ''; } function collectMultiRenamePairs(options) { var opts = options || {}; var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle; var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle; var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget; var pairs = []; $('.rmMultiPageBlock').each(function () { var $block = $(this); var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim(); var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) { return { targetName: normalizeTarget(targetRaw), templateTargetName: normalizeTemplateTarget(targetRaw) }; }).filter(function (item) { return item.targetName || item.templateTargetName; }); var pageName = normalizePage(pageRaw); var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean); var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean); if (!pageName && !targetNames.length && !templateTargetNames.length) return; pairs.push({ pageName: pageName, targetName: targetNames[0] || '', templateTargetName: templateTargetNames[0] || '', targetNames: targetNames, templateTargetNames: templateTargetNames }); }); return pairs; } function validateMultiRenamePairs(pairs, pageLabel, targetLabel) { var seen = {}; var pages = pairs || []; var pageWord = pageLabel || 'страницу'; var targetWord = targetLabel || 'новое название'; if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; } for (var i = 0; i < pages.length; i++) { var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName); var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName); if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; } if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; } if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; } if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; } seen[normTitle(pages[i].pageName)] = true; } return true; } function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) { return $.extend({}, extraOptions || {}, { formatItemLabel: buildRenameItemLabelFormatter(targetsByPage) }); } function formatCatLink(name) { return '[[:Категория:' + name + ']]'; } function formatMergeStatus(status) { return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status; } function applyGeneratedText($el, generated) { var cur = $el.val(); var prev = $el.data('rmGenerated') || ''; if (!prev || cur.indexOf(prev) === 0) { $el.val(generated + cur.slice(prev.length)); } else { $el.val(generated + (cur ? '\n' + cur : '')); } $el.data('rmGenerated', generated); } function getCurrentQuickPhrases() { return normalizeQuickPhrasesList( state.settings && state.settings.quickPhrases, settingsDefaults.quickPhrases || [] ); } function insertTextIntoTextarea($el, text) { var el = $el && $el[0]; var value; var start; var end; var updatedValue; var caretPos; if (!el) return; text = String(text || ''); if (!text) return; value = $el.val() || ''; start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length; end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start; updatedValue = value.slice(0, start) + text + value.slice(end); caretPos = start + text.length; $el.val(updatedValue).trigger('input').trigger('change').focus(); if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos); } function buildQuickPhrasesPanelHtml(textareaId) { var phrases = getCurrentQuickPhrases(); if (!phrases.length) return ''; return joinHtml([ '<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">', phrases.map(function (phrase) { return joinHtml([ '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId, '" data-rm-phrase="', escapeHtml(phrase), '">', escapeHtml(phrase), '</button>' ]); }).join(''), '</div>' ]); } function getMultiNominationCommentText(commentsByPage, pageTitle) { var key = normTitle(pageTitle); if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return ''; return normalizeQuickPhraseValue(commentsByPage[key]); } function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) { var list = Array.isArray(pages) ? pages.filter(Boolean) : []; return list.length > 0 && list.every(function (pageName) { return !!getMultiNominationCommentText(commentsByPage, pageName); }); } function hasMultiNominationText(pages, bodyText, commentsByPage) { return !!normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage); } function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) { if (hasMultiNominationText(pages, bodyText, commentsByPage)) return true; alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.'); return false; } function buildMultiNominationText(pages, bodyText, commentsByPage, options) { var opts = options || {}; var list = Array.isArray(pages) ? pages.filter(Boolean) : []; var body = normalizeQuickPhraseValue(bodyText); var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage); var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3); var headingMarks = new Array(headingLevel + 1).join('='); var formatItemLabel = typeof opts.formatItemLabel === 'function' ? opts.formatItemLabel : function (pageName) { return '[[:' + pageName + ']]'; }; var pageSections = list.map(function (pageName, index) { var comment = getMultiNominationCommentText(commentsByPage, pageName); var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n'; return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : ''); }).join(''); var commonSectionText = body ? appendNominationSignature(body) : (hasAllPageComments ? '' : appendNominationSignature('')); return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : ''); } function buildMultiNominationListText(pages, bodyText, commentsByPage, options) { var opts = options || {}; var list = Array.isArray(pages) ? pages.filter(Boolean) : []; var body = normalizeQuickPhraseValue(bodyText); var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage); var formatItemLabel = typeof opts.formatItemLabel === 'function' ? opts.formatItemLabel : function (pageName) { return '[[:' + pageName + ']]'; }; var pageLines = list.map(function (pageName) { var comment = getMultiNominationCommentText(commentsByPage, pageName); return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : ''); }).join('\n'); var commonText = body ? appendNominationSignature(body) : (hasAllPageComments ? '' : appendNominationSignature('')); return pageLines + (pageLines && commonText ? '\n' : '') + commonText; } function collectMultiNominationComments(normalizePageName) { var comments = {}; var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle; $('.rmMultiPageBlock').each(function () { var $block = $(this); var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim()); var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val()); if (!pageName) return; comments[pageName] = comment; }); return comments; } function getNominationPublishText(job) { if (job && job.isMulti) return String(job.msg || ''); return appendNominationSignature(job && job.msg); } function getNominationConflictRule(job) { if (!job || job.mode !== 'nominate') return null; if (job.opId === 'tRm' || job.opId === 'mRm') { return { id: 'ku', label: 'КУ', namePattern: '(?:к\\s*удалению|ку)', detect: function (articleText) { var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i); if (!match) return null; var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim()); return { label: 'КУ', templateName: templateName || 'КУ', templateDisplay: '{{' + (templateName || 'КУ') + '}}' }; } }; } if (job.opId === 'rnm' || job.opId === 'mRnm') { return { id: 'kpm', label: 'КПМ', namePattern: '(?:к\\s*переименованию|кпм|rename)', detect: function (articleText) { var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i); if (!match) return null; var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim()); return { label: 'КПМ', templateName: templateName || 'КПМ', templateDisplay: '{{' + (templateName || 'КПМ') + '}}' }; } }; } return null; } function detectNominationConflict(articleText, job) { var rule = getNominationConflictRule(job); if (!rule || typeof rule.detect !== 'function') return null; return rule.detect(articleText); } function getConflictDecisionForPage(job, pageName) { var decisions = job && job.conflictDecisions; var key = normTitle(pageName); return decisions && decisions[key] ? decisions[key] : null; } function getCategoryMergeRe() { return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i'); } function eachSequential(targets, iteratee, done) { var i = 0; (function next(err) { if (err || i >= targets.length) { done(err || null); return; } iteratee(targets[i++], next); }(null)); } function normalizeSectionForLink(sectionTitle) { return (sectionTitle || '').trim() .replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) { var v = (label || target || '').trim(); return v.charAt(0) === ':' ? v.slice(1) : v; }) .replace(/''+/g, '').replace(/\s+/g, ' ').trim(); } function getViewportWidth() { return Math.floor(Math.max( (document.documentElement && document.documentElement.clientWidth) || 0, (typeof window.innerWidth === 'number' && window.innerWidth) || 0, $(window).width() || 0 )); } function getVisualViewportWidth() { var widths = []; if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width); if (window.screen && window.screen.width > 0) widths.push(window.screen.width); return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth(); } function isTouchModalDevice() { return !!( (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0) ); } function getModalLayout() { var minWidth = parseInt(sz.taMinW, 10) || 180; var layoutWidth = getViewportWidth(); var visualWidth = getVisualViewportWidth(); var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth)); var isMobile = layoutWidth <= sz.mobileBp; var isTouchDesktop = !isMobile && isTouchModalDevice() && visualWidth > 0 && visualWidth <= sz.mobileBp && layoutWidth >= visualWidth + sz.touchDesktopGap; var useFullWidth = isMobile || isTouchDesktop; var maxOuterWidth; var defaultOuterWidth; var desktopWidth; if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap); else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32); else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth))); desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio)); if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth; else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide; else defaultOuterWidth = desktopWidth; return { minWidth: minWidth, isMobile: isMobile, isTouchDesktop: isTouchDesktop, useFullWidth: useFullWidth, shouldCenter: useFullWidth || mwCfg.skin === 'minerva', maxOuterWidth: maxOuterWidth, defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth) }; } function getDefaultResizableWidth(frameWidth) { var layout = getModalLayout(); return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0)); } function getBoxFrameWidth($el) { function px(prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width'); } // ═══════════════════════════════════════════════════════════════════════════ // API // ═══════════════════════════════════════════════════════════════════════════ function getApiUrl() { return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php'; } function getCsrfTokenValue() { return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function') ? mw.user.tokens.get('csrfToken') : null; } function storeCsrfToken(token) { if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return; mw.user.tokens.set({ csrfToken: token }); } function isValidCsrfToken(token) { return typeof token === 'string' && !!token && token !== '+\\'; } function fetchCsrfToken(forceRefresh, callback) { var cachedToken = getCsrfTokenValue(); if (!forceRefresh && isValidCsrfToken(cachedToken)) { callback(cachedToken); return; } $.ajax({ url: getApiUrl(), method: 'GET', dataType: 'json', data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' } }) .done(function (data) { var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken; if (isValidCsrfToken(token)) { storeCsrfToken(token); callback(token); return; } callback(null); }) .fail(function () { callback(null); }); } function apiReq(params, mode, callback) { var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options'; function sendRequest(retryWithFreshToken) { var reqParams = $.extend({}, params, { format: 'json', action: mode }); if (!isWrite) { $.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' }) .done(function (data) { if (callback) callback(data); }) .fail(function (jqXHR, status) { console.error('Ошибка API: ' + status); if (callback) callback({ error: { code: 'network', info: status } }); }); return; } fetchCsrfToken(!!retryWithFreshToken, function (token) { if (!isValidCsrfToken(token)) { if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } }); return; } reqParams.token = token; $.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' }) .done(function (data) { var err = data && data.error; var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || ''))); if (isBadToken && !retryWithFreshToken) { sendRequest(true); return; } if (callback) callback(data); }) .fail(function (jqXHR, status) { console.error('Ошибка API: ' + status); if (callback) callback({ error: { code: 'network', info: status } }); }); }); } sendRequest(false); } function saveSettingsToServer(settings, callback) { var normalized = normalizeRemoverSettings(settings); if (!mwCfg.wgUserName) { callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' }); return; } apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) { if (resp && resp.options === 'success') { callback(null, updateStoredSettingsState(normalized)); return; } callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' }); }); } function resetSettingsOnServer(callback) { if (!mwCfg.wgUserName) { callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' }); return; } apiReq({ change: settingsOptionName }, 'options', function (resp) { if (resp && resp.options === 'success') { callback(null, updateStoredSettingsState(settingsDefaults, true)); return; } callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' }); }); } function getFirstQueryPage(data) { var pages = data && data.query && data.query.pages; if (!pages) return null; return pages[Object.keys(pages)[0]] || null; } function extractRevisionContent(rev) { if (!rev) return null; if (typeof rev['*'] === 'string') return rev['*']; if (rev.slots && rev.slots.main) { if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*']; if (typeof rev.slots.main.content === 'string') return rev.slots.main.content; } return null; } function makeReadError(apiError, fallbackCode, fallbackInfo) { var err = apiError || {}; return { code: err.code || fallbackCode || 'read_failed', info: err.info || fallbackInfo || 'Не удалось получить содержимое.' }; } function getTextWithTimestamp(pageName, callback) { apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) { var content; var page; if (data && data.error) { callback(null, null, data.error); return; } if (!data || !data.query || !data.query.pages) { callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' }); return; } page = getFirstQueryPage(data); if (!page) { callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' }); return; } if (page.invalid !== undefined) { callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' }); return; } if (page.missing !== undefined) { callback(null, null, null); return; } if (!page.revisions || !page.revisions.length) { callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' }); return; } content = extractRevisionContent(page.revisions[0]); if (content === null) { callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' }); return; } callback(content, page.revisions[0].timestamp || null, null); }); } function getText(pageName, callback) { getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); }); } function editPageContent(pageTitle, options, buildFn, callback) { var opts = options || {}; var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1); (function attempt(retry) { getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».')); return; } if (sourceText === null) { callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' }); return; } var done = (function () { var called = false; return function (result) { if (called) return; called = true; if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; } if (result.skip) { callback(null, result.meta || null); return; } if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' }; if (opts.watchlist) ep.watchlist = opts.watchlist; if (opts.assertuser) ep.assertuser = opts.assertuser; if (opts.createonly) ep.createonly = opts.createonly; if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { var err = resp && resp.error ? resp.error : null; if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; } callback(err, result.meta || null); }); }; }()); var maybe = buildFn(sourceText, done); if (maybe !== undefined) done(maybe); }); }(0)); } // ═══════════════════════════════════════════════════════════════════════════ // ШАБЛОНЫ: удаление и вставка // ═══════════════════════════════════════════════════════════════════════════ function findBalancedTemplateEnd(text, start) { var depth = 0; var i = start; var len = text.length; while (i < len - 1) { if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; continue; } if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; if (depth === 0) return i; continue; } i++; } return -1; } function splitTemplateTopLevelParts(innerText) { var parts = []; var start = 0; var templateDepth = 0; var linkDepth = 0; var i = 0; var text = String(innerText || ''); while (i < text.length) { if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { templateDepth++; i += 2; continue; } if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) { templateDepth--; i += 2; continue; } if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') { linkDepth++; i += 2; continue; } if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) { linkDepth--; i += 2; continue; } if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) { parts.push(text.slice(start, i)); start = i + 1; } i++; } parts.push(text.slice(start)); return parts; } function getTemplateMatchAt(text, start, nameRe) { var end = findBalancedTemplateEnd(text, start); var parts; var rawName; var name; if (end < 0) return null; parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2)); rawName = String(parts.shift() || '').trim(); name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); if (!nameRe.test(name)) return null; return { start: start, end: end, text: text.slice(start, end), name: rawName, params: parts.map(function (part) { return part.trim(); }) }; } function findTemplateByPattern(text, namePattern) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var i = 0; var end; var match; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) return match; end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } return null; } function hasTemplateWithDateByPattern(text, namePattern, dateIso) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var targetDate = convertToStandardDate(dateIso); var i = 0; var end; var match; var templateDate; if (!targetDate) return false; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) { templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, '')); if (templateDate === targetDate) return true; i = match.end; continue; } end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } return false; } function getTemplateRemovalRange(text, match) { var start = match.start; var end = match.end; var before = text.slice(0, start).match(/<noinclude>\s*$/i); var after; if (before) { after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i); if (after) { return { start: before.index, end: end + after[0].length }; } } after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/); if (after) end += after[0].length; return { start: start, end: end }; } function stripTemplatesByPattern(text, namePattern) { var source = String(text || ''); var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i'); var ranges = []; var out = []; var pos = 0; var i = 0; var end; var match; var range; while (i < source.length - 1) { if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') { match = getTemplateMatchAt(source, i, nameRe); if (match) { range = getTemplateRemovalRange(source, match); ranges.push(range); i = Math.max(match.end, range.end); continue; } end = findBalancedTemplateEnd(source, i); if (end > i) { i = end; continue; } } i++; } if (!ranges.length) return { text: source, removed: false }; ranges.forEach(function (r) { if (r.start < pos) r.start = pos; out.push(source.slice(pos, r.start)); pos = r.end; }); out.push(source.slice(pos)); return { text: out.join(''), removed: true }; } function removeTemplatesByAliases(text, aliases) { var seen = {}, patterns = []; aliases.forEach(function (alias) { var tpl = alias.replace(RE_TEMPLATE_NS, '').trim(); var key = tpl.toLowerCase(); if (!tpl || seen[key]) return; seen[key] = true; patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+')); }); if (!patterns.length) return { text: text, removed: false }; return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')'); } function removeTransferTemplatesLocal(articleText, transferMode) { var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false }; if (transferMode === 'none') return result; if (transferMode === 'kbu' || transferMode === 'both') { var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')'); result.text = kbu.text; result.removedKbu = kbu.removed; } if (transferMode === 'kul' || transferMode === 'both') { var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR); result.text = kul.text; result.removedKul = kul.removed; } var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR); result.text = hangon.text; result.removedHangon = hangon.removed; return result; } function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) { var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu; var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul; var needHangon = transferMode !== 'none' && !localResult.removedHangon; if (!needKbu && !needKul && !needHangon) { callback(localResult); return; } var titleMap = {}; if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; }); if (needKul) titleMap['Шаблон:К улучшению'] = true; if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; } apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) { var page = getFirstQueryPage(data); if (page && page.templates) { page.templates.forEach(function (tpl) { var norm = normalizeTemplateName(tpl.title); if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) || (needKul && RE_KUL_PATTERN.test(norm)) || (needHangon && RE_HANGON.test(norm))) { titleMap[tpl.title] = true; } }); } var titles = Object.keys(titleMap); if (!titles.length) { callback(localResult); return; } function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); } function collectAndApplyAliases() { var allAliases = []; titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); }); var dedup = {}, aliases = []; allAliases.forEach(function (alias) { var key = normalizeAliasKey(alias); if (!key || dedup[key]) return; dedup[key] = true; aliases.push(alias); }); var updated = $.extend({}, localResult); var r = removeTemplatesByAliases(updated.text, aliases); updated.text = r.text; if (r.removed) { if (needKbu) updated.removedKbu = true; if (needKul) updated.removedKul = true; if (needHangon) updated.removedHangon = true; } callback(updated); } var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; }); if (!missingTitles.length) { collectAndApplyAliases(); return; } (function resolveChunk(offset) { if (offset >= missingTitles.length) { collectAndApplyAliases(); return; } var chunk = missingTitles.slice(offset, offset + 20); var chunkByKey = {}; chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; }); apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) { var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {}; Object.keys(pages).forEach(function (pid) { var p = pages[pid]; if (!p || !p.title) return; var sourceTitle = chunkByKey[normalizeAliasKey(p.title)]; if (!sourceTitle) return; var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; })); var seen = {}, unique = []; found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); }); tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle]; }); chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; }); resolveChunk(offset + 20); }); }(0)); }); } // ─── Вставка шаблонов ──────────────────────────────────────────────────── function findInsertPositionAfterProjectTemplates(text) { var pos = 0, len = text.length; while (pos < len) { var wsMatch = text.slice(pos).match(/^[\t ]*\n/); if (wsMatch) { pos += wsMatch[0].length; continue; } if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break; var afterOpen = text.slice(pos + 2); var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/); var templateEnd; if (!nameMatch) break; var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim(); if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break; templateEnd = findBalancedTemplateEnd(text, pos); if (templateEnd < 0) break; pos = templateEnd; if (pos < len && text.charAt(pos) === '\n') pos++; } return pos; } function insertTplOnTalkPage(text, tplText, sep) { var s = (sep === undefined) ? '\n' : sep; var insertPos = findInsertPositionAfterProjectTemplates(text); if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : ''); var before = text.slice(0, insertPos).replace(/\n+$/, ''); var after = text.slice(insertPos).replace(/^\n+/, ''); return before + '\n' + tplText + (after.length ? s + after : ''); } function wrapInNoinclude(text, templateText) { var match = text.match(RE_NOINCLUDE); if (match) { // Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху var before = text.slice(0, text.indexOf(match[0])); if (/\S/.test(before)) { return '<noinclude>' + templateText + '</noinclude>\n' + text; } var content = match[2]; if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n'; return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>'); } return '<noinclude>' + templateText + '</noinclude>\n' + text; } function buildDateSectionTalkTemplateText(templateName, dateIso, sectionTitle, sectionIndex) { var normalizedSection = String(sectionTitle || '').trim(); var index = parseInt(sectionIndex, 10); var tpl = templateName + '|' + dateIso; if (isNaN(index) || index < 1) index = 1; if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection; return T_OPEN + tpl + T_CLOSE; } function upsertDateSectionTemplateOnTalkPage(text, templateName, aliases, dateIso, sectionTitle) { var source = text || ''; var normalizedSection = String(sectionTitle || '').trim(); var names = []; asNonEmptyArray(aliases).concat(templateName).forEach(function (name) { var normalized = String(name || '').trim(); if (normalized && names.indexOf(normalized) === -1) names.push(normalized); }); var namePattern = names.map(function (name) { return escapeRegExp(String(name).trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i'); var tplMatch = source.match(tplRe); function buildExistingTplWithCanonicalName(suffix) { return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE; } if (!tplMatch) { return { text: insertTplOnTalkPage(source, buildDateSectionTalkTemplateText(templateName, dateIso, normalizedSection, 1), '\n'), status: 'created' }; } var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean); var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); }); var stdDate = convertToStandardDate(dateIso); if (existingDates.indexOf(stdDate) !== -1) { var canonicalTpl = buildExistingTplWithCanonicalName(''); if (canonicalTpl !== tplMatch[0]) { return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' }; } return { text: source, status: 'already_present' }; } var nextIdx = existingDates.length + 1; var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : ''); return { text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)), status: 'updated' }; } function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) { return upsertDateSectionTemplateOnTalkPage(text, 'Оставлено', ['оставлено'], dateIso, sectionTitle); } function upsertRemovedFromDeletionTemplateOnTalkPage(text, dateIso, sectionTitle) { return upsertDateSectionTemplateOnTalkPage(text, 'Снято с удаления', ['Снято с удаления'], dateIso, sectionTitle); } function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) { var normalizedSection = String(sectionTitle || '').trim(); var normalizedReason = normalizeQuickPhraseValue(reasonText); var normalizedDeadline = String(deadlineText || '').trim(); var index = parseInt(sectionIndex, 10); var tpl = 'Условно оставлено|' + dateIso; if (isNaN(index) || index < 1) index = 1; if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection; if (normalizedReason) tpl += '|пояснение=' + normalizedReason; if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline; return T_OPEN + tpl + T_CLOSE; } function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) { var source = text || ''; var normalizedSection = String(sectionTitle || '').trim(); var normalizedReason = normalizeQuickPhraseValue(reasonText); var normalizedDeadline = String(deadlineText || '').trim(); var tplRe = /\{\{\s*(?:subst\s*:\s*)?(условно\s*оставлено)\s*([^}]*)\}\}/i; var tplMatch = source.match(tplRe); function buildExistingTplWithCanonicalName(suffix) { return T_OPEN + 'Условно оставлено' + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE; } if (!tplMatch) { return { text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'), status: 'created' }; } var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean); var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); }); var stdDate = convertToStandardDate(dateIso); if (existingDates.indexOf(stdDate) !== -1) { var canonicalTpl = buildExistingTplWithCanonicalName(''); if (canonicalTpl !== tplMatch[0]) { return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' }; } return { text: source, status: 'already_present' }; } var nextIdx = existingDates.length + 1; var suffix = '|' + dateIso; if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection; if (normalizedReason) suffix += '|пояснение=' + normalizedReason; if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline; return { text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)), status: 'updated' }; } // ═══════════════════════════════════════════════════════════════════════════ // ПАЙПЛАЙН НОМИНАЦИИ // ═══════════════════════════════════════════════════════════════════════════ function runNominationPipeline(steps) { var s = steps; var ctx = { templateMeta: null, nominationInfo: null }; var stages = [ { name: 'шаблон', fn: function (next) { s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); }); } }, { name: 'номинация', pendingText: 'Публикуется номинация...', successText: 'Номинация опубликована.', errorText: 'Публикация номинации.', fn: function (next) { s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); }); } }, { name: 'подписка', shouldRun: function () { var info = ctx.nominationInfo; return !!(setSubscribe && info && info.pageTitle && info.sectionTitle); }, fn: function (next) { subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); }); } }, { name: 'оповещение', shouldRun: function () { return !!(setAlert && !s.skipNotify); }, fn: function (next) { s.notifyStep(ctx.nominationInfo, next); } } ]; (function run(i) { if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; } var stage = stages[i]; if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; } var statusId = stage.pendingText ? logStatus(stage.pendingText, null, { pending: true, trackError: false }) : null; try { stage.fn(function (err) { if (err) { if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId }); if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx); else markSubmitError(); return; } if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false }); run(i + 1); }); } catch (ex) { var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) }; if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId }); if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx); else markSubmitError(); } }(0)); } // ─── Публикация номинации ──────────────────────────────────────────────── function publishNomination(opts, callback) { var cb = callback || function () {}; var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1); function doPublish() { apiReq({ title: opts.pageTitle, section: 'new', sectiontitle: opts.sectionTitle, summary: opts.summary, text: opts.text, assertuser: mwCfg.wgUserName }, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null); }); } if (opts.sectionTitle) { if (!opts.navTemplate) { doPublish(); return; } apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName }, 'edit', function (resp) { if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; } doPublish(); }); return; } // Вставка в существующую страницу if (opts.createText !== undefined) { (function attempt(retry) { getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) { var result; var ep; if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; } result = pageText === null ? (typeof opts.createText === 'function' ? opts.createText() : opts.createText) : (opts.buildText ? opts.buildText(pageText) : null); if (typeof result === 'string') result = { text: result }; if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } if (result.skip) { cb(null); return; } if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; } ep = { title: opts.pageTitle, text: result.text, summary: result.summary || opts.summary, assertuser: mwCfg.wgUserName }; if (pageText === null) ep.createonly = true; else if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { var err = resp && resp.error ? resp.error : null; if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) { attempt(retry + 1); return; } cb(err); }); }); }(0)); return; } editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' }, function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; }, function (err) { cb(err || null); } ); } // ─── Оповещение авторов ────────────────────────────────────────────────── function notifyAuthor(pg, options, callback) { var opts = options || {}; var cb = callback || function () {}; var actionText = (typeof opts.actionText === 'string') ? opts.actionText : ''; var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : ''; var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : ''); var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true; var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена'; var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : ''; apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) { var page = getFirstQueryPage(queryResp); if (!page) { cb({ code: 'network', info: 'Network error' }); return; } if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; } if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; } var rv = page.revisions[0]; if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; } apiReq({ title: 'User talk:' + rv.user, section: 'new', sectiontitle: 'Remover: [[:' + pg + ']]', summary: opts.summary || makeSummary('уведомление автора'), text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' + discussionText + '~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>', assertuser: mwCfg.wgUserName }, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); }); }); } function notifyAuthorsForPages(pages, notifyOptions, callback) { var cb = callback || function () {}; var opts = notifyOptions || {}; var list = []; (pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); }); if (!list.length) { cb(); return; } eachSequential(list, function (pg, next) { var pageLink = buildQuotedStatusPageLink(pg); var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false }); notifyAuthor(pg, opts, function (err) { logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err, { statusId: statusId, trackError: opts.trackError !== false }); next(); }); }, cb); } // ─── Подписка на раздел ────────────────────────────────────────────────── function subscribeToTopic(pageTitle, sectionTitle, callback) { var cb = callback || function () {}; if (!setSubscribe || !sectionTitle) { cb(); return; } var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false }); var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase(); function finish(err, st) { if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; } logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false }); cb(); } function trySubscribe(attemptsLeft) { apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) { var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null; if (!items || !items.length) { if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; } finish(null, 'not_found'); return; } var commentname = null; for (var ti = items.length - 1; ti >= 0; ti--) { var t = items[ti]; if (t.type === 'heading') { var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim(); if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) { commentname = t.name; break; } } } if (!commentname) { if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); else finish(null, 'not_found'); return; } apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) { finish(res && res.error ? res.error : null, 'subscribed'); }); }); } setTimeout(function () { trySubscribe(2); }, 1500); } // ═══════════════════════════════════════════════════════════════════════════ // UI: модальные окна // ═══════════════════════════════════════════════════════════════════════════ function syncModalLayout() { var syncFn = $('#removerModal').data('rmSyncLayout'); if (typeof syncFn === 'function') syncFn(); } function clearModalLayoutSyncHandlers() { modalLayoutSyncHandlers = []; $('#removerModal').removeData('rmSyncLayout'); } function registerModalLayoutSync(handler) { if (typeof handler !== 'function') return; if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler); $('#removerModal').data('rmSyncLayout', function () { modalLayoutSyncHandlers.slice().forEach(function (fn) { if (typeof fn === 'function') fn(); }); }); } function registerResizeObserver(observer) { if (observer) resizeObservers.push(observer); } function resetModalObservers() { resizeObservers.forEach(function (observer) { if (observer && typeof observer.disconnect === 'function') observer.disconnect(); }); resizeObservers = []; clearModalLayoutSyncHandlers(); $(window).off('resize.removerModal'); $(window).off('.rmTaResize'); } function closeModal() { resetModalObservers(); $(window).off('keydown.remover'); if (isVector22) $('#content').css('min-width', ''); $('#removerModal').remove(); } function ensureModalStyles() { if (document.getElementById('removerModalDynamicStyles')) return; var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;'; var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;'; var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;'; var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)'; var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)'; var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)'; var css = [ '#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}', '#removerModal{color:inherit}', '#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}', '#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}', '#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}', '#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}', '#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}', '#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}', '#removerModal button:not(:disabled):hover{filter:brightness(.97)}', '#removerModal button:not(:disabled):active{transform:translateY(1px)}', '#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}', '#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}', '#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}', '#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}', '#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}', '#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}', '#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}', '#removerModal .rmRenameVariantAddBtn{font-size:15px!important}', '#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}', '#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}', '#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}', '#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}', '#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}', '#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}', '#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}', '#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}', '#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}', '#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}', '#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}', '#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}', '#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}', '#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}', '#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}', '#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}', '#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}', '#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}', '#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}', '#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}', '#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}', '#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}', '#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}', '#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}', '#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}', '#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}', '#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}', '#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}', '#removerModal .rmActionItem:active{transform:translateY(1px)}', '#removerModal .rmActionMain{display:flex;align-items:center}', '#removerModal .rmActionMain input[type="radio"]{margin-right:8px}', '#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}', '#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}', '#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}', '#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}', '#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}', '#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}', '#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmConflictGroup{margin-top:10px}', '#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}', '#removerModal .rmConflictChoice{padding:5px 10px}', '#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}', '#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}', '#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}', '#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}', '#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}', '#removerModal .rmSettingsSectionHeader{margin:0 0 12px}', '#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}', '#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}', '#removerModal .rmSettingsField:last-child{margin-bottom:0}', '#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}', '#removerModal .rmSettingsFieldControl{display:block;min-width:0}', '#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}', '#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}', '#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}', '#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}', '#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}', '#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}', '#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}', '#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}', '#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}', '#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}', '#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}', '#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}', '#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}', '#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}', '#removerModal.rmModalSettings #rmSettingsForm{gap:18px}', '#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}', '#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}', '#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}', '#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}', '#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}', '#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}', '#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}', '#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}', '#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}', '#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}', '#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}', '#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}', '#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}', '#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}', '#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}', '#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}', '#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}', '#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}', '#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}', '#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}', '#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}', '#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}', '#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}', '#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}', '#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}', '#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}', '#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}', '#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}', '#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}', '#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}', '#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}', '#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}', '#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}', '#removerModal #rmTransferModeGroup{gap:0}', '#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}', '#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}', '#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}', '#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}', '#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}', '#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}', '#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}', '#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}', '#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}', '#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}', '#removerModal #rmProtectTextBlock{margin-top:14px}', '#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}', '#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}', '#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}', '#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}', '#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}', '#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}', '#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}', '#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}', '#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}', '#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}', '#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}', '#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}', '#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}', '#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}', '#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}', '#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}', '#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}', '#removerModal .rmQuickPhrasesPanel{margin-top:8px}', '#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}', '#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}', '@media (max-width:' + sz.mobileBp + 'px){', '#removerModal button{white-space:normal!important}', '#removerModal #rmFooterButtons{align-items:flex-start!important}', '#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}', '#removerModal .rmSettingsSection{padding:12px 13px}', '#removerModal .rmSettingsField{padding:10px}', '#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}', '#removerModal .rmTransferPanel{padding:0}', '#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}', '#removerModal .rmTransferHintRow{grid-column:auto}', '#removerModal .rmQuickPhraseChip{max-width:100%}', '}' ].join(''); var style = document.createElement('style'); style.id = 'removerModalDynamicStyles'; style.textContent = css; document.head.appendChild(style); } function applyV2022Layout($modal, explicitWidth) { if (!isVector22) return; var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' }; if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px'; $modal.css(css); } function getPageUrl(pageTitle) { return (mw.util && typeof mw.util.getUrl === 'function') ? mw.util.getUrl(pageTitle) : '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_')); } function getPageUrlWithFragment(pageTitle, fragment) { var url = getPageUrl(pageTitle); var frag = normalizeSectionForLink(fragment); if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_')); return url; } function buildStatusPageLink(pageName) { var title = normTitle(pageName); return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(title) + '</a>'; } function buildQuotedStatusPageLink(pageName) { return '«' + buildStatusPageLink(pageName) + '»'; } function buildHeaderIconButtonHtml(id, title, label, text) { return joinHtml([ '<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ', 'style="', stHeaderIconBtn, '">', text || '', '</button>' ]); } function createModal(opts) { if (typeof opts === 'string') opts = { title: opts }; var layout = getModalLayout(); resetModalObservers(); ensureModalStyles(); if (isVector22) $('#content').css('min-width', ''); $('#removerModal').remove(); var subtitleHtml = ''; var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;'; var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;'; if (opts.subtitleHtml) { subtitleHtml = joinHtml([ '<div id="removerModalSubtitle" style="', subtitleStyle, '">', opts.subtitleHtml, '</div>' ]); } else if (opts.subtitlePage) { subtitleHtml = joinHtml([ '<div id="removerModalSubtitle" style="', subtitleStyle, '">', opts.subtitleLabel || 'Текущий день', ': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">', normTitle(opts.subtitlePage), '</a></div>' ]); } var settingsButtonHtml = opts.showSettingsButton === false ? '' : buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙'); var display = opts.inline ? 'inline-block' : 'block'; var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0'); var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : ''; var modalStyle = joinHtml([ 'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';', 'border:', stStyles.border, ';background:', stStyles.background, ';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow, ';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle ]); var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';'; var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;'; $('#content').prepend(joinHtml([ '<div id="removerModal" style="', modalStyle, '">', '<div id="removerModalHeaderBar" style="', headerStyle, '">', '<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>', settingsButtonHtml, '</div>', subtitleHtml, '<div id="removerModalContent"></div>', '<div id="removerModalFooter" style="margin-top:15px;"></div>', '</div>' ])); var $modal = $('#removerModal'); if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); else applyV2022Layout($modal); $('#removerSettingsTrigger').off('click').on('click', function () { openSettings(); }); } function buildFooterCheckboxHtml(name, checked, label) { return joinHtml([ '<label style="', stFooterCheckLabel, '">', '<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ', checked ? 'checked' : '', '>', label, '</label>' ]); } function buildFooterActionsHtml(buttonsHtml) { return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>'; } function renderModalFooter(mode, options) { var opts = options || {}; $('#removerModalFooter').css('width', ''); if (mode === 'submit') { var showCb = opts.showCheckbox !== false; var showSub = opts.showSubscribe === true; var ns = mwCfg.wgNamespaceNumber; var notifyLabel = ns === 0 ? 'Оповестить создателя статьи' : (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона' : (ns === 14 || ns === 15) ? 'Оповестить создателя категории' : 'Оповестить создателя страницы'; var cbInlineHtml = ''; if (showSub || showCb) { cbInlineHtml = joinHtml([ '<div id="rmFooterCheckboxes" style="', stFooterChecks, '">', showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '', showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '', '</div>' ]); } $('#removerModalFooter').html(joinHtml([ '<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">', cbInlineHtml, buildFooterActionsHtml(joinHtml([ '<button id="removerCancel" style="', stCancel, '">Отмена</button>', '<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>' ])), '</div>' ])); $('#removerCancel').click(function () { closeModal(); }); $('#removerSubmit').data('rmSubmitInProgress', false).click(function () { if ($(this).data('rmSubmitInProgress')) return; $(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' }); isError = false; if (!opts.preserveLogOnSubmit) { $('#rmLogBox').empty(); logStatusSeq = 0; } if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; } if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; } $(this).data('rmSubmitInProgress', true).prop('disabled', true); var submitResult; try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; } if (submitResult === false) unlockModalSubmit(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click(); }); } else if (mode === 'reload') { var newBtns = buildFooterActionsHtml(joinHtml([ '<button id="removerCancel" style="', stCancel, '">Закрыть</button>', '<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>' ])); $('#rmFooterCheckboxes').remove(); var $btns = $('#rmFooterButtons'); if ($btns.length) { $btns.css({ 'justify-content': 'flex-end' }).html(newBtns); } else { $('#removerModalFooter').append(joinHtml([ '<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">', newBtns, '</div>' ])); } $('#removerCancel').click(function () { closeModal(); }); $('#removerReload').click(function () { location.reload(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.keyCode === 27) $('#removerCancel').click(); if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click(); }); } else { // 'close' $('#removerModalFooter').html(joinHtml([ '<div style="display:flex;justify-content:flex-end;align-items:center;">', '<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>', '</div>' ])); $('#removerCancel').click(function () { closeModal(); }); $(window).off('keydown.remover').on('keydown.remover', function (e) { if (e.keyCode === 27) $('#removerCancel').click(); }); } } function unlockModalSubmit() { $('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false); } function markSubmitError() { isError = true; var errColor = '#d73333'; $('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false) .addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' }); } // ─── UI: статус и ссылки ───────────────────────────────────────────────── function startProcessing() { if ($('#rmLogBox').length) return; $('#removerModal').append( '<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>' ); syncLinkWidths(); } function logStatus(message, error, opts) { var o = opts || {}; if (o.trackError !== false && error && error.code) isError = true; var $box = $('#rmLogBox'); if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); } var statusId = o.statusId || ('rm-status-' + (++logStatusSeq)); var $row = $box.find('[data-rm-status-id="' + statusId + '"]'); if (!$row.length) { $row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>'); $box.append($row); } var html; if (error) { var errText = error.code ? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>' : escapeHtml(String(error)); html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText; } else if (o.pending) { html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>'; } else { html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>'; } $row.html(html); return statusId; } function formatLogErrorCode(code) { var value = String(code || ''); return value.toLowerCase() === 'error' ? 'Ошибка' : value; } function logPageEdit(pageName, error, opts) { logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts); } function syncLinkWidths() { var $box = $('#rmLogBox'); if (!$box.length) return; var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0; $box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' }); } function appendNominationLink(pageTitle, sectionTitle) { if (!pageTitle) return; var url = getPageUrl(pageTitle); var frag = normalizeSectionForLink(sectionTitle); if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_')); var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle); var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $target.append( '<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' + '<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' + '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>' ); } // ─── UI-строители ──────────────────────────────────────────────────────── function buildInfoBoxHtml(mainText, detailsText, isErr) { var cls = isErr ? ' class="error"' : ''; return joinHtml([ '<div class="rmInfoBox">', '<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>', detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '', '</div>' ]); } function buildActionsHtml(actions, inputName, listId) { var actionItemsHtml = actions.map(function (a, i) { var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : ''); var tagHtml = a.tag ? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>' : ''; return joinHtml([ '<label class="rmActionItem">', '<span class="rmActionMain">', '<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>', tagHtml, '<span>', a.label, '</span>', '</span>', meta ? '<span class="rmActionMeta">' + meta + '</span>' : '', '</label>' ]); }).join(''); return joinHtml([ '<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>', '<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">', actionItemsHtml, '</div>' ]); } function buildNestedCommentFieldsHtml(opts) { var options = opts || {}; var wrapId = options.wrapId || ''; var textareaId = options.textareaId || ''; var textareaClass = options.textareaClass ? ' ' + options.textareaClass : ''; var textareaStyleExtra = options.textareaStyleExtra || ''; var wrapStyleExtra = options.wrapStyleExtra || ''; var placeholder = options.placeholder || 'Комментарий (необязательно)'; var beforeHtml = options.beforeHtml || ''; var marginTop = options.marginTop || '6px'; var minHeight = parseInt(options.minHeight, 10) || 90; var isEmbedded = !!options.embedded; var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"'); var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;'; if (isEmbedded) { wrapStyle += 'padding:0;border:0;background:transparent;'; } else { wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';'; } wrapStyle += wrapStyleExtra; return joinHtml([ '<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">', beforeHtml, '<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass, '" placeholder="', escapeHtml(placeholder), '" style="', stInputFull, 'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>', buildQuickPhrasesPanelHtml(textareaId), '</div>' ]); } function buildConditionalRetFieldsHtml() { return buildNestedCommentFieldsHtml({ wrapId: 'rmCloseConditionalWrap', textareaId: 'rmCloseConditionalReason', placeholder: 'Условие / пояснение (необязательно)', marginTop: '8px', minHeight: 90, beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">' }); } function buildAddMultiPageButtonHtml(options) { var opts = options || {}; var title = opts.addTitle || 'Мультиноминация: добавить страницу'; return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn'); } function buildSquareAddButtonHtml(id, title, className, symbol) { var idAttr = id ? ' id="' + id + '"' : ''; var clsAttr = className ? ' class="' + className + '"' : ''; var label = title || 'Добавить'; return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>'; } function leftControlStyle(style) { return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;'); } function buildLeftSquareAddButtonHtml(id, title, className, symbol) { return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn)); } function buildLeftRemoveButtonHtml(className, title) { var cls = className || 'rmRemoveInput'; var label = title || 'Удалить'; return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>'; } function buildMultiRenameVariantAddButtonHtml() { return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷'); } function buildStartMultiPageButtonHtml(title) { var label = title || 'Мультиноминация: добавить страницу'; return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn'); } function buildMultiPageButtonsHtml(commentWrapId, commentId, options) { var opts = options || {}; var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;'); var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице'; var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице'; if (opts.showAdd) return buildAddMultiPageButtonHtml(opts); return joinHtml([ '<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId, '" data-rm-comment-textarea="', commentId, '" data-rm-comment-title="', escapeHtml(commentTitle), '" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle), '" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>', '<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>' ]); } function buildMultiRenameVariantRowHtml(value, options) { var opts = options || {}; return joinHtml([ '<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">', buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'), '<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'), '" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>', '</div>' ]); } function buildMultiPageRowHtml(index, options) { var opts = options || {}; var pageInputId = 'rmMultiPage' + index; var commentWrapId = 'rmMultiPageCommentWrap' + index; var commentId = 'rmMultiPageComment' + index; var pageValue = opts.pageValue || ''; var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : ''; var inputPlaceholder = opts.inputPlaceholder || 'Страница'; var targetInputClass = opts.targetInputClass || ''; var targetInputHtml = ''; var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)'; var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0'; var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'); var blockStyle = 'max-width:100%;box-sizing:border-box;'; var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, { showAdd: !!opts.showAdd, showComment: !!opts.showComment, addTitle: opts.addTitle, removeTitle: opts.removeTitle, commentTitle: opts.commentTitle, commentExpandedTitle: opts.commentExpandedTitle }); if (opts.targetInput) { targetInputHtml = joinHtml([ '<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'), '" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">' ]); } return joinHtml([ '<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">', '<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">', '<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>', opts.targetVariants ? '' : targetInputHtml, buttonsHtml, '</div>', opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '', buildNestedCommentFieldsHtml({ wrapId: commentWrapId, textareaId: commentId, textareaClass: 'rmMultiPageCommentInput', placeholder: commentPlaceholder, marginTop: multiNominationGap, minHeight: 90, embedded: true, wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;', textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';' }), '</div>' ]); } function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) { var addLabel = 'Добавить вариант нового заголовка'; var currentValue = currentPageName || ''; return joinHtml([ '<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">', '<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">', buildStartMultiPageButtonHtml(startMultiTitle), '</div>', '<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">', buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'), '<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">', '</div>', '<div id="', config.containerId, '"></div>', '</div>' ]); } function showInfoAndClose(mainText, detailsText, isErr) { $('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false)); renderModalFooter('close'); } function getSelectedAction(inputName, actionMap) { var id = $('[name="' + inputName + '"]:checked').val(); var sel = actionMap[id]; if (!sel) alert('Выберите действие.'); return sel || null; } function prependTemplateToNoinclude(text, templateText) { var source = String(text || ''); var tpl = String(templateText || '').trim(); if (!tpl) return source; var match = source.match(RE_NOINCLUDE); if (match) { var before = source.slice(0, source.indexOf(match[0])); if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source; var content = String(match[2] || '').replace(/^\n+/, ''); return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>'); } return '<noinclude>' + tpl + '</noinclude>\n' + source; } function buildGeneratedNominationTemplateText(job, pg) { var tplStr = ''; if (!job) return ''; if (job.opId === 'fRm') { tplStr = job.kbuTemplate || ''; if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo; if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment; return tplStr ? (T_OPEN + tplStr + T_CLOSE) : ''; } if (typeof job.articleTpl !== 'function') return ''; tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]); if (job.opId === 'merge' && job.tplpar) { tplStr = (job.op.nomination.articleTpl)( ('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1), job.date[0] ); } return tplStr ? (T_OPEN + tplStr + T_CLOSE) : ''; } function applyConflictTemplateResolution(articleText, job, pg, decision) { var rule = getNominationConflictRule(job); var generatedTemplate = buildGeneratedNominationTemplateText(job, pg); var source = String(articleText || ''); if (!generatedTemplate || !decision) return source; if (decision.templateAction === 'overwrite') { var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source; return prependTemplateToNoinclude(cleaned, generatedTemplate); } if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate); return source; } function inspectMultiNominationConflicts(job, callback) { var cb = callback || function () {}; var pages = (job && job.multiArticles) ? job.multiArticles.slice() : []; var conflicts = []; var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false }); if (!pages.length) { logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false }); cb(null, conflicts); return; } eachSequential(pages, function (pg, next) { getText(pg, function (articleText, readErr) { var conflict; if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; } if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; } conflict = detectNominationConflict(articleText, job); if (conflict) { conflicts.push($.extend({ pageName: pg }, conflict)); logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false }); } next(); }); }, function (err) { if (err) { logStatus('Проверка статей.', err, { statusId: statusId }); cb(err); return; } logStatus( conflicts.length ? 'Проверка завершена: найдены статьи с уже установленными шаблонами.' : 'Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false } ); cb(null, conflicts); }); } function buildNominationConflictResolutionHtml(conflicts) { return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' + '<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' + '<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' + '<div id="rmConflictList" class="rmConflictList">' + conflicts.map(function (conflict, index) { var pageLink = buildStatusPageLink(conflict.pageName); return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' + '<input type="hidden" class="rmConflictPageAction" value="skip">' + '<input type="hidden" class="rmConflictTemplateAction" value="keep">' + '<div class="rmConflictTitle">' + pageLink + '</div>' + '<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' + '<div class="rmConflictGroup">' + '<div class="rmConflictGroupTitle">Действие со статьёй</div>' + '<div class="rmConflictButtons" data-rm-conflict-group="page">' + '<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' + '</div></div>' + '<div class="rmConflictGroup">' + '<div class="rmConflictGroupTitle">Действие с шаблоном</div>' + '<div class="rmConflictButtons" data-rm-conflict-group="template">' + '<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' + '<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' + '</div>' + '<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' + '</div></div>'; }).join('') + '</div>'; } function updateNominationConflictCardState($card) { var pageAction = $card.find('.rmConflictPageAction').val() || 'skip'; var disableTemplate = pageAction !== 'keep'; var $templateButtons = $card.find('[data-rm-choice-type="template"]'); $card.toggleClass('is-skip', disableTemplate); $card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate); $templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate); } function bindNominationConflictResolutionUi() { var $content = $('#removerModalContent'); function setChoice($card, type, value) { var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction'; $card.find(inputClass).val(value); $card.find('[data-rm-choice-type="' + type + '"]').each(function () { var isActive = $(this).data('rmChoice') === value; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); updateNominationConflictCardState($card); } $content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () { var $btn = $(this); var $card = $btn.closest('.rmConflictCard'); if ($btn.prop('disabled')) return; setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice')); }); $('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); }); } function collectNominationConflictResolution(conflicts) { var decisions = {}; (conflicts || []).forEach(function (conflict, index) { var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]'); decisions[normTitle(conflict.pageName)] = { pageAction: $card.find('.rmConflictPageAction').val() || 'skip', templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep' }; }); return decisions; } function applyNominationConflictResolutionToJob(job, decisions) { var resultArticles; var headerText; if (!job || !job.isMulti) { job.conflictDecisions = decisions || {}; return { value: job }; } resultArticles = (job.multiArticles || []).filter(function (pageName) { var decision = decisions && decisions[normTitle(pageName)]; return !decision || decision.pageAction !== 'skip'; }); if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' }; job.conflictDecisions = decisions || {}; job.multiArticles = resultArticles.slice(); job.pages = resultArticles.slice().reverse(); if (job.multiRenamePairs && job.multiRenamePairs.length) { job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) { return pair && resultArticles.indexOf(pair.pageName) !== -1; }); } if (job.multiRenameTargets) { job.multiRenameTargets = resultArticles.reduce(function (map, pageName) { map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets'); return map; }, {}); } if (job.multiRenameTemplateTargets) { job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) { map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets'); return map; }, {}); } headerText = String(job.multiHeaderText || '').trim(); job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]')); job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, ''); job.msg = job.multiNominationFormat === 'list' ? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null) : buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false }) : { leadingBlankLine: false }); job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]'); return { value: job }; } function showNominationConflictResolution(job, conflicts, onContinue) { resetModalObservers(); $('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts)); bindNominationConflictResolutionUi(); syncLinkWidths(); renderModalFooter('submit', { submitText: 'Продолжить номинирование', showSubscribe: true, preserveLogOnSubmit: true, onSubmit: function () { var decisions = collectNominationConflictResolution(conflicts); var applied = applyNominationConflictResolutionToJob(job, decisions); if (applied.error) { alert(applied.error); return false; } if (typeof onContinue === 'function') onContinue(applied.value); return true; } }); } function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) { var opts = options || {}; var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180; var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100; var allowWidthResize = opts.allowWidth !== false; var syncFn = typeof sync === 'function' ? sync : function () {}; var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; }; var usePointerEvents = typeof window.PointerEvent === 'function'; var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 }; var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;'; if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';'; var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>'); function getCoord(evt, key) { var e = evt.originalEvent || evt; if (e.touches && e.touches.length) return e.touches[0][key]; if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key]; return e[key]; } function stopDrag() { dragState.active = false; $(window).off('.rmTaResize'); } function onDragMove(evt) { var clientX; var clientY; if (!dragState.active) return; clientX = getCoord(evt, 'clientX'); clientY = getCoord(evt, 'clientY'); if (typeof clientY !== 'number') return; if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px'); $ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px'); syncFn(); if (evt.preventDefault) evt.preventDefault(); } function startDrag(evt) { var clientY = getCoord(evt, 'clientY'); if (typeof clientY !== 'number') return; dragState.active = true; dragState.startX = getCoord(evt, 'clientX') || 0; dragState.startY = clientY; dragState.startWidth = $ta.outerWidth(); dragState.startHeight = $ta.outerHeight(); if (evt.preventDefault) evt.preventDefault(); $(window).off('.rmTaResize'); if (usePointerEvents) { $(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag); } else { $(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag); } } $ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' }); $ta.next('[data-rm-textarea-grip]').remove(); $ta.after($grip); if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag); else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag); } function applyModalContentWidth($modal, contentWidth, options) { var opts = options || {}; var layout = getModalLayout(); var modalFrame = getBoxFrameWidth($modal); var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame)); var modalWidth = safeContentWidth + modalFrame; var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0; $modal.css({ width: modalWidth + 'px', 'max-width': layout.maxOuterWidth + 'px', 'box-sizing': 'border-box', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' }).toggleClass('rmCompactContent', safeContentWidth < 520); $('.' + RESIZE_CLASS).css({ width: safeContentWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); $('#rmMsg,#nominationReason,#rmReportText').each(function () { var $textarea = $(this); var textareaId = this.id; if (!$textarea.length) return; $textarea.css('width', safeContentWidth + 'px'); $textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px'); $('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px'); }); $('.rmNestedCommentInput').each(function () { var $textarea = $(this); var $wrap = $textarea.parent(); var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0; var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0; var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0; var wrapOuterWidth; var textareaWidth; if (!$wrap.length || !$wrap.is(':visible')) return; wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame); textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame); $wrap.css({ width: wrapOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' }); $textarea.css({ width: textareaWidth + 'px', 'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px' }); $textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px'); $('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px'); }); syncLinkWidths(); if (isVector22) { var $content = $('#content'); if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' }); else if ($content.length) $content.css({ 'min-width': '' }); } else { $('#content').css({ 'min-width': '' }); } if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth); return safeContentWidth; } function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) { var $ta = $('#' + textareaId); var $wrap = $('#' + wrapId); var $modal = $('#removerModal'); var $container = $wrap.parent(); var layout = getModalLayout(); var safeMinWidth = parseInt(minWidth, 10) || 280; var safeMinHeight = parseInt(minHeight, 10) || 90; var initialWidth; var modalFrame; var containerFrame; var wrapFrame; function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } function getMaxTextareaWidth(currentLayout) { return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame); } function getEffectiveMinWidth(currentLayout) { return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout)); } function sync() { var currentLayout = getModalLayout(); var maxTextareaWidth = getMaxTextareaWidth(currentLayout); var textareaWidth = $ta.outerWidth(); var contentWidth; if (!$wrap.is(':visible')) return; if (textareaWidth > maxTextareaWidth) { $ta.css('width', maxTextareaWidth + 'px'); textareaWidth = $ta.outerWidth(); } contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame); applyModalContentWidth($modal, contentWidth); } if (!$ta.length || !$wrap.length || !$modal.length) return; modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width'); containerFrame = $container.length ? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width') : 0; wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width'); initialWidth = Math.min( Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)), getMaxTextareaWidth(layout) ); $ta.css({ width: initialWidth + 'px', 'min-width': getEffectiveMinWidth(layout) + 'px', 'min-height': safeMinHeight + 'px', 'box-sizing': 'border-box', resize: layout.useFullWidth ? 'none' : 'both', 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' }); $ta.data('rmNestedContainerFrame', containerFrame); $ta.data('rmNestedWrapFrame', wrapFrame); $ta.data('rmNestedMinWidth', safeMinWidth); $ta.next('[data-rm-textarea-grip]').remove(); if (layout.useFullWidth) { bindTouchTextareaGrip($ta, sync, function () { return getMaxTextareaWidth(getModalLayout()); }, { minWidth: getEffectiveMinWidth(layout), minHeight: safeMinHeight }); } registerModalLayoutSync(sync); $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); if (typeof ResizeObserver === 'function') { var observer = new ResizeObserver(sync); observer.observe($ta[0]); registerResizeObserver(observer); } sync(); } function setupResizableModal(textareaId) { var $ta = $('#' + textareaId); var $modal = $('#removerModal'); var layout = getModalLayout(); function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; } applyV2022Layout($modal); $modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' }); var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box'; var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width'); var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0; var minWidth = layout.minWidth; $modal.data('rmInitialContentW', initialContentW); $ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box', border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase, color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW }); $(window).off('.rmTaResize'); function sync() { var currentLayout = getModalLayout(); var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame)); var w = $ta.outerWidth(); if (w > maxTextareaWidth) { $ta.css('width', maxTextareaWidth + 'px'); w = $ta.outerWidth(); } applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w)); } if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () { return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame)); }); else { $ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' }); $ta.next('[data-rm-textarea-grip]').remove(); } registerModalLayoutSync(sync); $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); if (typeof ResizeObserver === 'function') { var observer = new ResizeObserver(sync); observer.observe($ta[0]); registerResizeObserver(observer); } sync(); } function addInputRow(opts) { var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0; var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0); var rowClass = opts.rowClass ? ' ' + opts.rowClass : ''; var widthStyle = opts.fitParentWidth ? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;' : (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : '')); $('#' + opts.containerId).append(joinHtml([ '<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">', opts.prefixHtml || '', '<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">', opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>', '</div>' ])); syncModalLayout(); } $(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () { $(this).closest('.rmInputRow').remove(); syncModalLayout(); }); $(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) { var targetId; var phrase; e.preventDefault(); targetId = $(this).data('rmTarget'); phrase = $(this).attr('data-rm-phrase') || ''; if (!targetId) return; insertTextIntoTextarea($('#' + targetId), phrase); }); function buildMultiInputHtml(c) { var addLabel = c.addBtnLabel || '+ Добавить'; var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : ''; var addSymbol = c.type === 'rename' ? '⤷' : '+'; return joinHtml([ '<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">', buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol), '</div>', '<div id="', c.containerId, '"></div>' ]); } function wireMultiInput(c) { $('#' + c.addBtnId).click(function () { var count = $('#' + c.containerId + ' .rmInputRow').length; if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; } addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass, rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '', indentLeft: 0, fitParentWidth: c.type === 'rename', prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '', removeBeforeInput: c.type === 'rename' }); }); } function buildSettingsFieldHtml(label, controlHtml, helpText, options) { var opts = options || {}; var helpHtml = helpText ? '<div class="rmSettingsFieldHint">' + helpText + '</div>' : ''; var labelHtml = opts.forId ? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>' : '<div class="rmSettingsFieldLabel">' + label + '</div>'; return joinHtml([ '<div class="rmSettingsField">', labelHtml, '<div class="rmSettingsFieldControl">', controlHtml, '</div>', helpHtml, '</div>' ]); } function buildSettingsSectionHtml(title, bodyHtml, helpText, options) { var opts = options || {}; var headerHtml = ''; var description = []; if (opts.titleNote) description.push(opts.titleNote); if (helpText) description.push(helpText); if (title || description.length) { headerHtml = joinHtml([ '<div class="rmSettingsSectionHeader">', title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '', description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '', '</div>' ]); } return joinHtml([ '<div class="rmSettingsSection">', headerHtml, bodyHtml, '</div>' ]); } function buildSettingsSimpleCheckboxHtml(id, text) { return joinHtml([ '<label class="rmSettingsCheck">', '<input id="', id, '" type="checkbox">', '<span>', text, '</span>', '</label>' ]); } function buildQuickPhrasesSettingsEditorHtml() { return joinHtml([ '<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">', '<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>', '<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">', '<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>', '</div>' ]); } function getQuickPhraseEditor() { return $('#rmSettingsQuickPhrasesEditor'); } function getQuickPhraseEditorState() { var $editor = getQuickPhraseEditor(); var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []); var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10); if (isNaN(editingIndex)) editingIndex = -1; return { editor: $editor, phrases: phrases, editingIndex: editingIndex }; } function setQuickPhraseEditorState(phrases, editingIndex) { var $editor = getQuickPhraseEditor(); var normalized = normalizeQuickPhrasesList(phrases, []); var safeEditingIndex = parseInt(editingIndex, 10); if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1; if (!$editor.length) return; $editor.data('rmQuickPhrases', normalized); $editor.data('rmQuickPhraseEditingIndex', safeEditingIndex); renderQuickPhraseEditor(); } function clearQuickPhraseDropState() { var $editor = getQuickPhraseEditor(); $editor.removeData('rmQuickPhraseDragIndex'); $editor.removeData('rmQuickPhraseDropIndex'); $editor.removeData('rmQuickPhraseDropAfter'); $editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after'); } function renderQuickPhraseEditor() { var state = getQuickPhraseEditorState(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); var $meta = $('#rmSettingsQuickPhraseMeta'); if (!state.editor.length || !$list.length || !$input.length) return; if (state.phrases.length) { $list.html(state.phrases.map(function (phrase, index) { var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : ''); return joinHtml([ '<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">', '<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>', '<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons); $('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || ''); $('#rmSettingsSignatureSeparator').val(data.signatureSeparator || ''); $('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', ')); $('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', ')); setQuickPhraseEditorState(data.quickPhrases || [], -1); $('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing'); clearQuickPhraseDropState(); applyMenuTitlePresetControls(menuTitleValue); updateSignatureSeparatorPreview(data.signatureSeparator || ''); } function collectSettingsFormValues(options) { var opts = options || {}; var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val()); var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val()); var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle'); if (namespaces.invalid.length) { return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' }; } if (disabledItems.invalid.length) { return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' }; } if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput(); return { value: normalizeRemoverSettings({ notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'), subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'), showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'), menuTitle: forcePresetOnly ? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string' ? storedMenuTitle : (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset())) : (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()), signatureSeparator: $('#rmSettingsSignatureSeparator').val(), excludedNamespaces: namespaces.values, disabledItems: disabledItems.values, quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues() }) }; } function updateSettingsSubmitReadyState(baselineSettings) { var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true }); var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings))); $('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError')); $('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none'); } function bindSettingsSubmitReadyState(baselineSettings) { var update = function () { setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0); }; $('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update); $('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on( 'click.rmSettingsReady keyup.rmSettingsReady', '.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput', update ); $('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update); updateSettingsSubmitReadyState(baselineSettings); } function buildSettingsFormHtml(menuLabelsHint) { var menuFields = buildSettingsFieldHtml('Заголовок отдельного меню', '<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(), getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) + buildSettingsFieldHtml('Визуальное оформление меню', '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>'); var messageFields = buildSettingsFieldHtml('Префикс перед подписью', '<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>', { forId: 'rmSettingsSignatureSeparator' }) + buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(), 'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' }); var defaultFields = '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') + buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>'; var disableFields = buildSettingsFieldHtml('Скрыть пункты меню', '<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint, { forId: 'rmSettingsDisabledItems' }) + buildSettingsFieldHtml('Не показывать в пространствах имён', '<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.', { forId: 'rmSettingsExcludedNamespaces' }); return joinHtml([ '<div id="rmSettingsForm" style="max-width:100%;">', '<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>', buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'), buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'), buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'), buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'), '</div>' ]); } function buildSettingsFooterLeftHtml() { return joinHtml([ '<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">', '<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>', '<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ', 'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ', 'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>', '</div>' ]); } function openSettings() { var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults); var menuLabelsHint = buildSettingsMenuItemsHint(); var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $(); var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice(); function restorePreviousModal() { closeModal(); if ($previousModal.length) { $('#content').prepend($previousModal); modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice(); if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); syncModalLayout(); syncLinkWidths(); } } createModal({ title: 'Конфигурация', width: 'compact', showSettingsButton: false }); $('#removerModal').addClass('rmModalSettings'); $('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←')); $('#rmSettingsBack').on('click', restorePreviousModal); $('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint)); fillSettingsFormValues(currentSettings); bindMenuTitlePresetControls(); bindSignatureSeparatorPreview(); bindQuickPhrasesEditor(); renderModalFooter('submit', { showCheckbox: false, submitText: 'Сохранить', onSubmit: function () { var collected = collectSettingsFormValues(); var shouldReset; var saveFn; if (collected.error) { alert(collected.error); return false; } shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults); saveFn = shouldReset ? function (callback) { resetSettingsOnServer(callback); } : function (callback) { saveSettingsToServer(collected.value, callback); }; saveFn(function (err) { if (err) { alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.'); unlockModalSubmit(); return; } location.reload(); }); } }); var $settingsActions = $('#rmFooterActionButtons'); $settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>'); $settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>'); bindSettingsSubmitReadyState(currentSettings); $('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml()); $('#rmSettingsResetFooter').on('click', function () { fillSettingsFormValues(settingsDefaults); updateSettingsSubmitReadyState(currentSettings); $('#removerSubmit').trigger('focus'); }); } // ─── Завершение обработки ──────────────────────────────────────────────── function finalizeSuccess(nominationInfo, usePageReload) { if (isError) { var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>'); markSubmitError(); return; } renderModalFooter('reload'); if (nominationInfo && nominationInfo.pageTitle) { appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle); } if (!usePageReload && !nominationInfo) location.reload(); } function finalizeFastRemoval(notifiedPages, summary) { if (isError || !setAlert || !notifiedPages || !notifiedPages.length) { finalizeSuccess(null, false); return; } notifyAuthorsForPages(notifiedPages, { summary: summary, actionText: 'к быстрому удалению' }, function () { finalizeSuccess(null, false); }); } // ─── Общий runner ──────────────────────────────────────────────────────── /** * Универсальный запуск полного пайплайна номинации. * @param {Object} o * templateStep — функция (next) → обработка шаблонов на статьях * nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle}) * notifyStep — функция (nominationInfo, next) * skipNotify — boolean * skipLink — boolean, не показывать ссылку на номинацию */ function runFlow(o) { runNominationPipeline({ templateStep: o.templateStep, nominationStep: o.nominationStep, notifyStep: o.notifyStep || function (info, next) { next(); }, skipNotify: o.skipNotify, onSuccess: function (ctx) { if (isError) { markSubmitError(); return; } renderModalFooter('reload'); if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) { appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle); } }, onFailure: function () { markSubmitError(); } }); } // ═══════════════════════════════════════════════════════════════════════════ // ЯДРО: обработка статей (apply template + nomination page) // ═══════════════════════════════════════════════════════════════════════════ /** * Применяет шаблон к одной статье/категории. * Понимает режим inArticle (вставка через <noinclude>), * режим closeAction (снятие шаблона + запись на СО), * режим cleanupAction (снятие КБУ/КУЛ). * * @param {string} pg — название страницы * @param {Object} job — параметры задания (см. buildJob) * @param {function} callback(err, meta) */ function applyTemplateToPage(pg, job, callback) { var mode = job.mode; // ── Снятие КБУ/КУЛ ────────────────────────────────────────────────── if (mode === 'cleanup') { var tm = job.transferMode || 'none'; if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; } editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var local = removeTransferTemplatesLocal(article, tm); removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) { if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; } done({ text: updated.text }); }); }, function (err) { callback(err); }); return; } // ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ─────────────── if (mode === 'denom') { getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; } if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; } if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; } var tplPattern = job.sourceTemplate.split('|').map(function (alias) { return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tpl = findTemplateByPattern(article, tplPattern); if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; } var normalizedTplDate = convertToStandardDate(tpl.params[0]); var tplExtraParams = tpl.params.slice(1); var tplExtra = tplExtraParams.join('|').trim(); var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams); if (!RE_DATE_ISO.test(normalizedTplDate)) { callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' }); return; } var date = getDate(normalizedTplDate); var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1]; var retTalkSection = ''; var sectionNW, tplpar, newTalkTpl; if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; } if (job.closeType === 'noRnm') { if (!renameTargets.length) { callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' }); return; } sectionNW = pg + ' → ' + renameTargets.join(', '); tplpar = pg + '|' + renameTargets.join('|'); } if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion') { retTalkSection = tplExtra; sectionNW = retTalkSection || pg; tplpar = retTalkSection ? ('l1=' + retTalkSection) : ''; } var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate); var talkTitle = getTalkPage(pg); newTalkTpl = (job.closeType === 'retConditional') ? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1) : (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE); getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) { if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; } var sourceTalkText = talkText || ''; var talkPageMissing = talkText === null; var talkResult = (job.closeType === 'ret') ? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection) : (job.closeType === 'withdrawnDeletion') ? upsertRemovedFromDeletionTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection) : (job.closeType === 'retConditional') ? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline) : { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' }; function saveArticle() { var cleaned = stripTemplatesByPattern(article, tplPattern).text; var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName }; if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (t) { var editErr = t && t.error ? t.error : null; callback(editErr, editErr ? null : { discussionPage: nomPlace, discussionSection: sectionNW, summary: editSummary }); }); } if (talkResult.text === sourceTalkText) { saveArticle(); return; } var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName }; if (talkPageMissing) talkEp.createonly = true; else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp; apiReq(talkEp, 'edit', function (talkResp) { if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; } saveArticle(); }); }); }); return; } // ── Обычная номинация: вставка шаблона в статью ───────────────────── // mode === 'nominate' var isKu = job.opId === 'tRm' || job.opId === 'mRm'; var conflictRule = getNominationConflictRule(job); var conflictLabel = conflictRule ? conflictRule.label : 'номинации'; editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var hasExistingNomination = conflictRule && conflictRule.detect(article); var conflictDecision = getConflictDecisionForPage(job, pg); function buildResult(finalText) { var generatedTpl = buildGeneratedNominationTemplateText(job, pg); return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText }; } function finishConflictResolution(sourceText) { var resolvedText; var pageLink = buildQuotedStatusPageLink(pg); if (conflictDecision.templateAction === 'keep') { if (sourceText !== article) { return { text: sourceText, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' } }; } return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } }; } resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision); return { text: resolvedText, meta: { successMessage: conflictDecision.templateAction === 'overwrite' ? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.' : 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.' } }; } if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) { return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } }; } if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') { if (job.transferMode && job.transferMode !== 'none') { var localConflict = removeTransferTemplatesLocal(article, job.transferMode); removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) { done(finishConflictResolution(updated.text)); }); return; } return finishConflictResolution(article); } if (isKu && job.transferMode && job.transferMode !== 'none') { var local = removeTransferTemplatesLocal(article, job.transferMode); removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); }); return; } return buildResult(article); }, function (err) { callback(err); } ); } /** * Обрабатывает список страниц последовательно. * @param {string[]} pages * @param {Object} job * @param {function} onDone(notifiedPages, err, pageMeta) */ function processPageList(pages, job, onDone) { var notifiedPages = []; var pageMeta = {}; eachSequential(pages.slice().reverse(), function (pg, nextPage) { var pageLink = buildQuotedStatusPageLink(pg); var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false }); applyTemplateToPage(pg, job, function (err, meta) { var normPg = normTitle(pg); var isClose = job.mode === 'cleanup' || job.mode === 'denom'; if (!isClose) { if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false }); else logPageEdit(pg, err, { statusId: statusId }); } else { if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); } else { logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false }); if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false }); } } if (!err) { notifiedPages.push(pg); if (meta) pageMeta[normPg] = meta; } nextPage(err || null); }); }, function (err) { onDone(notifiedPages, err, pageMeta); }); } // ═══════════════════════════════════════════════════════════════════════════ // ПОСТРОЕНИЕ JOB из формы // ═══════════════════════════════════════════════════════════════════════════ /** * Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov). * @param {Object} op — запись из OPERATIONS * @param {string} pg — целевая страница (уже разрешённая) * @param {boolean} isMulti — режим мультиноминации * @returns {Object|false} — job или false при ошибке ввода */ function buildNominationJob(op, pg, isMulti) { var nom = op.nomination; var date = getDate(); var msg = normalizeQuickPhraseValue($('#rmMsg').val()); var rawMsg = msg; var opId = isMulti ? (nom.multiOpId || op.id) : op.id; var tplpar = ''; var section, sectionNW, extraPages, multiArticles = []; var multiHeaderText = ''; var multiArticleComments = {}; var multiFormat = 'sections'; var multiRenamePairs = []; var multiRenameTargets = {}; var multiRenameTemplateTargets = {}; var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length; // Вычислить section и tplpar в зависимости от типа дополнительного ввода if (nom.extraInput) { var ei = nom.extraInput; if (ei.type === 'rename') { if (isRenameWithRowTargets) { multiRenamePairs = collectMultiRenamePairs(); if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false; multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames'); multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames'); tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames); section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames); } else { var rn = collectInputValues('.rmRenameInput'); if (!rn.length) { alert('Укажите новое название.'); return false; } tplpar = buildRenameTemplateParam(rn); section = formatRenameItemLabel(pg, rn); } } else if (ei.type === 'merge') { var mn = collectInputValues('.rmMergeInput'); if (!mn.length) { alert('Укажите статью для объединения.'); return false; } tplpar = pg + '|' + mn.join('|'); extraPages = mn; section = formatPagesWithAnd([pg].concat(mn)); } else if (ei.type === 'split') { var sn = collectInputValues('.rmSplitInput'); if (!sn.length) { alert('Укажите статьи для разделения.'); return false; } tplpar = formatPagesWithAnd(sn); section = '[[:' + pg + ']] → ' + tplpar; } } if (isMulti) { var ttl = $('#rmHeader').val() || ''; var articles = isRenameWithRowTargets ? multiRenamePairs.map(function (pair) { return pair.pageName; }) : collectInputValues('.rmMultiPageInput'); multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections'; multiArticleComments = collectMultiNominationComments(); multiHeaderText = ttl; multiArticles = articles.slice(); if (!articles.length) { alert('Укажите страницу.'); return false; } if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false; section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : ''); msg = multiFormat === 'list' ? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null) : buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false }) : { leadingBlankLine: false }); } if (!section) section = '[[:' + pg + ']]'; sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, ''); var nomPageDate = date[1]; var nomPage = nom.nomPage(nomPageDate); var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]'); return { mode: 'nominate', opId: opId, op: op, date: date, tplpar: tplpar, articleTpl: nom.articleTpl || function () { return ''; }, inArticle: nom.inArticle !== false, transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'), summary: summary, msg: msg, nomPage: nomPage, navTemplate: nom.navTemplate, section: section, sectionNW: sectionNW, comment: nom.comment || '', extraPages: extraPages || [], isMulti: !!isMulti, multiHeaderText: multiHeaderText, multiNominationBody: rawMsg, multiArticleComments: multiArticleComments, multiNominationFormat: multiFormat || 'sections', multiRenamePairs: multiRenamePairs, multiRenameTargets: multiRenameTargets, multiRenameTemplateTargets: multiRenameTemplateTargets, multiArticles: multiArticles, pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || [])) }; } function getTransferModeFromButtons() { var kbu = $('#rmTransferBtnKbu').hasClass('is-active'); var kul = $('#rmTransferBtnKul').hasClass('is-active'); if (kbu && kul) return 'both'; if (kbu) return 'kbu'; if (kul) return 'kul'; return 'none'; } function buildKbuFormHtml(reasons) { return joinHtml([ '<select id="rmSel" style="', stInputFull, '">', reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''), '</select>', '<input id="fiRm" type="hidden" style="', stInputFull, '">', '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">', buildQuickPhrasesPanelHtml('fiRmComment') ]); } function buildNominationMultiHeaderHtml(pg, options) { var opts = options || {}; var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'); return joinHtml([ '<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">', '<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">', buildAddMultiPageButtonHtml(opts), '</div>', '</div>', '<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">', buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })), '</div>' ]); } function buildTransferBoxHtml() { return joinHtml([ '<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">', '<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>', '<div id="rmTransferModeGroup" class="rmSegmentedBar">', '<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>', '<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>', '</div>', '<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>', '</div></div>' ]); } function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) { return joinHtml([ '<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">', '<div class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>', '</div>', '</div>' ]); } function getMultiNominationUiOptions(kind, options) { var opts = options || {}; var isArticle = kind === 'article'; var isRename = !!opts.renameMulti; var actionText = typeof opts.actionText === 'string' ? opts.actionText : (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : '')); var itemAcc = isArticle ? 'статью' : 'категорию'; var itemDat = isArticle ? 'статье' : 'категории'; var itemGen = isArticle ? 'статьи' : 'категории'; var result = { inputPlaceholder: isArticle ? 'Статья' : 'Категория', addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''), removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''), commentTitle: 'Добавить комментарий к этой ' + itemDat, commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat, commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)' }; if (isRename) { $.extend(result, { targetInput: true, targetInputClass: 'rmMultiRenameTargetInput', targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:', targetVariants: true }); } if (opts.setup) { $.extend(result, { defaultPage: opts.defaultPage || '', multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap' }); if (isRename) $.extend(result, { singleOnlySelector: '#rmSingleRenameBlock', hideContainerWhenSingle: true, singleCurrentPageSelector: '#rmSingleRenameCurrent', singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput', singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput', singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer', singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия', singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined }); } return result; } function buildNominationFormHtml(nom, pg, multiMode) { var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename'; var multiOptions = getMultiNominationUiOptions('article', { renameMulti: isRenameMulti, deletionMulti: multiMode && nom.template === 'к удалению' }); return joinHtml([ isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '', multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '', nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '', '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>', buildQuickPhrasesPanelHtml('rmMsg'), nom.supportsTransfer ? buildTransferBoxHtml() : '', multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : '' ]); } function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) { var opts = options || {}; var multiOptions = getMultiNominationUiOptions('category', opts); return joinHtml([ opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '', multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '', variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '', '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>', buildQuickPhrasesPanelHtml('nominationReason'), multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : '' ]); } function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) { var $textarea = $('#' + textareaId); if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return; setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90); $textarea.data('rmNestedResizerReady', true); } function setMultiPageCommentExpanded($btn, expanded) { var wrapId = $btn.data('rmCommentWrap'); var textareaId = $btn.data('rmCommentTextarea'); var $wrap = $('#' + wrapId); var title = expanded ? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице') : ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице'); if (!$btn.length || !$wrap.length) return; $btn.attr('aria-expanded', expanded ? 'true' : 'false') .attr('aria-label', title) .attr('title', title) .toggleClass('is-active', expanded) .text('✎'); $wrap.toggle(expanded); if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId); } function getMultiPageCommentTargets($block) { var $textarea = $block.find('.rmMultiPageCommentInput').first(); var $wrap = $textarea.parent(); return { wrapId: $wrap.attr('id') || '', textareaId: $textarea.attr('id') || '' }; } function collectMultiRenameTargetValues($block) { return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () { return ($(this).val() || '').trim(); }).get().filter(Boolean); } function setMultiPageRowControls($block, showAdd, showComment, options) { var opts = options || {}; var $row = $block.find('.rmMultiPageRow').first(); var ids = getMultiPageCommentTargets($block); var $commentBtn = $row.find('.rmMultiPageCommentToggle'); if (!$row.length) return; if (showAdd) { if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false); if (ids.wrapId) $('#' + ids.wrapId).hide(); $block.find('.rmMultiRenameVariantsContainer').hide(); $row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove(); if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts)); return; } $block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants); $row.find('.rmAddMultiPage').remove(); if (!$row.find('.rmMultiPageCommentToggle').length) { $row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment }))); return; } $row.find('.rmMultiPageCommentToggle').toggle(showComment); } function setupMultiPageNominationUi(options) { var opts = options || {}; var containerSelector = opts.containerSelector || '#rmMultiPagesContainer'; var pageCounter = parseInt(opts.nextIndex, 10) || 1; var wasMultiModeExpanded = false; function restoreEmptySinglePageInput() { var $pageInput = $(containerSelector + ' .rmMultiPageInput').first(); if (!$pageInput.length || String($pageInput.val() || '').trim()) return; $pageInput.val(opts.defaultPage || ''); } function copySingleCurrentPageToFirstRow() { var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $(); var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first(); var value = String(($source.val && $source.val()) || '').trim(); if (!$source.length || !$target.length || !value) return; $target.val(value); } function copyFirstRowPageToSingleCurrent() { var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $(); var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first(); var value = String(($source.val && $source.val()) || '').trim(); if (!$source.length || !$target.length) return; $target.val(value || opts.defaultPage || ''); } function getSingleRenameTargets() { var targets = []; var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $(); if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim()); if (opts.singleRenameVariantSelector) { $(opts.singleRenameVariantSelector).each(function () { var value = String($(this).val() || '').trim(); if (value) targets.push(value); }); } return targets.slice(0, 3); } function setMultiBlockRenameTargets($block, targets) { var list = asNonEmptyArray(targets).slice(0, 3); var $target = $block.find('.rmMultiRenameTargetInput').first(); var $container = $block.find('.rmMultiRenameVariantsContainer').first(); if (!$target.length) return; $target.val(list[0] || ''); if (!$container.length) return; $container.find('.rmMultiRenameVariantRow').remove(); list.slice(1).forEach(function (value) { $container.append(buildMultiRenameVariantRowHtml(value, opts)); }); } function copySingleRenameTargetsToFirstRow() { var $block = $(containerSelector + ' .rmMultiPageBlock').first(); if (!$block.length) return; setMultiBlockRenameTargets($block, getSingleRenameTargets()); } function copyFirstRowRenameTargetsToSingle() { var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $(); var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first(); var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3); var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $(); if (!$sourceBlock.length || !$target.length) return; $target.val(targets[0] || ''); if (!$variantContainer.length) return; $variantContainer.empty(); targets.slice(1).forEach(function (value) { addInputRow({ containerId: opts.singleRenameVariantContainerId, placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант', inputClass: opts.singleRenameInputClass, rowClass: 'rmRenameVariantRow', indentLeft: 0, fitParentWidth: true, prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'), removeBeforeInput: true }); $variantContainer.find('input').last().val(value); }); } function updateMultiMode() { var $blocks = $(containerSelector + ' .rmMultiPageBlock'); var hasExtra = $blocks.length > 1; var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap; $(containerSelector).css({ gap: pageGap, marginBottom: pageGap }); $('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra); if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra); if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra); if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra); if (!hasExtra && wasMultiModeExpanded) { restoreEmptySinglePageInput(); copyFirstRowPageToSingleCurrent(); copyFirstRowRenameTargetsToSingle(); } $blocks.each(function (index) { setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts); }); wasMultiModeExpanded = hasExtra; syncModalLayout(); } $(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () { var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1; if (wasSingle) { copySingleCurrentPageToFirstRow(); copySingleRenameTargetsToFirstRow(); } $(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts)); updateMultiMode(); }); $(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () { var $block = $(this).closest('.rmMultiPageBlock'); var $container = $block.find('.rmMultiRenameVariantsContainer').first(); var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length; if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; } $container.append(buildMultiRenameVariantRowHtml('', opts)).show(); syncModalLayout(); }); $(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () { $(this).closest('.rmMultiRenameVariantRow').remove(); syncModalLayout(); }); $(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () { var $btn = $(this); if (!$btn.is(':visible')) return; setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true'); syncModalLayout(); }); $(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () { $(this).closest('.rmMultiPageBlock').remove(); updateMultiMode(); }); updateMultiMode(); return { update: updateMultiMode, isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; } }; } function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) { var $root = $(rootSelector); $root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () { var $btn = $(this); $root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false'); $btn.addClass('is-active').attr('aria-pressed', 'true'); }); } function buildProtectAddButtonHtml() { return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage'); } function buildProtectPageRowHtml(id, pageName, isFirstRow) { return joinHtml([ '<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">', '<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"', pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>', isFirstRow ? buildProtectAddButtonHtml() : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>', '</div>' ]); } function buildReportFormHtml(ctx, isZka) { var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.'; if (isZka) { return joinHtml([ '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">', '<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>', buildQuickPhrasesPanelHtml('rmReportText') ]); } return joinHtml([ '<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">', '<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>', '<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>', '</div></div>', '<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">', '<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>', buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true), '<div id="rmProtectPagesContainer"></div>', '</div>', '<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>', '</div></div>', '<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>', '</div></div>', '<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>', '<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>', '</div></div>', '<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>', buildQuickPhrasesPanelHtml('rmReportText'), '</div>' ]); } // ═══════════════════════════════════════════════════════════════════════════ // ОБРАБОТЧИКИ ОПЕРАЦИЙ // ═══════════════════════════════════════════════════════════════════════════ var handlers = { // ── КБУ ───────────────────────────────────────────────────────────── showKbu: function (op) { var forCategory = !!(op && op.forCategory); var reasons = getFastRemoveReasons(); createModal({ title: 'Быстрое удаление', width: 'compact', subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>' }); $('#removerModalContent').html(buildKbuFormHtml(reasons)); function updateKbuReasonControls() { var reason = reasons[$('#rmSel').val()] || reasons[0]; var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null; var showComment = true; $('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason)); if (paramCfg) { var noComment = paramCfg.charAt(0) === '!'; $('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show(); showComment = !noComment; } else { $('#fiRm').attr('type', 'hidden').hide(); } $('#fiRmComment').toggle(showComment); $('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment); } $('#rmSel').change(updateKbuReasonControls); $('#rmSel').trigger('change'); renderModalFooter('submit', { submitText: 'Номинировать', onSubmit: function () { var idx = $('#rmSel').val(); var addInfo = $('#fiRm').val(); var comment = $('#fiRmComment').val(); startProcessing(); if (forCategory) { var tpl = reasons[idx][0]; var categorySummary = makeSummary('номинация категории на быстрое удаление'); if (addInfo) tpl += '|1=' + addInfo; if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment; editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' }, function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; }, function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else { logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false }); finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary); } }); } else { var job = { mode: 'nominate', opId: 'fRm', kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment, summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'), inArticle: true }; processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) { finalizeFastRemoval(notifiedPages, job.summary); }); } return true; } }); }, // ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ──────── showNomination: function (op) { var nom = op.nomination; var pg = normTitle(mwCfg.wgPageName); var date = getDate()[1]; var nomPage = nom.nomPage(date); var multiMode = nom.supportsMulti; function updateTransferUi() { var mode = getTransferModeFromButtons(); var isNone = mode === 'none'; var isKbu = mode === 'kbu' || mode === 'both'; var isKul = mode === 'kul' || mode === 'both'; $('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false'); $('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false'); $('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false'); var t = transferTexts[mode]; if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); } applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : ''); } createModal({ title: 'Номинация: ' + nom.template, subtitlePage: nomPage, subtitleLabel: 'Текущий день' }); $('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode)); setupResizableModal('rmMsg'); // Логика переноса if (nom.supportsTransfer) { $(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () { if (this.id === 'rmTransferBtnNone') { $('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active'); $('#rmTransferBtnNone').addClass('is-active'); } else { $(this).toggleClass('is-active'); var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active'); $('#rmTransferBtnNone').toggleClass('is-active', !anyOn); } updateTransferUi(); }); updateTransferUi(); } // Многостраничный режим if (multiMode) { setupMultiPageNominationUi(getMultiNominationUiOptions('article', { setup: true, defaultPage: pg, renameMulti: nom.extraInput && nom.extraInput.type === 'rename', deletionMulti: nom.template === 'к удалению' })); bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn'); } if (nom.extraInput) wireMultiInput(nom.extraInput); renderModalFooter('submit', { submitText: 'Номинировать', showSubscribe: true, onSubmit: function () { var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1; var singleCurrentInput = $('#rmSingleRenameCurrent').val() || ''; var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : ''; var changed = inputVal && inputVal !== pg; function executeJob(job) { startProcessing(); runFlow({ templateStep: function (next) { if (!job.inArticle) { next(); return; } processPageList(job.pages, job, function (notifiedPages, err) { job._notifiedPages = notifiedPages; next(err); }); }, nominationStep: function (done) { publishNomination({ pageTitle: job.nomPage, navTemplate: job.navTemplate, sectionTitle: job.section, summary: job.summary, text: getNominationPublishText(job) }, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); }); }, notifyStep: function (nominationInfo, next) { var pages = job._notifiedPages || []; if (!setAlert || !pages.length) { next(); return; } notifyAuthorsForPages(pages, { summary: job.summary, actionText: job.comment, discussionPage: nominationInfo && nominationInfo.pageTitle, discussionSection: nominationInfo && nominationInfo.sectionTitle }, next); }, skipLink: op.id === 'fRm' }); } function run(targetPg) { var job = buildNominationJob(op, targetPg, isMulti); if (!job) { unlockModalSubmit(); return; } if (job.isMulti && job.inArticle && getNominationConflictRule(job)) { startProcessing(); inspectMultiNominationConflicts(job, function (err, conflicts) { if (err) { markSubmitError(); return; } if (!conflicts.length) { executeJob(job); return; } showNominationConflictResolution(job, conflicts, function (resolvedJob) { executeJob(resolvedJob); }); }); return; } executeJob(job); } if (changed) { apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) { if (data && data.error) { unlockModalSubmit(); alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.'); return; } var page = getFirstQueryPage(data); if (!page) { unlockModalSubmit(); alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.'); return; } if (page.missing !== undefined) { unlockModalSubmit(); alert('Страница «' + inputVal + '» не существует.'); return; } run(normTitle(page.title || inputVal)); }); } else { run(pg); } return true; } }); }, // ── Снятие номинации (статья) ──────────────────────────────────────── showArticleClose: function () { showCloseActionsModal({ inputName: 'rmCloseAction', listId: 'rmCloseActions', emptyText: 'Не найдено подходящих шаблонов для закрытия.', emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.', getActions: function (articleText) { var actions = []; if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true }); if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true }); if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снятие', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true }); if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true }); if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована',talkNotice: true }); var hasKbu = RE_KBU_ON_PAGE.test(articleText); var hasKul = RE_KUL_ON_PAGE.test(articleText); if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' }); if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' }); if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' }); return actions; }, afterRender: function (actions) { var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; }); var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; }); if (hasDoneRnm) { $('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append( '<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>' ); } if (hasConditionalRet) { $('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml()); } }, afterFooterRender: function (_, actionMap) { function ensureConditionalTextareaResizer() { var $textarea = $('#rmCloseConditionalReason'); if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return; setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90); $textarea.data('rmConditionalResizerReady', true); } function updateUi() { var sel = actionMap[$('[name="rmCloseAction"]:checked').val()]; $('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle)); $('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields)); if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer(); var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu'); var $cb = $('[name="rmUAlert"]'); var $cbLabel = $('[name="rmUAlert"]').closest('label'); if ($cb.length) $cb.prop('disabled', disableNotify); if ($cbLabel.length) $cbLabel.css({ visibility: disableNotify ? 'hidden' : 'visible', pointerEvents: disableNotify ? 'none' : '' }); syncModalLayout(); } $(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi); updateUi(); }, onSubmit: function (sel, pageName) { var job; if (sel.mode === 'denom') { var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : ''; var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : ''; var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : ''; if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; } if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) { alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.'); return false; } job = { mode: 'denom', closeType: sel.closeType, resultTemplate: sel.resultTemplate, sourceTemplate: sel.sourceTemplate, oldTitle: oldTitle, conditionalReason: conditionalReason, conditionalDeadline: conditionalDeadline, notifyActionText: sel.comment, skipNotify: false }; } else { job = { mode: 'cleanup', transferMode: sel.transferMode, summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel), notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both') ? 'больше не номинирована к срочному улучшению' : '', skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both') }; } processPageList([pageName], job, function (notifiedPages, err, pageMeta) { function finishClose() { if (isError) { markSubmitError(); } else { renderModalFooter('reload'); } } if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; } var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {}; notifyAuthorsForPages(notifiedPages, { summary: meta.summary || job.summary, actionText: job.notifyActionText, discussionPage: meta.discussionPage, discussionSection: meta.discussionSection, includeProposedPrefix: false }, finishClose); }); return true; } }); }, // ── ОБКАТ: номинация категории ─────────────────────────────────────── showCatNomination: function (op) { var catType = op.catType; var catMeta = getCategoryNominationMeta(catType); var now = new Date(); var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear(); var pageName = normalizeCategoryPageName(mwCfg.wgPageName); var multiMode = !!catMeta.supportsMulti; var renameMultiMode = !!catMeta.renameMulti && multiMode; createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' }); var variantCfgs = { rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' }, merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' } }; var vCfg = variantCfgs[catType]; var categoryMultiOptions = { renameMulti: renameMultiMode, actionText: catMeta.actionText, skipVariantInput: renameMultiMode }; $('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions)); setupResizableModal('nominationReason'); if (multiMode) { setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({ setup: true, defaultPage: pageName }, categoryMultiOptions))); bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn'); } if (vCfg) wireMultiInput(vCfg); renderModalFooter('submit', { submitText: 'Номинировать', showSubscribe: true, onSubmit: function () { var reason = normalizeQuickPhraseValue($('#nominationReason').val()); var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1; var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({ normalizePageName: normalizeCategoryPageName, normalizeTargetName: normalizeCategoryTargetPageName, normalizeTemplateTargetName: normalizeCategoryTargetName }) : []; var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames'); var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames'); var singleRenameCategory = renameMultiMode && !hasMultiRenameRows ? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName) : ''; var targetPages = hasMultiRenameRows ? renamePairs.map(function (pair) { return pair.pageName; }) : (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]); if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName]; var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1); var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections'; var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {}; var notifiedPages = []; if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false; if (!targetPages.length) { alert('Укажите категорию.'); return false; } if (isMulti) { if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false; } else if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; } var discussionTarget = isMulti ? targetPages : targetPages[0]; var discussionReason = isMulti ? (multiFormat === 'list' ? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null) : buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 }) : { headingLevel: 4 })) : reason; var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true, renameTargetsByPage: renameTargetsByCategory } : null; var mainName = null, additionalNames = []; if (vCfg) { if (hasMultiRenameRows) { mainName = renamePairs[0] && renamePairs[0].templateTargetName; additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : []; } else { mainName = $('#' + vCfg.firstId).val().trim(); if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; } additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput'); } } startProcessing(); runFlow({ templateStep: function (next) { addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) { notifiedPages = processedPages || []; next(err); }, hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : null); }, nominationStep: function (done) { createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions); }, notifyStep: function (nominationInfo, next) { if (!setAlert || !nominationInfo) { next(); return; } var section = normalizeSectionForLink(nominationInfo.sectionTitle || ''); notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, { summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'), actionText: catMeta.actionText, discussionPage: nominationInfo.pageTitle, discussionSection: nominationInfo.sectionTitle }, next); } }); return true; } }); }, // ── Снятие номинации (категория) ───────────────────────────────────── showCatClose: function () { showCloseActionsModal({ inputName: 'rmCategoryCloseAction', showCheckbox: false, emptyText: 'Не найдено подходящих шаблонов для завершения.', emptyDetails: 'Проверяются ОБКАТ и КУ.', getActions: function (catText) { var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|'); var actions = []; if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) { actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true }); } if (RE_KU_ON_PAGE.test(catText)) { actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' }); } return actions; }, onSubmit: function (sel, pageName) { if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName); if (sel.mode === 'cleanup') removeKuFromCategory(pageName); return true; } }); }, // ── Защита / Запрос к администраторам ─────────────────────────────── showReport: function (op) { var mode = op.reportMode || 'protect'; var ctx = getReporterContext(mode); var isZka = mode === 'request'; var protectMode = 'install'; var pageCounter = 1; function buildProtectText(pm) { if (pm === 'remove') { var removeLevels = []; $('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); }); return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : ''; } var levels = [], reasons = []; $('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); }); $('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); }); if (!levels.length && !reasons.length) return ''; var text = 'Просьба установить'; if (levels.length) text += ' ' + levels.join(' и/или '); if (reasons.length) text += ' по причине: ' + reasons.join(', '); return text + '.'; } function applyProtectMode(m) { protectMode = m; var isInstall = m === 'install'; $('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false'); $('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false'); $('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты'); var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты'; $('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>'); $('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall); $('#rmRemoveLevelsWrap').toggle(!isInstall); $('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active'); $('#rmReportText').val('').removeData('rmGenerated'); } function updateProtectMultiUi() { var $rows = $('#rmProtectMultiWrap .rmProtectPageRow'); var hasExtra = $rows.length > 1; if (!$rows.length) { $('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true)); $rows = $('#rmProtectMultiWrap .rmProtectPageRow'); hasExtra = false; } $('#rmProtectHeaderWrap').toggle(hasExtra); if (!hasExtra) $('#rmProtectHeader').val(''); $rows.each(function () { var $row = $(this); $row.find('.rmProtectAddPage,.rmRemoveInput').remove(); $row.append(hasExtra ? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>' : buildProtectAddButtonHtml() ); }); syncModalLayout(); } createModal({ title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы', width: 'compact', subtitleHtml: isZka ? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 'Категория:' + target; addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) { var url = getPageUrl(targetPage); var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>'; if (success) { var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : ''; logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false }); } else { logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false }); } next(); }); }, cb); } function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) { editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' }, function (text) { var existing = text.match(getCategoryMergeRe()); if (existing) { var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; }); var norm = sourceCatName.replace(/\s+/g, ' ').trim(); if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; } return { text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }), summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'), meta: { status: 'updated' } }; } return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } }; }, function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); } ); } // ── Категории: обсуждение ──────────────────────────────────────────────── function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) { var opts = options || {}; var pages = Array.isArray(pageName) ? pageName : null; var titleText; if (pages && pages.length) { titleText = String(opts.headerText || '').trim(); if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage); if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''); return '=== ' + titleText + ' ==='; } var title = '=== [[:' + pageName + ']]'; if (type === 'rename' || type === 'merge') { title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName); if (additionalNames && additionalNames.length) { var conj = type === 'rename' ? ' или ' : ' и '; var head = additionalNames.slice(0, -1).map(formatCatLink).join(', '); title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]); } } else if (type === 'deletion') { title += ' → удалить'; } return title + ' ==='; } function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) { var cb = callback || function () {}; var opts = options || {}; var now = new Date(); var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate(); var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' =='; var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year; var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts); var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, ''); var discText = discTitle + '\n' + discBody + '\n'; var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim(); var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]'; var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget; publishNomination({ pageTitle: discPage, readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.', summary: makeSummary(summaryText), createText: function () { return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText; }, buildText: function (text) { var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text); if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) { return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } }; } if (todayMatch) { var dayContentStart = todayMatch.index + todayMatch[0].length; var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart)); var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length; return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') }; } return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) }; } }, function (err) { if (err) { cb(err); return; } cb(null, { pageTitle: discPage, sectionTitle: sectionTitle }); }); } // ── Категории: завершение ОБКАТ ─────────────────────────────────────────── function markCategoryDiscussionAsDone(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон обсуждения...', errorText: 'Снятие шаблона обсуждения.', successText: 'Шаблон обсуждения снят.', editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' }, buildFn: function (text) { var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|'); var patterns = [ new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'), new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i') ]; var match = null; for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; } if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } }; return { text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'), meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) } }; }, onSuccess: function (meta) { var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false }); updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) { if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; } logStatus( (info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.', null, { statusId: talkStatusId, trackError: false } ); renderModalFooter('reload'); }); } }); } function updateCategoryTalkPage(categoryName, templateDate, callback) { var cb = callback || function () {}; var talkPage = getTalkPage(categoryName); var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE; getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) { if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; } if (text === null) { apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true }, 'edit', function (resp) { if (resp && resp.error) { if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000); else cb(resp.error); } else cb(null, { status: 'created' }); }); return; } var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i'); var tplMatch = text.match(discussedRe); var newText = text; if (tplMatch) { var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean); if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; } newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); }); } else { newText = insertTplOnTalkPage(text, newTpl); } if (newText === text) { cb(null, { status: 'no_changes' }); return; } var ep = { title: talkPage, text: newText, summary: tplMatch ? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate) : makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate) }; if (baseTimestamp) ep.basetimestamp = baseTimestamp; apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); }); }); } // ── Быстрое объединение (Ctrl+клик КОБ) ───────────────────────────────── function buildQuickMergeHtml(tplDate, targets, currentCatName) { return joinHtml([ '<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>', '<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">', targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'), '</pre>', '<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>', '<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>' ]); } function showQuickMergeModal() { getText(mwCfg.wgPageName, function (text, readErr) { if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; } if (!text) { alert('Не удалось получить содержимое.'); return; } var mergeRe = getCategoryMergeRe(); var match = text.match(mergeRe); if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; } var params = match[1].split('|').map(function (p) { return p.trim(); }); var tplDate = params[0]; var targets = params.slice(1); if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; } createModal({ title: 'Быстрое добавление шаблона объединения' }); var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName)); $('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName)); renderModalFooter('submit', { showCheckbox: false, submitText: 'Добавить шаблоны', onSubmit: function () { startProcessing(); $('#removerSubmit').prop('disabled', true); eachSequential(targets, function (target, next) { addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) { if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false }); else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false }); next(); }); }, function () { if (isError) markSubmitError(); else renderModalFooter('close'); }); return true; } }); }); } // ── ЗКА/Защита: публикация ─────────────────────────────────────────────── function getReporterContext(mode) { var rawPage = mwCfg.wgPageName; var pageName = normTitle(rawPage) .replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:') .replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:'); var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage); var displayName = normTitle(rawPage) .replace(/(Special|Служебная):(Вклад|Contributions)\//i, '') .replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '') .replace(/(user|участни(к|ца)):/i, ''); var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]'); var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты'; return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage }; } function getTopDiscussionInsertIndex(pageText) { var introEnd = getIntroBlockEndIndex(pageText); var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd)); return firstHeading ? introEnd + firstHeading.index : introEnd; } function getIntroBlockEndIndex(pageText) { var pos = 0; while (pos < pageText.length) { var next = skipWhitespace(pageText, pos); var end; if (pageText.slice(next, next + 4) === '<!--') { end = pageText.indexOf('-->', next + 4); if (end === -1) return next; pos = end + 3; continue; } end = skipTemplate(pageText, next); if (end !== next) { pos = end; continue; } return next; } return pos; } function skipWhitespace(text, pos) { while (pos < text.length && /\s/.test(text.charAt(pos))) pos++; return pos; } function skipTemplate(text, pos) { var depth = 0; var i = pos; if (text.slice(pos, pos + 2) !== '{{') return pos; while (i < text.length) { if (text.slice(i, i + 4) === '<!--') { var commentEnd = text.indexOf('-->', i + 4); if (commentEnd === -1) return pos; i = commentEnd + 3; continue; } if (text.slice(i, i + 2) === '{{') { depth++; i += 2; continue; } if (text.slice(i, i + 2) === '}}') { depth--; i += 2; if (depth === 0) return i; continue; } i++; } return pos; } function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) { var sep = separator || '\n\n'; var before = pageText.slice(0, insertAt).replace(/\s+$/, ''); var block = String(blockText || '').replace(/\s+$/, ''); var after = pageText.slice(insertAt).replace(/^\s+/, ''); return (before ? before + sep : '') + block + (after ? sep + after : '\n'); } function insertTopDiscussionSection(pageText, sectionText) { return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n'); } function doReport(ctx, fast, protectMode) { var header = $('#rmReportHeader').val() || ctx.pageLink; var text = $('#rmReportText').val() || ''; var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам'; var isRemoveProtect = !isZka && protectMode === 'remove'; startProcessing(); var targetPage, editParams, sectionForLink; if (fast) { targetPage = 'Википедия:Запросы к администраторам/Быстрые'; sectionForLink = null; editParams = { appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName + '| страница = | пояснение = ' + text + T_CLOSE + '\n', summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]') }; } else if (isZka) { targetPage = ctx.reportPage; sectionForLink = extractDisplayedText(header); var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName); editParams = { text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~', summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос') }; } else { targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты'; var pages = collectInputValues('.rmProtectPageInput'); if (!pages.length) pages = [ctx.pageName]; var sectionTitle, pageLines; if (pages.length === 1) { sectionTitle = '[[' + pages[0] + ']]'; pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE; } else { sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', '); pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n'); } sectionForLink = extractDisplayedText(sectionTitle); editParams = { section: 'new', sectiontitle: sectionTitle, text: pageLines + '\n' + text + ' ~~' + '~~', summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос') }; } var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false }); if (isZka && !fast) { editPageContent(targetPage, { summary: editParams.summary, assertuser: mwCfg.wgUserName, readError: 'Не удалось получить содержимое страницы «' + targetPage + '».' }, function (pageText) { return { text: insertTopDiscussionSection(pageText, editParams.text) }; }, function (err) { if (err) { logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId }); markSubmitError(); $('#rmReportFast').prop('disabled', false); return; } logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false }); appendNominationLink(targetPage, sectionForLink); if (sectionForLink) subscribeToTopic(targetPage, sectionForLink); renderModalFooter('reload'); }); return; } apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) { if (resp && resp.error) { logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId }); markSubmitError(); if (isZka) $('#rmReportFast').prop('disabled', false); return; } logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false }); appendNominationLink(targetPage, sectionForLink); if (sectionForLink) subscribeToTopic(targetPage, sectionForLink); renderModalFooter('reload'); }); } // ═══════════════════════════════════════════════════════════════════════════ // ДИСПЕТЧЕР // ═══════════════════════════════════════════════════════════════════════════ function handleMenuClick(item, event) { isError = false; var op = OPERATIONS_MAP[item.id]; // Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона if (item.id === 'cat-merge' && event && event.ctrlKey) { showQuickMergeModal(); return; } if (!op) { console.error('RemoverCore: неизвестная операция', item.id); return; } var handlerFn = handlers[op.handler]; if (typeof handlerFn !== 'function') { console.error('RemoverCore: обработчик не найден', op.handler); return; } handlerFn(op, event); } // ─── Экспорт ───────────────────────────────────────────────────────────── window.RemoverCore = { handleMenuClick: handleMenuClick }; }()); b863j41jpj2ib1f6vn48onqrop2a3d8 Wikipedia:ArticleGuidance/Person 4 175042 745178 741721 2026-06-04T12:19:22Z EAMedina (WMF) 65334 745178 wikitext text/x-wiki (Old testing outline, may be deleted) {{Infobox person}} [Full Name] (born [birth date] in [birthplace]) is a [nationality] [profession] known for [primary reason for notability]. [He/She/They] [brief additional notable fact, e.g., won the [Award Name] in [year] / directed [notable work] / led [organization]] debug. == Early life == [Last Name] was born on [birth date] in [birthplace] to [parent description]. [He/She/They] attended [School Name] in [city], and later studied [subject] at [University Name], graduating in [year]. == Career == === Early career === [Last Name] began [his/her/their] career at [Organization Name] in [year], where [he/she/they] [early role or contribution]. [He/She/They] first gained wider recognition in [year] when [early notable achievement]. === Major achievements === [Last Name]'s most significant work is [work or achievement], which [impact or significance]. In [year], [he/she/they] received the [Award Name] for [reason]. == Personal life == [Last Name] married [Spouse Name] in [year] and has [number] children. [He/She/They] resides in [City, Country] and is known for [personal interest or charitable work]. == Legacy == [Last Name] is regarded as [assessment of significance] in [field or discipline]. [His/Her/Their] work on [contribution] has influenced [subsequent developments or figures], and [he/she/they] is commemorated by [memorial, award, or institution named after them]. == References == <references> $1 </references> == External links == * [https://Official-Website-URL Official Website] [[Category:People]] edbp7erqtrcoq8u4morbkos0byswh6n Wikipedia:ArticleGuidance/City 4 175043 745179 741720 2026-06-04T12:20:20Z EAMedina (WMF) 65334 745179 wikitext text/x-wiki Old testing outline, may be deleted {{Infobox settlement}} [City Name] is a city in [State/Province], [Country], with a population of approximately [number] as of [year]. It is known for [notable characteristic] and serves as [administrative/economic/cultural role] for [region]. == History == [City Name] was founded in [year] by [founder or group], originally serving as a [trading post/military fort/colonial settlement]. The city expanded significantly during [period], driven by [factor such as industrialisation or the railway]. === Early history === The area was first settled by [people or group] around [date]. [Key founder or explorer] established [City Name] in [year], and the first [census/record] recorded a population of [number]. == Geography == [City Name] is situated in [geographic description, e.g., a river valley / a coastal plain] at [elevation], bordered by [neighboring cities or natural features]. The climate is [climate type], with [brief description of seasons or weather patterns]. == Demographics == As of the [year] census, [City Name] had a population of [number]. The most widely spoken languages are [language] and [language], and [percentage]% of residents identify as [ethnic group]. == Economy == The city's economy is centred on [primary industry], with major employers including [Company Name] and [Company Name]. [City Name] is also a hub for [secondary sector or service industry]. == Culture == [City Name] is home to [cultural institution], and hosts the annual [Festival Name] each [month]. The city is known for [dish or culinary tradition], and its [arts/sports] scene includes [notable venue or team]. == References == <references> $1 </references> == External links == * [https://Official-Website-URL Official Website] [[Category:Cities]] qriv4zsstbinq822qd4nzwj5lzpmpw2 User:MrJaroslavik/ExperimentsWarning.js 2 175380 745235 744887 2026-06-05T09:20:44Z MrJaroslavik 44012 745235 javascript text/javascript EW.doWarn = function(warn, pageName, comment) { let currentMonthYear = EW.getMonthYear(); let d = new Date(); let currentMonthNum = d.getMonth() + 1; // 1-12 let currentYear = d.getFullYear(); new mw.Api().get({ action: 'query', prop: 'revisions', rvprop: 'content|timestamp', titles: wgPageName, format: 'json' }).done(function(data) { let pages = data.query.pages; let pageId = Object.keys(pages)[0]; let oldText = ""; let baseTimestamp = ""; if (pageId !== "-1") { oldText = pages[pageId].revisions[0]['*']; baseTimestamp = pages[pageId].revisions[0].timestamp; } // Sestavení šablony a komentáře let templateCall = pageName ? `{{subst:${warn.template}|${pageName}}}` : `{{subst:${warn.template}}}`; let commentLine = comment ? `\n${comment}` : ''; let newWarning = `\n${templateCall}${commentLine} --[[User:MrJaroslavik|MrJaroslavik]] ([[User talk:MrJaroslavik|talk]]) 09:20, 5 June 2026 (UTC)`; let wikitextToSave = ""; let headingText = warn.sectiontitle || currentMonthYear; if (warn.sectiontitle) { // STAV 1: Pevný nadpis (např. Vítejte) - vložíme vždy nakonec s novým nadpisem let prefix = oldText.trim() === '' ? '' : '\n\n'; wikitextToSave = oldText + `${prefix}== ${headingText} ==\n` + newWarning; } else { // STAV 2: Kontrola existence nadpisu pro tento měsíc let monthHeaderRegex = new RegExp(`==\\s*${currentMonthYear}\\s*==`, 'i'); if (monthHeaderRegex.test(oldText)) { // Nadpis už existuje -> přidáme varování jen na úplný konec wikitextToSave = oldText + newWarning; } else { // Nadpis neexistuje. Hledáme první podpis tohoto měsíce! // Regulární výraz chytá data jako "4. 6. 2026", "4.6.2026", "04. 6. 2026" let sigRegex = new RegExp(`\\b\\d{1,2}\\.\\s*0?${currentMonthNum}\\.\\s*${currentYear}\\b`); let match = oldText.match(sigRegex); if (match) { // Našli jsme starší varování z tohoto měsíce bez nadpisu! let matchIndex = match.index; // Najdeme začátek tohoto bloku (nejbližší dvě odřádkování směrem nahoru) let blockStart = oldText.lastIndexOf('\n\n', matchIndex); if (blockStart === -1) { blockStart = 0; // Žádné odřádkování není, vložíme nadpis na úplný začátek } else { blockStart += 2; // Posuneme se za to odřádkování } // Rozřízneme starý text a vložíme nadpis let textBefore = oldText.substring(0, blockStart); let textAfter = oldText.substring(blockStart); let prefix = textBefore.trim() === '' ? '' : '\n\n'; let modifiedOldText = textBefore + `${prefix}== ${currentMonthYear} ==\n\n` + textAfter; // Výsledek: Upravená historie s nadpisem + naše nové varování na konci wikitextToSave = modifiedOldText + newWarning; } else { // Nenašli jsme ani varování z tohoto měsíce -> klasicky nakonec s nadpisem let prefix = oldText.trim() === '' ? '' : '\n\n'; wikitextToSave = oldText + `${prefix}== ${currentMonthYear} ==\n` + newWarning; } } } // Ochrana před editačním konfliktem (pokud stránka existuje) let editParams = { action: 'edit', title: wgPageName, text: wikitextToSave, summary: warn.summary + EW.summarySuffix, minor: false }; if (baseTimestamp !== "") { editParams.basetimestamp = baseTimestamp; editParams.starttimestamp = Math.round(new Date().getTime() / 1000); } // Odeslání na Wikipedii new mw.Api().postWithEditToken(editParams).done((result) => { if (result.edit && result.edit.result === 'Success') { location.reload(); } else { alert('Chyba při ukládání varování.'); } }).fail((code, err) => { if (code === 'editconflict') { alert('Editační konflikt! Někdo stránku mezitím upravil. Zkuste to znovu.'); location.reload(); } else { alert('Nastala chyba API: ' + code); } }); }); }; f3sw89ae4122066nopt481errmxkiby 745236 745235 2026-06-05T09:22:53Z MrJaroslavik 44012 e 745236 javascript text/javascript // ExperimentsWarning.js // ------------------------------------------------------- // Vkládá varování na diskusní stránky uživatelů - s možností odeslat jedním kliknutím nebo před odesláním přidat název stránky // ------------------------------------------------------- // <nowiki> $(function() { const EW = {}; window.ExperimentsWarning = EW; // Spustit pouze ve jmenném prostoru Diskuse s wikipedistou (NS 3) if (mw.config.get('wgNamespaceNumber') !== 3) return; const wgPageName = mw.config.get('wgPageName'); EW.summarySuffix = ''; // Seznam varovacích šablon rozdělený do kategorií const warningGroups = [{ category: 'Experimenty:', items: [{ id: 'exp1', label: 'Experimenty', template: 'Experimenty', summary: 'Varování: Experimenty' }, { id: 'exp2', label: 'Experimenty2', template: 'Experimenty2', summary: 'Varování: Experimenty2' }, { id: 'exp3', label: 'Experimenty3', template: 'Experimenty3', summary: 'Varování: Experimenty3' }] }, { category: 'Odstraňování obsahu:', items: [{ id: 'exp-o', label: 'Experimenty-o', template: 'Experimenty-o', summary: 'Varování: Experimenty-o' }, { id: 'exp2o', label: 'Experimenty2o', template: 'Experimenty2o', summary: 'Varování: Experimenty2o' }, { id: 'exp3o', label: 'Experimenty3o', template: 'Experimenty3o', summary: 'Varování: Experimenty3o', nopagetitle: true }] }, { category: 'Odstraňování urg. šablon:', items: [{ id: 'exp-u', label: 'Experimenty-u', template: 'Experimenty-u', summary: 'Varování: Experimenty-u', nopagetitle: true }, { id: 'exp2u', label: 'Experimenty2u', template: 'Experimenty2u', summary: 'Varování: Experimenty2u', nopagetitle: true }, { id: 'exp3u', label: 'Experimenty3u', template: 'Experimenty3u', summary: 'Varování: Experimenty3u', nopagetitle: true }] }, { category: 'Další varování:', items: [{ id: 'vulgarity', label: 'Vulgarity', template: 'Vulgarity', summary: 'Varování: Vulgarity' }, { id: 'kompov', label: 'KomentářPOV', template: 'KomentářPOV', summary: 'Varování: KomentářPOV' }, { id: 'nahled', label: 'Náhled', template: 'Náhled', summary: 'Varování: Náhled', nopagetitle: true }, { id: 'shrnuti', label: 'Shrnutí', template: 'Shrnutí', summary: 'Varování: Shrnutí', nopagetitle: true }, { id: 'welcome', label: 'Vítejte', template: 'Vítejte', summary: 'Vítejte na Wikipedii!', sectiontitle: 'Vítejte', nopagetitle: true }, { id: 'welcomeanon', label: 'Vítejte-anonym', template: 'Vítejte|a', summary: 'Vítejte na Wikipedii!', sectiontitle: 'Vítejte', nopagetitle: true }] }, { category: 'Po smazání stránky:', items: [{ id: 'exp0', label: 'Experimenty0', template: 'Experimenty0', summary: 'Varování: Experimenty0' }, { id: 'exp0B', label: 'Experimenty0B', template: 'Experimenty0B', summary: 'Varování: Experimenty0B' }, { id: 'expup', label: 'ExperimentyUP', template: 'ExperimentyUP', summary: 'Varování: ExperimentyUP' }, { id: 'nevyznamne', label: 'Nevýznamné', template: 'Nevýznamné', summary: 'Varování: Nevýznamné' }] }, { category: 'Odložené smazání:', items: [{ id: 'vyznamnostautor', label: 'Významnost', template: 'Významnost autor', summary: 'Varování: Významnost', requirePage: true }, { id: 'uoveritautor', label: 'Urgentně Ověřit', template: 'Urgentně ověřit autor', summary: 'Varování: Urgentně ověřit', requirePage: true }, { id: 'copyvioautor', label: 'Copyvio', template: 'Copyvio autor', summary: 'Varování: Copyvio', requirePage: true }, { id: 'subpahylautor', label: 'Subpahýl', template: 'Subpahýl autor', summary: 'Varování: Subpahýl', requirePage: true }, { id: 'reklamaautor', label: 'Reklama', template: 'Reklama autor', summary: 'Varování: Reklama', requirePage: true }, { id: 'prelozitautor', label: 'Přeložit', template: 'Přeložit autor', summary: 'Varování: Přeložit', requirePage: true }, { id: 'strojovyprekladautor', label: 'Stroj. překlad', template: 'Strojový překlad autor', summary: 'Varování: Strojový překlad', requirePage: true }] }]; // Funkce pro získání českého formátu "Měsíc Rok" (např. "Květen 2026") EW.getMonthYear = function() { const months = ['Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec']; const d = new Date(); return months[d.getMonth()].charAt(0).toUpperCase() + months[d.getMonth()].slice(1) + ' ' + d.getFullYear(); }; EW.setup = function() { const $toolbar = $('<div>', { id: 'ExperimentsWarning-toolbar', css: { margin: '10px 0', padding: '8px', border: '1px solid #a2a9b1', backgroundColor: '#f8f9fa', fontWeight: 'bold', borderRadius: '2px' } }); // Přidání nadpisu a legendy $toolbar.append($('<div>', { css: { borderBottom: '1px solid #a2a9b1', marginBottom: '10px', paddingBottom: '6px', color: '#202122' } }).html( '<span style="font-size: 1.1em; font-weight: bold;">Vložení varovných šablon</span>' + '<span style="margin-left: 15px; font-size: 0.85em; font-weight: normal; color: #54595d;">' + 'Legenda: <b>(S/K)</b> = stránka a/nebo komentář, <b>(K)</b> = pouze komentář | ' + '<span style="color: #d33; font-weight: bold;">Červené položky vyžadují název stránky</span>' + '</span>' )); warningGroups.forEach(group => { const $row = $('<div>', { css: { marginBottom: '4px' } }); // Název kategorie s fixní šířkou pro zarovnání sloupců (bez zalamování) $row.append($('<span>', { text: group.category, css: { display: 'inline-block', width: '230px', color: '#54595d', whiteSpace: 'nowrap' } })); group.items.forEach((warn, index) => { if (index > 0) $row.append(' | '); // Zkrácené popisky pro čistší vzhled const labelSK = warn.nopagetitle ? ' (K)' : ' (S/K)'; const titleSK = warn.nopagetitle ? 'Vložit s komentářem' : 'Vložit s názvem stránky a komentářem'; if (warn.requirePage) { // STAV 1: Šablony vyžadující stránku (červené, jeden odkaz) $row.append($('<a>', { href: 'javascript:void(0)', text: warn.label + labelSK, title: titleSK + ' (vyžaduje název stránky)', css: { color: '#d33' }, click: function() { EW.openDialog(warn); } })); } else { // STAV 2: Klasické modré tlačítko pro rychlou akci $row.append($('<a>', { href: 'javascript:void(0)', text: warn.label, title: `Vložit {{subst:${warn.template}}}`, click: function() { if (!confirm(`Opravdu chcete vložit šablonu {{${warn.template}}}?`)) return; $(this).text('Odesílám...'); EW.doWarn(warn, null, null); } })); // STAV 3: Šedé doplňkové tlačítko pro dialog $row.append($('<a>', { href: 'javascript:void(0)', text: labelSK, title: titleSK, css: { color: '#72777d', // Šedá pro nerušivý vzhled fontSize: '0.9em', marginLeft: '2px' }, click: function() { EW.openDialog(warn); } })); } }); $toolbar.append($row); }); // Zavěsíme na úplný začátek hlavního obsahu (zaručeně funguje i v action=edit) $('#bodyContent').prepend($toolbar); }; EW.doWarn = function(warn, pageName, comment) { let currentMonthYear = EW.getMonthYear(); let d = new Date(); let currentMonthNum = d.getMonth() + 1; // 1-12 let currentYear = d.getFullYear(); new mw.Api().get({ action: 'query', prop: 'revisions', rvprop: 'content|timestamp', titles: wgPageName, format: 'json' }).done(function(data) { let pages = data.query.pages; let pageId = Object.keys(pages)[0]; let oldText = ""; let baseTimestamp = ""; if (pageId !== "-1") { oldText = pages[pageId].revisions[0]['*']; baseTimestamp = pages[pageId].revisions[0].timestamp; } // Sestavení šablony a komentáře let templateCall = pageName ? `{{subst:${warn.template}|${pageName}}}` : `{{subst:${warn.template}}}`; let commentLine = comment ? `\n${comment}` : ''; let newWarning = `\n${templateCall}${commentLine} --~~~~`; let wikitextToSave = ""; let headingText = warn.sectiontitle || currentMonthYear; if (warn.sectiontitle) { // STAV 1: Pevný nadpis (např. Vítejte) - vložíme vždy nakonec s novým nadpisem let prefix = oldText.trim() === '' ? '' : '\n\n'; wikitextToSave = oldText + `${prefix}== ${headingText} ==\n` + newWarning; } else { // STAV 2: Kontrola existence nadpisu pro tento měsíc let monthHeaderRegex = new RegExp(`==\\s*${currentMonthYear}\\s*==`, 'i'); if (monthHeaderRegex.test(oldText)) { // Nadpis už existuje -> přidáme varování jen na úplný konec wikitextToSave = oldText + newWarning; } else { // Nadpis neexistuje. Hledáme první podpis tohoto měsíce! // Regulární výraz chytá data jako "4. 6. 2026", "4.6.2026", "04. 6. 2026" let sigRegex = new RegExp(`\\b\\d{1,2}\\.\\s*0?${currentMonthNum}\\.\\s*${currentYear}\\b`); let match = oldText.match(sigRegex); if (match) { // Našli jsme starší varování z tohoto měsíce bez nadpisu! let matchIndex = match.index; // Najdeme začátek tohoto bloku (nejbližší dvě odřádkování směrem nahoru) let blockStart = oldText.lastIndexOf('\n\n', matchIndex); if (blockStart === -1) { blockStart = 0; // Žádné odřádkování není, vložíme nadpis na úplný začátek } else { blockStart += 2; // Posuneme se za to odřádkování } // Rozřízneme starý text a vložíme nadpis let textBefore = oldText.substring(0, blockStart); let textAfter = oldText.substring(blockStart); let prefix = textBefore.trim() === '' ? '' : '\n\n'; let modifiedOldText = textBefore + `${prefix}== ${currentMonthYear} ==\n\n` + textAfter; // Výsledek: Upravená historie s nadpisem + naše nové varování na konci wikitextToSave = modifiedOldText + newWarning; } else { // Nenašli jsme ani varování z tohoto měsíce -> klasicky nakonec s nadpisem let prefix = oldText.trim() === '' ? '' : '\n\n'; wikitextToSave = oldText + `${prefix}== ${currentMonthYear} ==\n` + newWarning; } } } // Ochrana před editačním konfliktem (pokud stránka existuje) let editParams = { action: 'edit', title: wgPageName, text: wikitextToSave, summary: warn.summary + EW.summarySuffix, minor: false }; if (baseTimestamp !== "") { editParams.basetimestamp = baseTimestamp; editParams.starttimestamp = Math.round(new Date().getTime() / 1000); } // Odeslání na Wikipedii new mw.Api().postWithEditToken(editParams).done((result) => { if (result.edit && result.edit.result === 'Success') { location.reload(); } else { alert('Chyba při ukládání varování.'); } }).fail((code) => { if (code === 'editconflict') { alert('Editační konflikt! Někdo stránku mezitím upravil. Zkuste to znovu.'); location.reload(); } else { alert('Nastala chyba API: ' + code); } }); }); }; EW.openDialog = function(warn) { function WarnDialog(config) { WarnDialog.super.call(this, config); } OO.inheritClass(WarnDialog, OO.ui.ProcessDialog); WarnDialog.static.name = "WarnDialog"; WarnDialog.static.title = `Varování: ${warn.label}`; WarnDialog.static.actions = [{ label: 'Zrušit', flags: 'safe' }, { action: 'submit', label: 'Odeslat', flags: ['primary', 'progressive'] }]; WarnDialog.prototype.initialize = function() { WarnDialog.super.prototype.initialize.call(this); var rootPanel = new OO.ui.PanelLayout({ padded: true, expanded: false }); // Pole pro komentář (vždy přítomno) this.commentInput = new OO.ui.MultilineTextInputWidget({ rows: 3, placeholder: 'případný komentář, přidá se pod šablonu' }); // Pokud šablona podporuje stránky, přidáme i pole pro stránku if (!warn.nopagetitle) { this.pageInput = new OO.ui.TextInputWidget({ placeholder: 'Např. Praha (bez závorek [[ ]])' }); rootPanel.$element.append( new OO.ui.FieldLayout(this.pageInput, { label: `Název článku pro šablonu {{${warn.template}}}:`, align: 'top' }).$element ); } // Přidáme komentář rootPanel.$element.append( new OO.ui.FieldLayout(this.commentInput, { label: 'Komentář:', align: 'top' }).$element ); this.$body.append(rootPanel.$element); }; WarnDialog.prototype.getActionProcess = function(action) { var dialog = this; if (action === 'submit') { // Přečteme stránku jen pokud input existuje, jinak null var pageName = dialog.pageInput ? dialog.pageInput.getValue().trim() : null; var comment = dialog.commentInput.getValue().trim(); if (warn.requirePage && !pageName) { alert('U této šablony musíte uvést název stránky!'); return new OO.ui.Process(); } dialog.actions.setAbilities({ submit: false, cancel: false }); dialog.pushPending(); EW.doWarn(warn, pageName || null, comment || null); } return WarnDialog.super.prototype.getActionProcess.call(this, action); }; var dialog = new WarnDialog({ size: "medium" }); var windowManager = new OO.ui.WindowManager(); $(document.body).append(windowManager.$element); windowManager.addWindows([dialog]); windowManager.openWindow(dialog); }; // Načtení potřebných knihoven a spuštění mw.loader.using(['mediawiki.api', 'oojs-ui-core', 'oojs-ui-windows'], () => $(EW.setup)); }); // </nowiki> 5iwb7enc1xny298u8e36ruyomw60xka User:Iiirxs/Starter kit/Welcome friend 2 175861 745183 2026-06-04T14:09:59Z Iiirxs 49827 Initialised by StarterKit tool — ready for translation 745183 wikitext text/x-wiki <div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;"> <span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/> <span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/> <span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span> </div> <noinclude>[[Category:Starter kit templates]]</noinclude> 2gaz0l1265uxned195lmw3a6r0oocxa User talk:Supertian8 3 175862 745187 2026-06-04T14:55:13Z Supertian8 67751 Welcome message sent as part of [[The Userscript Tour]]. 745187 wikitext text/x-wiki == The Userscript Tour == <div style="padding: .2em; border: medium solid #44a;"> [[File:TUT ninja developer.png|right|220px|link=]] <span style="color: #59d; font-size: 180%;">'''Welcome to [[MediaWiki:The Userscript Tour|<span style="color: #59d;">The Userscript Tour</span>]]!'''</span> <br> <br> '''Hi mate!''' We're so delighted you wanted to play, to unveil the realm of userscripts. Trust us, you are sure to appreciate the power and flexibility of userscripts in no time! The Tour is divided into four missions, each with its own goals to help you implement userscripts with fundamental constructs incrementally. Together these ''recipes of development'' aid you build cool JavaScript programs to change the very appearance and behavior of your user account. :) {{:TUT/Navigation1}} </div> 3kt7g87eoaoxs1ekf5yyu81vtdmufsn User:Supertian8/common.js 2 175863 745188 2026-06-04T14:58:14Z Supertian8 67751 invert script 745188 javascript text/javascript mw.loader.load( 'https://test.wikipedia.org/w/index.php?title=User:Enterprisey/invert.js&action=raw&ctype=text/javascript' ); mn5mfkzyfyr2ff5dzzh6h6gk1c69qef 745191 745188 2026-06-04T15:03:03Z Supertian8 67751 zoomage 745191 javascript text/javascript mw.loader.load( 'https://test.wikipedia.org/w/index.php?title=User:Enterprisey/invert.js&action=raw&ctype=text/javascript' ); mw.loader.load( 'https://test.wikipedia.org/w/index.php?title=User:Supertian8/zoomThePage.js&action=raw&ctype=text/javascript' ); at6wk0eju42skjbj6q1mjfjw2imeubs 745192 745191 2026-06-04T15:05:19Z Supertian8 67751 dummy 745192 javascript text/javascript mw.loader.load( 'https://test.wikipedia.org/w/index.php?title=User:Enterprisey/invert.js&action=raw&ctype=text/javascript' ); mw.loader.load('https://test.wikipedia.org/w/index.php?title=User:Supertian8/zoomThePage.js&action=raw&ctype=text/javascript' ); b9ov4ryr0q84r8pg02twn2wasvfg5gk User:Supertian8/zoomThePage.js 2 175864 745189 2026-06-04T15:01:04Z Supertian8 67751 blank 745189 javascript text/javascript phoiac9h4m842xq45sp7s6u21eteeq1 745190 745189 2026-06-04T15:02:14Z Supertian8 67751 enfillen 745190 javascript text/javascript const btnZoomIn: HTMLElement = document.getElementById("zoomIn"); const btnZoomOut: HTMLElement = document.getElementById("zoomOut"); let zoomLevel = 0; btnZoomIn.addEventListener("click", () => { if (zoomLevel < 2) { zoomLevel = zoomLevel + 0.1; textContent.style.zoom = `${zoomLevel}`; } }); btnZoomOut.addEventListener("click", () => { if (zoomLevel > 1) { zoomLevel = zoomLevel - 0.1; textContent.style.zoom = `${zoomLevel}`; } }); cffcx3qmwxiwv5odhbv1gbnewc5mxta Khurramite sack of Makenyats Monastery 0 175865 745193 2026-06-04T15:58:17Z ~2026-33147-10 74293 Created page with "P" 745193 wikitext text/x-wiki P 9h1hr6bv3mfwt8eguupnhdxsgk17mbd Khurramite raids in Armenia 0 175866 745194 2026-06-04T15:59:22Z ~2026-33147-10 74293 Created page with "120.000 killed" 745194 wikitext text/x-wiki 120.000 killed 21zjl7c58xw3z498fwyx822avdtvgsq Movses Daskhurantsi considered him a "man-devouring, land-devastating, bloodthirsty beast." 0 175867 745195 2026-06-04T16:00:04Z ~2026-33147-10 74293 Created page with "Movses Daskhurantsi considered him a "man-devouring, land-devastating, bloodthirsty beast."" 745195 wikitext text/x-wiki Movses Daskhurantsi considered him a "man-devouring, land-devastating, bloodthirsty beast." ig14tcfljtke6vmhdgfzy3qu1jeekw6 User:SBassett (WMF)/Sandbox/Test 2 175868 745198 2026-06-04T17:26:03Z SBassett (WMF) 40986 test timeline 745198 wikitext text/x-wiki <timeline> ImageSize = width:600 height:160 PlotArea = left:50 right:30 top:30 bottom:30 DateFormat = yyyy Period = from:2020 till:2026 TimeAxis = orientation:horizontal ScaleMajor = unit:year increment:1 start:2020 Define $dx = 25 # text shift x Define $dy = -5 # text shift y PlotData= bar:Events width:20 color:blue from:2020 till:2021 text:"Project Launch" shift:($dx,$dy) from:2022 till:2023 text:"Major Update" shift:($dx,$dy) </timeline> dmcxq8woto8z3hgnwhhgyt5ml13z4ro 745199 745198 2026-06-04T17:26:34Z SBassett (WMF) 40986 test url 745199 wikitext text/x-wiki <timeline> ImageSize = width:600 height:160 PlotArea = left:50 right:30 top:30 bottom:30 DateFormat = yyyy Period = from:2020 till:2026 TimeAxis = orientation:horizontal ScaleMajor = unit:year increment:1 start:2020 Define $dx = 25 # text shift x Define $dy = -5 # text shift y PlotData= bar:Events width:20 color:blue from:2020 till:2021 text:"Project Launch" shift:($dx,$dy) from:2022 till:2023 text:"Major Update" shift:($dx,$dy) from:start till:2024 shift:($dx,15) text:Vladimir~Ilyich~[[http://localhost:8081/wiki/Special:MyLanguage/Lenin|Lenin]] </timeline> 1vx1k2c2ynnk51yf7b1qyfe2ib48fd1 745200 745199 2026-06-04T17:27:10Z SBassett (WMF) 40986 test w/ url 745200 wikitext text/x-wiki <timeline> ImageSize = width:600 height:160 PlotArea = left:50 right:30 top:30 bottom:30 DateFormat = yyyy Period = from:2020 till:2026 TimeAxis = orientation:horizontal ScaleMajor = unit:year increment:1 start:2020 Define $dx = 25 # text shift x Define $dy = -5 # text shift y PlotData= bar:Events width:20 color:blue from:2020 till:2021 text:"Project Launch" shift:($dx,$dy) from:2022 till:2023 text:"Major Update" shift:($dx,$dy) from:start till:2024 shift:($dx,15) text:Vladimir~Ilyich~[Special:MyLanguage/Lenin|Lenin] </timeline> m7gk2if00a751v6gairv5acwk2n5s7b 745201 745200 2026-06-04T17:27:22Z SBassett (WMF) 40986 test w/ url 745201 wikitext text/x-wiki <timeline> ImageSize = width:600 height:160 PlotArea = left:50 right:30 top:30 bottom:30 DateFormat = yyyy Period = from:2020 till:2026 TimeAxis = orientation:horizontal ScaleMajor = unit:year increment:1 start:2020 Define $dx = 25 # text shift x Define $dy = -5 # text shift y PlotData= bar:Events width:20 color:blue from:2020 till:2021 text:"Project Launch" shift:($dx,$dy) from:2022 till:2023 text:"Major Update" shift:($dx,$dy) from:start till:2024 shift:($dx,15) text:Vladimir~Ilyich~[[Special:MyLanguage/Lenin|Lenin]] </timeline> fj59rkfuw81tbi5mvwp21cccvd9spz2 745202 745201 2026-06-04T20:27:02Z SBassett (WMF) 40986 test abs 745202 wikitext text/x-wiki <timeline> ImageSize = width:600 height:160 PlotArea = left:50 right:30 top:30 bottom:30 DateFormat = yyyy Period = from:2020 till:2026 TimeAxis = orientation:horizontal ScaleMajor = unit:year increment:1 start:2020 Define $dx = 25 # text shift x Define $dy = -5 # text shift y PlotData= bar:Events width:20 color:blue from:2020 till:2021 text:"Project Launch" shift:($dx,$dy) from:2022 till:2023 text:"Major Update" shift:($dx,$dy) from:start till:2024 shift:($dx,15) text:Vladimir~Ilyich~[[https://test.wikipedia.org/wiki/Special:MyLanguage/Lenin|Lenin]] </timeline> eveaqgmvc79c1paq47240xk7ym018e2 Википедия:К удалению/4 июня 2026 0 175869 745205 2026-06-04T21:54:57Z Solidest 54422 [[Участник:Solidest/Remover|Remover]]: автоматическая шапка 745205 wikitext text/x-wiki {{КУ-Навигация}} 0i38svaw3w45s8vulws0qczofnxlk6l 745206 745205 2026-06-04T21:54:58Z Solidest 54422 [[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/4 июня 2026#Projectwiki]] 745206 wikitext text/x-wiki {{КУ-Навигация}} == [[:Projectwiki]] == [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 21:54, 4 June 2026 (UTC) gqruf3146gewvcgiflf4pr4unpbew5x Обсуждение:Projectwiki 0 175870 745207 2026-06-04T21:55:30Z Solidest 54422 [[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/4 июня 2026#Projectwiki]] — Снято с удаления 745207 wikitext text/x-wiki {{Снято с удаления|2026-06-04}} ohp3z9d4fa5b3esjwt6s1s57vl01g5y User:SherryYang-WMF/common.css 2 175871 745209 2026-06-04T22:05:14Z SherryYang-WMF 74266 testing whether Muhammad depictions show in image carousel if I suppress for my account 745209 css text/css .page-Muhammad .depiction {display: none;} 2fdyvgy6yenbsmp97ur20bu3our0sqn 745210 745209 2026-06-04T22:07:52Z SherryYang-WMF 74266 745210 css text/css .page-Muhammad .depiction {display: none;} .page-World .depiction {display: none;} ayqyiajs9iie5yodgqeking35rs8ed4 Template:Infobox settlement/doc 10 175872 745214 2026-06-04T23:45:28Z SSethi (WMF) 36407 Imported from https://test.wikipedia.org/wiki/User:Iiirxs/Template:Infobox_settlement/doc by StarterKit infobox tool (content under CC BY-SA) 745214 wikitext text/x-wiki == Usage == Use this template on articles about settlements (cities, towns, villages, etc.). All fields are optional — if omitted, values are pulled automatically from Wikidata. === Basic usage (Wikidata auto-fill) === <pre>{{Infobox settlement}}</pre> === With explicit values === <pre> {{Infobox settlement | name = Athens | image = Athens Montage.jpg | country = Greece | region = Attica | population = 3,153,000 | coordinates = 37°58′N 23°43′E }} </pre> == Parameters == {| class="wikitable" ! Parameter !! Wikidata property !! Description |- | <code>name</code> || — || Settlement name. Defaults to the page name. |- | <code>image</code> || P18 || Image filename (without <code>File:</code> prefix). Defaults to Wikidata P18. |- | <code>country</code> || P17 || Country the settlement is located in. Defaults to Wikidata P17. |- | <code>region</code> || P131 || Administrative territorial entity (region, prefecture, municipality, etc.). Defaults to Wikidata P131. |- | <code>population</code> || P1082 || Population figure. Defaults to Wikidata P1082. |- | <code>coordinates</code> || P625 || Geographic coordinates. Defaults to Wikidata P625. |} == Notes == * Requires the [[mw:Extension:Wikibase Client|WikibaseClient]] extension for Wikidata property lookup. * Requires the [[mw:Extension:Scribunto|Scribunto]] extension and [[Module:Infobox]] for rendering. <templatedata> { "description": "Infobox for settlements. All fields are optional and fall back to Wikidata properties automatically.", "params": { "name": { "label": "Name", "description": "Settlement name. Defaults to the page name.", "type": "string", "suggested": true }, "image": { "label": "Image", "description": "Image filename without File: prefix. Defaults to Wikidata P18.", "type": "wiki-file-name", "suggested": true }, "country": { "label": "Country", "description": "Country the settlement is in. Defaults to Wikidata P17.", "type": "string" }, "region": { "label": "Region", "description": "Administrative division (region, prefecture, etc.). Defaults to Wikidata P131.", "type": "string" }, "population": { "label": "Population", "description": "Population figure. Defaults to Wikidata P1082.", "type": "number" }, "coordinates": { "label": "Coordinates", "description": "Geographic coordinates. Defaults to Wikidata P625.", "type": "string" } } } </templatedata> d1hdr68wr32f6i5rcican2ykkovk7if User:Oshwah/NewUserPatrol-TEST.js 2 175873 745215 2026-06-05T04:41:24Z Oshwah 26741 Create copy of script to test. 745215 javascript text/javascript //<nowiki> var nup_http_request, nup_log_list_enabled, nup_log_list_number_of_logs, nup_log_list_refresh_rate; //Initialize script. function nup_init() { /*Add "nup_log_list_enabled = true;" to your common.js page to automatically enable this script on page load, which will begin querying the API for the newest log entries. The default setting if this option isn't added is "false", meaning that the script and querying feature will be disabled by default on page load; you'll just need to click on the "enable this box" link in order to turn the script on.*/ /*Add "nup_log_list_number_of_logs = **REPLACE THIS TEXT WITH A NUMBER**;" to your common.js page to set the number of newest log entries that the script will query and list. For example, adding "nup_log_list_number_of_logs = 5;" to your common.js page would set the script to query and list the newest 5 log entries that are returned. The default number if this option isn't added is 10. The maximum number of log entries that can be set is 50, and the minimum number that can be set is 1. If a number larger than 50 or less than 1 is set, the script will revert to using the maximum or minimum of 50 or 1, respectively.*/ /*Add "nup_log_list_refresh_rate = **REPLACE THIS TEXT WITH A NUMBER**;" to your common.js page to set the number of seconds that the script will wait before querying and updating the list of newest log entries. For example, adding "nup_log_list_refresh_rate = 15;" to your common.js page would set the script to wait for 15 seconds between querying and refreshing the list of newest log entries again. The default number if this option isn't added is 5. The minimum number that can be set is 2. If a number less than 2 is set, the script will revert to using the minimum of 2 seconds.*/ //Check if any user-defined settings are present (see above comments for instructions and info). If a setting isn't present, set the script to use the default setting instead (detailed in the comments above). if (nup_log_list_enabled === undefined) { nup_log_list_enabled = false; } if (nup_log_list_number_of_logs === undefined) { nup_log_list_number_of_logs = 10; } if (nup_log_list_refresh_rate === undefined) { nup_log_list_refresh_rate = 5; } /*Maximum and minimum limits are set on the "nup_log_list_number_of_logs" and "nup_log_list_refresh_rate" user-defined settings to prevent any user-defined settings from being allowed to flood the servers. Check if any of the user-defined settings are outside of the limits defined (see comments above for explanation and for those limits). If so, modify those settings to be within those limits.*/ if (nup_log_list_number_of_logs > 50) { nup_log_list_number_of_logs = 50; } if (nup_log_list_number_of_logs < 1) { nup_log_list_number_of_logs = 1; } if (nup_log_list_refresh_rate < 2) { nup_log_list_refresh_rate = 2; } //Retrieve our cookie. if (document.cookie.length > 0) { let c_start = document.cookie.indexOf("nup_show_box="); if (c_start != -1) { c_start = c_start + 13; let c_end = document.cookie.indexOf(";", c_start); if (c_end == -1) { c_end = document.cookie.length; } if (document.cookie.substring(c_start, c_end) == "yes") { nup_log_list_enabled = true; } else { nup_log_list_enabled = false; } } } /*Create an event listener to detect change in browser tab visibility. If the browser tab is switched and becomes inactive (such as the user switching to another browser tab), the automatic query and refresh of newest log entries are halted until the browser tab becomes active again. If the browser tab is switched and becomes active, a query and refresh is immediately performed and the automatic query and refresh of newest log entries are resumed. The script always remains set to enabled (if enabled, of course); only the automatic query and refresh of logs is halted on inactive tabs.*/ document.addEventListener('visibilitychange', function() { if (nup_log_list_enabled === true && !document.hidden) { window.console.log("[NEW-USER-PATROL]: Browser tab visibility change has been detected. Tab is now ACTIVE. Performing immediate refresh and resuming automatic refreshes..."); nup_send_api_request(); } else { window.console.log("[NEW-USER-PATROL]: Browser tab visibility change has been detected. Tab is now INACTIVE. Automatic refreshes are halted until the tab becomes active again."); } }); //If the script is currently enabled, make a new request and begin populating a list of newest log entries. Else, created a disabled box with a button to turn the script on. if (nup_log_list_enabled === true) { nup_send_api_request(); } else { nup_draw_disabled_box(); } } //Initialize AJAX function nup_create_ajax_request() { try { nup_http_request = new XMLHttpRequest(); } catch (e) { try { nup_http_request = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { nup_http_request = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { alert("Your browser does not support AJAX!"); return false; } } } nup_http_request.onreadystatechange = function() { if (nup_http_request.readyState == 4) { nup_ajax_response(); } }; return true; } //Check if the script is enabled and current browser tab is active. If so, create and send the API request to the server. function nup_send_api_request() { //Check if the script is enabled and the browser tab is currently active. if (nup_log_list_enabled === false || document.hidden) { window.console.log("[NEW-USER-PATROL]: The script is currently set to disabled or the browser tab is not currently active; skipping creation and sending of API request..."); return; } //Update the list header to add "(...)" to the end in order to inform the user that the script is currently querying the API and updating the list. let nup_cur_box = document.getElementById('p-newuserpatrolscript'); if (nup_cur_box !== null) { nup_cur_box.firstChild.firstChild.data = 'New users (...)'; } /*Call the request creation function. If it returns false, an error occurred due to an issue with AJAX. If a list of logs already exists (meaning that a query was successful previously), update the list header and inform the user that the request failed. Otherwise, if no list exists, this is the script's first attempt at performing an API request; send an alert to the user letting them know that an error occurred regarding AJAX creation.*/ if (nup_create_ajax_request() === false) { if (nup_cur_box !== null) { nup_cur_box.firstChild.firstChild.data = 'New users (Update FAILED!)'; } else { alert("Error with AJAX object creation."); } } //Set the necessary parameters and transmit the actual API request to the server. nup_http_request.open("GET", "/w/api.php?action=query&list=logevents&leaction=newusers/create&format=json&lelimit=" + nup_log_list_number_of_logs, true); nup_http_request.setRequestHeader("Api-User-Agent", "NewUserPatrolScript-TEST/1.0"); nup_http_request.send(null); } //We've received a response from the server from our API request. function nup_ajax_response() { //Parse the JSON data that was returned from the server and separate each item returned. let nup_response_data = JSON.parse(nup_http_request.responseText); let nup_response_items = nup_response_data.query.logevents; //Create the container div that will hold the entire script body and everything in it - the list title, the ul (list) of newest log entry nodes, the disable button. let nup_script_global_div = document.createElement('div'); nup_script_global_div.className = 'pBody'; //Create a ul (list) that will hold each li (list item) of newest log entry nodes. let nup_log_list_ul = document.createElement('ul'); nup_script_global_div.appendChild(nup_log_list_ul); /*This loop takes each item (log entry) retrieved from the API request, pulls the username of the account that triggered the creation of the current log entry, and uses substring to remove the "User:" portion from the beginning of the username string. It then creates a URL string for the contributions page of the current account and adds it to an href link with the account's username set as the text title. In the end, the loop populates the li list with a user-defined number of usernames that created the newest log entries retrieved via the API that are links to their contribution pages.*/ for (let i = 0; i < nup_response_items.length; i++) { //Pull the username of the account that created the current log entry, use substring to remove "User:" from the string, and create the URL to the current account's contributions page. let nup_log_entry_username = nup_response_items[i].title.substring(5); let nup_log_entry_url = 'https://test.wikipedia.org/wiki/Special:Contributions/' + encodeURIComponent(nup_log_entry_username); //Create HTML href link with the title set to the current account's username and URL to navigate to the current account's contributions page, and append it to the li list. let log_a_node = document.createElement('a'); log_a_node.style.fontSize = 'small'; log_a_node.setAttribute('href', nup_log_entry_url); log_a_node.appendChild(document.createTextNode(nup_log_entry_username)); //Create new list item, add the HTML link created above to the list item, then add the list item to the list. let log_li_node = document.createElement('li'); log_li_node.appendChild(log_a_node); nup_log_list_ul.appendChild(log_li_node); } //Create portal attribute div that will hold the entire script's elements. let nup_portal_pid_div = document.createElement('div'); nup_portal_pid_div.setAttribute('id', 'p-newuserpatrolscript'); nup_portal_pid_div.className = 'portal'; //Create a heading with the list's title and append it to the list, then append the div of links to the list. let nup_list_header_title = document.createElement('h6'); nup_list_header_title.appendChild(document.createTextNode('New users')); nup_portal_pid_div.appendChild(nup_list_header_title); nup_portal_pid_div.appendChild(nup_script_global_div); //Create a button to allow the user to disable the script. let toggle_p = document.createElement('p'); toggle_p.style.fontSize = 'x-small'; toggle_p.style.margin = '0px'; toggle_p.style.textAlign = 'right'; //Add the disable script button to the bottom of the list. let toggle_a = document.createElement('a'); toggle_a.appendChild(document.createTextNode('disable this box')); toggle_a.onclick = nup_disable_box; toggle_p.appendChild(toggle_a); nup_script_global_div.appendChild(toggle_p); /*Search for the "p-newuserpatrolscript" element (the old div list of links that we generated). If it is found, simply replace it with the new div of ul links that we just created. Otherwise, if none is found, it means that this is the first time that we've queried the API and constructed a list of ul links; locate the "p-tb" (toolbox) element and insert the new div list element to be displayed after it.*/ let nup_portal_old_div = document.getElementById('p-newuserpatrolscript'); let nup_panel_location = mw.config.get('skin') === 'vector-2022' ? document.getElementById('vector-page-tools') : document.getElementById('mw-panel'); if (nup_portal_old_div !== null) { nup_panel_location.replaceChild(nup_portal_pid_div, nup_portal_old_div); window.console.log("[NEW-USER-PATROL]: List updated!"); } else { let nup_node = document.getElementById('p-tb'); try { nup_panel_location.insertBefore(nup_portal_pid_div, nup_node.nextSibling); //This is effectively "insert after"... window.console.log("[NEW-USER-PATROL]: List inserted for the first time!"); } catch (e) { //NotFoundError - The current page doesn't feature described element that we're trying to insert the list after (new or different skin being used?). } } /*If the current browser tab is currently detected as active, schedule the next API request and list refresh to be executed at the set (either user-defined or default) time period. Otherwise, if the current browser tab is now detected as inactive, skip scheduling the next refresh execution. If the tab becomes active again, the event listener will detect the visibility change and call the appropriate refresh function then.*/ if (!document.hidden) { setTimeout(nup_send_api_request, nup_log_list_refresh_rate * 1000); } else { window.console.log("[NEW-USER-PATROL]: Current browser tab is no longer currently active; halted setting the timeout for next automatic list refresh..."); } } //If the user clicks on "disable this box", it halts any further API requests and list refreshing, and replaces the script div function nup_disable_box() { nup_log_list_enabled = false; window.console.log("[NEW-USER-PATROL]: Script disabled by user via nup_disable_box (\"disable this box\") button click."); nup_draw_disabled_box(); document.cookie = "nup_show_box=no; path=/"; } function nup_enable_box() { nup_log_list_enabled = true; window.console.log("[NEW-USER-PATROL]: Script enabled by user via nup_enable_box (\"enable this box\") button click. Performing immediate refresh..."); document.cookie = "nup_show_box=yes; path=/"; nup_send_api_request(); } function nup_draw_disabled_box() { //Create the container div that will hold the entire script body and everything in it - the list title and the enable button. let nup_script_global_div = document.createElement('div'); nup_script_global_div.className = 'pBody'; //Create portal attribute div that will hold the entire script's elements. let nup_portal_pid_div = document.createElement('div'); nup_portal_pid_div.setAttribute('id', 'p-newuserpatrolscript'); nup_portal_pid_div.className = 'portal'; //Create a heading with the list's title - state that it's currently disabled), and append it to the list. let nup_list_header_title = document.createElement('h6'); nup_list_header_title.appendChild(document.createTextNode('New users (Disabled)')); nup_portal_pid_div.appendChild(nup_list_header_title); nup_portal_pid_div.appendChild(nup_script_global_div); //Create a button to allow the user to enable the script. let toggle_p = document.createElement('p'); toggle_p.style.fontSize = 'x-small'; toggle_p.style.margin = '0px'; //Add the enable script button to the bottom of the list. let toggle_a = document.createElement('a'); toggle_a.appendChild(document.createTextNode('enable this box')); toggle_a.onclick = nup_enable_box; toggle_p.appendChild(toggle_a); nup_script_global_div.appendChild(toggle_p); /*Search for the "p-newuserpatrolscript" element (the old div list of links that we generated). If it is found, simply replace it with the disabled script element that we just created. Otherwise, if none is found, it means that we've never queried the API and constructed a list of newest log entries yet; locate the "p-tb" (toolbox) element and insert the disabled script element to be displayed after it.*/ let nup_portal_old_div = document.getElementById('p-newuserpatrolscript'); let nup_panel_location = ((mw.config.get('skin') === 'vector-2022') ? (document.getElementById('vector-page-tools')) : (document.getElementById('mw-panel'))); if (nup_portal_old_div !== null) { nup_panel_location.replaceChild(nup_portal_pid_div, nup_portal_old_div); } else { let nup_node = document.getElementById('p-tb'); try { nup_panel_location.insertBefore(nup_portal_pid_div, nup_node.nextSibling); //This is effectively "insert after"... } catch (e) { //NotFoundError - The current page doesn't feature described element that we're trying to insert the list after (new or different skin being used?). } } } $(nup_init); //</nowiki> 2bhgliyn5wwkdt7ct768z0zdzb4na4r 745217 745215 2026-06-05T04:43:27Z Oshwah 26741 Oshwah moved page [[User:Oshwah/NewUserPatrol.js]] to [[User:Oshwah/NewUserPatrol-TEST.js]] without leaving a redirect: Better title. 745215 javascript text/javascript //<nowiki> var nup_http_request, nup_log_list_enabled, nup_log_list_number_of_logs, nup_log_list_refresh_rate; //Initialize script. function nup_init() { /*Add "nup_log_list_enabled = true;" to your common.js page to automatically enable this script on page load, which will begin querying the API for the newest log entries. The default setting if this option isn't added is "false", meaning that the script and querying feature will be disabled by default on page load; you'll just need to click on the "enable this box" link in order to turn the script on.*/ /*Add "nup_log_list_number_of_logs = **REPLACE THIS TEXT WITH A NUMBER**;" to your common.js page to set the number of newest log entries that the script will query and list. For example, adding "nup_log_list_number_of_logs = 5;" to your common.js page would set the script to query and list the newest 5 log entries that are returned. The default number if this option isn't added is 10. The maximum number of log entries that can be set is 50, and the minimum number that can be set is 1. If a number larger than 50 or less than 1 is set, the script will revert to using the maximum or minimum of 50 or 1, respectively.*/ /*Add "nup_log_list_refresh_rate = **REPLACE THIS TEXT WITH A NUMBER**;" to your common.js page to set the number of seconds that the script will wait before querying and updating the list of newest log entries. For example, adding "nup_log_list_refresh_rate = 15;" to your common.js page would set the script to wait for 15 seconds between querying and refreshing the list of newest log entries again. The default number if this option isn't added is 5. The minimum number that can be set is 2. If a number less than 2 is set, the script will revert to using the minimum of 2 seconds.*/ //Check if any user-defined settings are present (see above comments for instructions and info). If a setting isn't present, set the script to use the default setting instead (detailed in the comments above). if (nup_log_list_enabled === undefined) { nup_log_list_enabled = false; } if (nup_log_list_number_of_logs === undefined) { nup_log_list_number_of_logs = 10; } if (nup_log_list_refresh_rate === undefined) { nup_log_list_refresh_rate = 5; } /*Maximum and minimum limits are set on the "nup_log_list_number_of_logs" and "nup_log_list_refresh_rate" user-defined settings to prevent any user-defined settings from being allowed to flood the servers. Check if any of the user-defined settings are outside of the limits defined (see comments above for explanation and for those limits). If so, modify those settings to be within those limits.*/ if (nup_log_list_number_of_logs > 50) { nup_log_list_number_of_logs = 50; } if (nup_log_list_number_of_logs < 1) { nup_log_list_number_of_logs = 1; } if (nup_log_list_refresh_rate < 2) { nup_log_list_refresh_rate = 2; } //Retrieve our cookie. if (document.cookie.length > 0) { let c_start = document.cookie.indexOf("nup_show_box="); if (c_start != -1) { c_start = c_start + 13; let c_end = document.cookie.indexOf(";", c_start); if (c_end == -1) { c_end = document.cookie.length; } if (document.cookie.substring(c_start, c_end) == "yes") { nup_log_list_enabled = true; } else { nup_log_list_enabled = false; } } } /*Create an event listener to detect change in browser tab visibility. If the browser tab is switched and becomes inactive (such as the user switching to another browser tab), the automatic query and refresh of newest log entries are halted until the browser tab becomes active again. If the browser tab is switched and becomes active, a query and refresh is immediately performed and the automatic query and refresh of newest log entries are resumed. The script always remains set to enabled (if enabled, of course); only the automatic query and refresh of logs is halted on inactive tabs.*/ document.addEventListener('visibilitychange', function() { if (nup_log_list_enabled === true && !document.hidden) { window.console.log("[NEW-USER-PATROL]: Browser tab visibility change has been detected. Tab is now ACTIVE. Performing immediate refresh and resuming automatic refreshes..."); nup_send_api_request(); } else { window.console.log("[NEW-USER-PATROL]: Browser tab visibility change has been detected. Tab is now INACTIVE. Automatic refreshes are halted until the tab becomes active again."); } }); //If the script is currently enabled, make a new request and begin populating a list of newest log entries. Else, created a disabled box with a button to turn the script on. if (nup_log_list_enabled === true) { nup_send_api_request(); } else { nup_draw_disabled_box(); } } //Initialize AJAX function nup_create_ajax_request() { try { nup_http_request = new XMLHttpRequest(); } catch (e) { try { nup_http_request = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { nup_http_request = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { alert("Your browser does not support AJAX!"); return false; } } } nup_http_request.onreadystatechange = function() { if (nup_http_request.readyState == 4) { nup_ajax_response(); } }; return true; } //Check if the script is enabled and current browser tab is active. If so, create and send the API request to the server. function nup_send_api_request() { //Check if the script is enabled and the browser tab is currently active. if (nup_log_list_enabled === false || document.hidden) { window.console.log("[NEW-USER-PATROL]: The script is currently set to disabled or the browser tab is not currently active; skipping creation and sending of API request..."); return; } //Update the list header to add "(...)" to the end in order to inform the user that the script is currently querying the API and updating the list. let nup_cur_box = document.getElementById('p-newuserpatrolscript'); if (nup_cur_box !== null) { nup_cur_box.firstChild.firstChild.data = 'New users (...)'; } /*Call the request creation function. If it returns false, an error occurred due to an issue with AJAX. If a list of logs already exists (meaning that a query was successful previously), update the list header and inform the user that the request failed. Otherwise, if no list exists, this is the script's first attempt at performing an API request; send an alert to the user letting them know that an error occurred regarding AJAX creation.*/ if (nup_create_ajax_request() === false) { if (nup_cur_box !== null) { nup_cur_box.firstChild.firstChild.data = 'New users (Update FAILED!)'; } else { alert("Error with AJAX object creation."); } } //Set the necessary parameters and transmit the actual API request to the server. nup_http_request.open("GET", "/w/api.php?action=query&list=logevents&leaction=newusers/create&format=json&lelimit=" + nup_log_list_number_of_logs, true); nup_http_request.setRequestHeader("Api-User-Agent", "NewUserPatrolScript-TEST/1.0"); nup_http_request.send(null); } //We've received a response from the server from our API request. function nup_ajax_response() { //Parse the JSON data that was returned from the server and separate each item returned. let nup_response_data = JSON.parse(nup_http_request.responseText); let nup_response_items = nup_response_data.query.logevents; //Create the container div that will hold the entire script body and everything in it - the list title, the ul (list) of newest log entry nodes, the disable button. let nup_script_global_div = document.createElement('div'); nup_script_global_div.className = 'pBody'; //Create a ul (list) that will hold each li (list item) of newest log entry nodes. let nup_log_list_ul = document.createElement('ul'); nup_script_global_div.appendChild(nup_log_list_ul); /*This loop takes each item (log entry) retrieved from the API request, pulls the username of the account that triggered the creation of the current log entry, and uses substring to remove the "User:" portion from the beginning of the username string. It then creates a URL string for the contributions page of the current account and adds it to an href link with the account's username set as the text title. In the end, the loop populates the li list with a user-defined number of usernames that created the newest log entries retrieved via the API that are links to their contribution pages.*/ for (let i = 0; i < nup_response_items.length; i++) { //Pull the username of the account that created the current log entry, use substring to remove "User:" from the string, and create the URL to the current account's contributions page. let nup_log_entry_username = nup_response_items[i].title.substring(5); let nup_log_entry_url = 'https://test.wikipedia.org/wiki/Special:Contributions/' + encodeURIComponent(nup_log_entry_username); //Create HTML href link with the title set to the current account's username and URL to navigate to the current account's contributions page, and append it to the li list. let log_a_node = document.createElement('a'); log_a_node.style.fontSize = 'small'; log_a_node.setAttribute('href', nup_log_entry_url); log_a_node.appendChild(document.createTextNode(nup_log_entry_username)); //Create new list item, add the HTML link created above to the list item, then add the list item to the list. let log_li_node = document.createElement('li'); log_li_node.appendChild(log_a_node); nup_log_list_ul.appendChild(log_li_node); } //Create portal attribute div that will hold the entire script's elements. let nup_portal_pid_div = document.createElement('div'); nup_portal_pid_div.setAttribute('id', 'p-newuserpatrolscript'); nup_portal_pid_div.className = 'portal'; //Create a heading with the list's title and append it to the list, then append the div of links to the list. let nup_list_header_title = document.createElement('h6'); nup_list_header_title.appendChild(document.createTextNode('New users')); nup_portal_pid_div.appendChild(nup_list_header_title); nup_portal_pid_div.appendChild(nup_script_global_div); //Create a button to allow the user to disable the script. let toggle_p = document.createElement('p'); toggle_p.style.fontSize = 'x-small'; toggle_p.style.margin = '0px'; toggle_p.style.textAlign = 'right'; //Add the disable script button to the bottom of the list. let toggle_a = document.createElement('a'); toggle_a.appendChild(document.createTextNode('disable this box')); toggle_a.onclick = nup_disable_box; toggle_p.appendChild(toggle_a); nup_script_global_div.appendChild(toggle_p); /*Search for the "p-newuserpatrolscript" element (the old div list of links that we generated). If it is found, simply replace it with the new div of ul links that we just created. Otherwise, if none is found, it means that this is the first time that we've queried the API and constructed a list of ul links; locate the "p-tb" (toolbox) element and insert the new div list element to be displayed after it.*/ let nup_portal_old_div = document.getElementById('p-newuserpatrolscript'); let nup_panel_location = mw.config.get('skin') === 'vector-2022' ? document.getElementById('vector-page-tools') : document.getElementById('mw-panel'); if (nup_portal_old_div !== null) { nup_panel_location.replaceChild(nup_portal_pid_div, nup_portal_old_div); window.console.log("[NEW-USER-PATROL]: List updated!"); } else { let nup_node = document.getElementById('p-tb'); try { nup_panel_location.insertBefore(nup_portal_pid_div, nup_node.nextSibling); //This is effectively "insert after"... window.console.log("[NEW-USER-PATROL]: List inserted for the first time!"); } catch (e) { //NotFoundError - The current page doesn't feature described element that we're trying to insert the list after (new or different skin being used?). } } /*If the current browser tab is currently detected as active, schedule the next API request and list refresh to be executed at the set (either user-defined or default) time period. Otherwise, if the current browser tab is now detected as inactive, skip scheduling the next refresh execution. If the tab becomes active again, the event listener will detect the visibility change and call the appropriate refresh function then.*/ if (!document.hidden) { setTimeout(nup_send_api_request, nup_log_list_refresh_rate * 1000); } else { window.console.log("[NEW-USER-PATROL]: Current browser tab is no longer currently active; halted setting the timeout for next automatic list refresh..."); } } //If the user clicks on "disable this box", it halts any further API requests and list refreshing, and replaces the script div function nup_disable_box() { nup_log_list_enabled = false; window.console.log("[NEW-USER-PATROL]: Script disabled by user via nup_disable_box (\"disable this box\") button click."); nup_draw_disabled_box(); document.cookie = "nup_show_box=no; path=/"; } function nup_enable_box() { nup_log_list_enabled = true; window.console.log("[NEW-USER-PATROL]: Script enabled by user via nup_enable_box (\"enable this box\") button click. Performing immediate refresh..."); document.cookie = "nup_show_box=yes; path=/"; nup_send_api_request(); } function nup_draw_disabled_box() { //Create the container div that will hold the entire script body and everything in it - the list title and the enable button. let nup_script_global_div = document.createElement('div'); nup_script_global_div.className = 'pBody'; //Create portal attribute div that will hold the entire script's elements. let nup_portal_pid_div = document.createElement('div'); nup_portal_pid_div.setAttribute('id', 'p-newuserpatrolscript'); nup_portal_pid_div.className = 'portal'; //Create a heading with the list's title - state that it's currently disabled), and append it to the list. let nup_list_header_title = document.createElement('h6'); nup_list_header_title.appendChild(document.createTextNode('New users (Disabled)')); nup_portal_pid_div.appendChild(nup_list_header_title); nup_portal_pid_div.appendChild(nup_script_global_div); //Create a button to allow the user to enable the script. let toggle_p = document.createElement('p'); toggle_p.style.fontSize = 'x-small'; toggle_p.style.margin = '0px'; //Add the enable script button to the bottom of the list. let toggle_a = document.createElement('a'); toggle_a.appendChild(document.createTextNode('enable this box')); toggle_a.onclick = nup_enable_box; toggle_p.appendChild(toggle_a); nup_script_global_div.appendChild(toggle_p); /*Search for the "p-newuserpatrolscript" element (the old div list of links that we generated). If it is found, simply replace it with the disabled script element that we just created. Otherwise, if none is found, it means that we've never queried the API and constructed a list of newest log entries yet; locate the "p-tb" (toolbox) element and insert the disabled script element to be displayed after it.*/ let nup_portal_old_div = document.getElementById('p-newuserpatrolscript'); let nup_panel_location = ((mw.config.get('skin') === 'vector-2022') ? (document.getElementById('vector-page-tools')) : (document.getElementById('mw-panel'))); if (nup_portal_old_div !== null) { nup_panel_location.replaceChild(nup_portal_pid_div, nup_portal_old_div); } else { let nup_node = document.getElementById('p-tb'); try { nup_panel_location.insertBefore(nup_portal_pid_div, nup_node.nextSibling); //This is effectively "insert after"... } catch (e) { //NotFoundError - The current page doesn't feature described element that we're trying to insert the list after (new or different skin being used?). } } } $(nup_init); //</nowiki> 2bhgliyn5wwkdt7ct768z0zdzb4na4r User:Oshwah/common.css 2 175874 745222 2026-06-05T06:30:50Z Oshwah 26741 Copy common.css from en-wp to see their effects here. 745222 css text/css /* Add access key labels */ a[accesskey]:before { content: " " attr(accesskey) " "; text-transform: uppercase; white-space: pre; font-family: sans-serif; margin-right: 0.5ex; } /* Changes the color used in the admin highlighter script to make ArbCom members orange instead of dark gray. */ .userhighlighter_arbcom {background-color: #FF9933 !important}; /* #54595D = Grey (strike-through line) #3366CC = Blue (enabled link) #72777D = Grey (placeholder text, e.g. "Username or IP removed") */ /* Revdel'd text link */ span.history-deleted a { text-style: italic; } /* Revdel'd strike-through text (over timestamp of revision) */ span.history-deleted.mw-changeslist-date { /*text-decoration: line-through;*/ color: #3366CC; } /* Revdel'd username or IP "(Username or IP removed)" */ span.history-deleted.mw-userlink { /*text-decoration: line-through;*/ color: #72777D; } /* Revdel'd edit summary "(edit summary removed)" */ span.history-deleted.comment { /*text-decoration: line-through;*/ color: #72777D; } /* Suppressed revision timestamp text link */ span.mw-history-suppressed a { /*text-style: italic;*/ color: #3366CC; } /* Suppressed strike-through text (over timestamp of revision) */ span.history-deleted.mw-history-suppressed.mw-changeslist-date { /*text-decoration-style: double;*/ color: #3366CC; } /* Suppressed username or IP "(Username or IP removed) */ span.history-deleted.mw-history-suppressed.mw-userlink { /*text-decoration-style: double;*/ color: #72777D; } /* Suppressed edit summary "(edit summary removed) */ span.history-deleted.mw-history-suppressed.comment { /*text-decoration-style: double;*/ color: #72777D; } /* Suppressed revision timestamp text (on Special:Undelete) */ /* NOTE: THIS MAY OVERRULE ABOVE SUPPRESSED CSS */ span.history-deleted.mw-history-suppressed { /*text-style: italic;*/ color: #3366CC; } /* Suppressed revision timestamp text (visited) */ /* NOTE: THIS MAY OVERRULE ABOVE SUPPRESSED CSS */ span.history-deleted.mw-history-suppressed a:visited { color: #0B0080; } /* Expired temporary accounts */ a.mw-tempuserlink-expired { text-decoration: none; /* Removes the strikethrough text, which can be confused for the account being blocked. */ /*font-style: italic;*/ color: #FF0000; } /* Expired temporary accounts (while mouse is hovering) */ a.mw-tempuserlink-expired:hover { text-decoration: underline; /* Removes the strikethrough text (but keeps the underlined text decoration), which can be confused for the account being blocked. */ text-decoration-thickness: 0.075em; } /* Expands height of edit window to be larger */ textarea { height: 600px; } ibygz6ao1ojx2f0gvzs649sn6ftktdc