Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.47.0-wmf.6
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
Wikipedia:Village pump
4
35019
746729
743881
2026-06-15T03:56:16Z
Pine
41485
/* June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project */ new section
746729
wikitext
text/x-wiki
{{mbox|type=style|text=<p>Before posting here, remember that [[Wikipedia:What Test Wiki is not|Test Wikipedia is not a community]]. The idea of "community consensus" is therefore meaningless.<p>If you would like to start a discussion relating to this wiki, consider posting to the [[mail:wikitech-l|wikitech-l]] mailing list.}}
'''This is the test wiki for MediaWiki developers. It is not intended for research or other serious use of content. If you were trying to reach the encylopedic content, please use the [[:en:|English Wikipedia]].'''
'''Ce wiki est un wiki de test pour les développeurs MediaWiki. Il ne sert pas à la recherche ni à d'autres fins officielles de son contenu. Si vous recherchez le contenu encyclopédique, il se trouve sur la [[:fr:|Wikipedia française]].'''
'''미디어위키 개발자를 위한 테스트 위키입니다. 이는 내용의 연구나 기타 진지하게 사용하기 위한 것이 아닙니다. 백과사전적인 내용을 찾으려면, [[:ko:|한국어 위키백과를]] 이용하세요.'''
'''Este é a wiki de teste para desenvolvedores do MediaWiki. Não se destina a pesquisa ou outro uso sério de conteúdo. Se você estava tentando acessar o conteúdo da enciclopédia, use a [[:pt:|Wikipédia em português]].'''
'''Це тестова вікі для розробників MediaWiki. Цей сайт не призначений для якихось досліджень або наповнення контентом. Вікіпедія знаходиться [[:uk:|тут]].'''
'''Sta cuà ła xé ła Wiki de prova par i desviłupadori de MediaWiki. Sta Wiki no ła xé intendesta par ła riserca o altre doparasion da contegnui ençiclopèdeghi. Se te jeri drio intentar de catar fora contegnui da ençiclopedia, varda [[:vec:|Wikipèdia in Vèneto]].'''
'''这是一个供MediaWiki开发者测试的维基站点,请不要在此放置严肃的内容页面或进行研究。请访问[[:zh:|中文维基百科]]以访问百科全书内容。'''
There is an [[/Archive|archive]].
{{Wikipedia:Village pump/topic list}}
__TOC____NEWSECTIONLINK__<!--
####################################################
########## PLEASE LEAVE THE ABOVE LINES ##########
####################################################-->
==Script==
Hello, can someone please make edits that were blocked by an edit filter, see [https://test.wikipedia.org/w/index.php?title=Special:AbuseLog&wpSearchUser=LuniZunie], we are testing warning and reporting and do not want to report or revert the wrong user on normal Wikipedia and blow it up. Also are there autoconfirmed users? [[User:Pro-anti-air|Pro-anti-air]] ([[User talk:Pro-anti-air|talk]]) 03:50, 9 November 2025 (UTC)
:I don't know what you want me to do exactly, as I cannot edits this page, import to this title, or move a page to this title. I have made [[User:Koavf/scripts/WikiShield.js]] if that helps. Ping me if you want me to do something. —[[User:Koavf|Justin (<span style="color:grey">ko'''a'''vf</span>)]]<span style="color:red">❤[[User talk:Koavf|T]]☮[[Special:Contributions/Koavf|C]]☺[[Special:Emailuser/Koavf|M]]☯</span> 04:03, 9 November 2025 (UTC)
:It's a global filter, pinging @[[User:Codename Noreste|Codename Noreste]]. [[User:Tanbiruzzaman|Tanbiruzzaman]] ([[User talk:Tanbiruzzaman|talk]]) 04:05, 9 November 2025 (UTC)
:@[[User:Pro-anti-air|Pro-anti-air]], to bypass the filter you need to make 2+ edit in other page. [[User:Tanbiruzzaman|Tanbiruzzaman]] ([[User talk:Tanbiruzzaman|talk]]) 04:07, 9 November 2025 (UTC)
::I'm confused, what rights do you get from editing more? Isn't autoconfirmed just 4 days? [[User:Pro-anti-air|Pro-anti-air]] ([[User talk:Pro-anti-air|talk]]) 04:10, 9 November 2025 (UTC)
: [[User:Koavf|Koavf]], [[User:Pro-anti-air|Pro-anti-air]], and [[User:Tanbiruzzaman|Tanbiruzzaman]]: I adjusted the global filter so that such false positives do not happen again. [[User:Codename Noreste|Codename Noreste]] ([[User talk:Codename Noreste|talk]]) 04:21, 9 November 2025 (UTC)
::Thank you! [[User:Pro-anti-air|Pro-anti-air]] ([[User talk:Pro-anti-air|talk]]) 04:26, 9 November 2025 (UTC)
:: @[[User:Codename Noreste|Codename Noreste]] Thank you so much!! [[User:LuniZunie|LuniZunie]] ([[User talk:LuniZunie|talk]]) 16:47, 9 November 2025 (UTC)
== Report concerning [[Special:Contributions/Tanbiruzzammn|Tanbiruzzammn]] ==
* {{vandal|Tanbiruzzammn}}
Vandalism. Long-term abuse. impersonation of User:Tanbiruzzaman[[User:TenWhile6| ]]<small>[[:m:Special:MyLanguage/User:TenWhile6/XReport|XReport]]</small> --[[User:MathXplore|MathXplore]] ([[User talk:MathXplore|talk]]) 13:44, 9 December 2025 (UTC)
:Account is locked. I nuked the pages. -[[User:Barras|<span style="color:blue; font-family:Bookman Old Style">'''Barras'''</span>]] [[User Talk:Barras|<span style="color:red; font-family:Bookman Old Style">'''talk'''</span>]] 21:45, 9 December 2025 (UTC)
== Report concerning [[Special:Contributions/Bucheon|Bucheon]] ==
* {{vandal|Bucheon}}
Long-term abuse[[User:TenWhile6| ]]<small>[[:m:Special:MyLanguage/User:TenWhile6/XReport|XReport]]</small> --[[User:PieWriter|PieWriter]] ([[User talk:PieWriter|talk]]) 10:25, 19 February 2026 (UTC)
== Versions and dates ==
On the [[Special:Version]] page, I see:
{| class="wikitable"
!Product
!Version
|-
|[https://www.mediawiki.org/ MediaWiki]
|[[mw:MediaWiki_1.46/wmf.17|1.46.0-wmf.17]] [[git:mediawiki/core/+/8644e6d2bb489940eb0028286386746dd40a5711|(8644e6d)]]
00:34, 3 March 2026
|}
which suggests that version .17 was released today, March 3rd.
However, the [[mw:MediaWiki_1.46/Roadmap|Roadmap]] says that version .18 should be released today instead.
Are these values out of synch or am I reading this wrong? [[Special:Contributions/~2026-13668-13|~2026-13668-13]] ([[User talk:~2026-13668-13|talk]]) 03:38, 3 March 2026 (UTC)
:I now see version .18 listed in that table:
:{| class="wikitable"
!Product
!Version
|-
|[https://www.mediawiki.org/ MediaWiki]
|[[mw:MediaWiki_1.46/wmf.18|1.46.0-wmf.18]] [[git:mediawiki/core/+/9784b5fa1e290e0ff423b8c68c1d061e910f9075|(9784b5f)]]
02:08, 3 March 2026
|}
:(And I've worked out how to put tables in replies.) [[Special:Contributions/~2026-13668-13|~2026-13668-13]] ([[User talk:~2026-13668-13|talk]]) 06:17, 3 March 2026 (UTC)
== Upcoming Wikimedia Café meetup regarding the [[:meta:Wikimedia Foundation Annual Plan/2026-2027|the 2026-2027 Wikimedia Foundation Annual Plan]] ==
{{tmbox
| image = [[File:Wikimedia Café logo in plain SVG format.svg|45px]]
| type=notice
| text = Hello! There will be a '''[[:meta:Wikimedia Café|Wikimedia Café]]''' meetup on '''Saturday, 11 April 2026 at 14:00 UTC''' ([https://zonestamp.toolforge.org/1775916000 timestamp conversion tool]), focusing on the [[:meta:Wikimedia Foundation Annual Plan/2026-2027|the 2026-2027 Wikimedia Foundation Annual Plan]]. The featured guests will be {{Noping|KStineRowe (WMF)|label1=Kelsi Stine-Rowe}} (senior manager, [[:meta:Movement Communications|Movement Communications]], Wikimedia Foundation), and {{Noping|Samwalton9 (WMF)|label1=Sam Walton}} (senior product manager, [[:mw:Moderator Tools|Moderator Tools]], Wikimedia Foundation). <br />
In addition to this Café session, [[:meta:Wikimedia Foundation Annual Plan/2026-2027/Collaboration|several additional meetings regarding the Annual Plan are listed on the Collaboration page]], and you may participate on the [[:meta:Talk:Wikimedia Foundation Annual Plan/2026-2027|talk page]]. <br />
This Café meetup will be approximately two hours long. Attendees may choose to attend only for a part. Please see the Café page for more information, including [[:meta:Wikimedia Café#Signups for the April 2026 session|how to register]]. <br />
[[File:Buntstifte Eberhard Faber crop 64h.jpg|860px|alt=cropped image of colored pencils]]
}}
<span style="white-space:nowrap;">[[User:Pine|<span style="color:#01796f; text-shadow:#00BFFF 0 0 1.0em">↠Pine</span>]] [[User talk:Pine|<span style="color:DeepSkyBlue">(<b style="color:#FFDF00;text-shadow:#FFDF00 0 0 1.0em">✉</b>)</span>]]</span> 03:46, 30 March 2026 (UTC)
== Changes to electionadmin userright ==
I'm [[phab:T423962|proposing]] that the following changes to the existing implementation of the <code>electionadmin</code> and <code>sysop</code> be made:
* Remove the <code>securepoll-create-poll</code> right from admins
* Rename <code>election-admin</code> to <code>electionclerk</code> (to match how the userright is used across global wikis)
* Give sysops the ability to grant <code>electionclerk</code> (again reflects, fawiki, zhwiki, and enwiki where the [[mw:Extension:SecurePoll|SecurePoll]] extension is deployed)
If folks have concerns about these changes, feel free to voice the concerns here or on the phab ticket :) [[User:Sohom Datta|Sohom Datta]] ([[User talk:Sohom Datta|talk]]) 03:54, 21 April 2026 (UTC)
:+1 :) <span style="display:inline-block;">[[User:HouseBlaster|House]][[Special:Contribs/HouseBlaster|'''Blaster''']] ([[User talk:HouseBlaster|talk]] • he/they)</span> 05:13, 21 April 2026 (UTC)
:+1 since this is aligning with how securepoll is being deployed to the other projects. [[User:Robertsky|Robertsky]] ([[User talk:Robertsky|talk]]) 05:40, 21 April 2026 (UTC)
: +1 for easier testing, to align with other projects. [[User:Chaotic Enby|Chaotic Enby]] ([[User talk:Chaotic Enby|talk]]) 15:22, 23 April 2026 (UTC)
== Report concerning [[Special:Contributions/Princebarackalibarrydaddyobamaiii|Princebarackalibarrydaddyobamaiii]] ==
* {{vandal|Princebarackalibarrydaddyobamaiii}}
Vandalism[[User:TenWhile6| ]]<small>[[:m:Special:MyLanguage/User:TenWhile6/XReport|XReport]]</small> --[[User:MathXplore|MathXplore]] ([[User talk:MathXplore|talk]]) 12:23, 18 May 2026 (UTC)
== May 2026 Wikimedia Café meetups regarding the Wikimedia Foundation Annual Plan ==
<div class="border-box" style="background-color: var(--background-color-warning-subtle, #f8eaba); max-width: 875px; padding: 5px; border: 1px solid black; margin: 5px; color: var(--clr-dark)">
<div class="box" style="float:left; padding-top: 15px; padding-right: 15px;">[[File:Wikimedia Café logo in plain SVG format.svg|75px|alt=The logo for the Wikimedia Café]]</div>
Hello! There will be two '''[https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9 Wikimedia Café]''' discussion opportunities during the last weekend of May. Both sessions will focus on the [https://meta.wikimedia.org/wiki/Wikimedia_Foundation_Annual_Plan/2026-2027 the 2026-2027 Wikimedia Foundation Annual Plan]. Participants may attend either or both sessions.
#'''Saturday, 30 May 2026 at 15:00 UTC''' ([https://zonestamp.toolforge.org/1780153200 timestamp converter]), at a time friendly to the Americas, Africa, and Europe
#'''Sunday, 31 May 2026 at 05:00 UTC''' ([https://zonestamp.toolforge.org/1780203600 timestamp converter]), at a time friendly to Asia and the Pacific
Café participants are highly encouraged to read in advance [https://en.wikipedia.org/wiki/User:Sohom_Datta/annual_plan_guide at least this summary of the plan]. Optionally, Café participants are encouraged to read portions of the plan that interest them and [https://meta.wikimedia.org/wiki/Talk:Wikimedia_Foundation_Annual_Plan/2026-2027 ask questions or provide feedback on the Annual Plan talk page].
Please see the Café page for more information, including [https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9#May_2026_meetings_with_a_focus_on_Wikimedia_Foundation_Annual_Plan/2026-2027 tables of timestamp conversions for both sessions], [https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9#Agenda._This_will_be_an_approximately_1_hour_Caf%C3%A9_session,_and_is_extendible_for_an_additional_30_minutes_if_needed. the agenda], and [https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9#How_to_attend_the_session how to register]!
<br />
[[File:Buntstifte Eberhard Faber crop 64h.jpg|860px|alt=cropped image of colored pencils]]</div>
<span style="white-space:nowrap;">[[User:Pine|<span style="color:#01796f; text-shadow:#00BFFF 0 0 1.0em">↠Pine</span>]] [[User talk:Pine|<span style="color:DeepSkyBlue">(<b style="color:#FFDF00;text-shadow:#FFDF00 0 0 1.0em">✉</b>)</span>]]</span> 19:54, 21 May 2026 (UTC)
== June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project ==
<div class="border-box" style="background-color: var(--background-color-warning-subtle, #f8eaba); max-width: 875px; padding: 5px; border: 1px solid black; margin: 5px; color: var(--clr-dark)">
<div class="box" style="float:left; padding-top: 10px; padding-right: 10px; padding-left: 10px; padding-bottom: 10px;">[[File:Wikimedia Café logo in plain SVG format.svg|60px|alt=The logo for the Wikimedia Café]]</div>
Hello! There will be two '''[https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9 Wikimedia Café]''' discussion opportunities during the last weekend of June. Both sessions will focus on the [https://en.wikipedia.org/wiki/Wikipedia:Editor_reflections English Wikipedia Editor Reflections project]. The featured guest in the Café will be [https://en.wikipedia.org/wiki/User:Clovermoss User:Clovermoss]. Participants may attend either or both sessions.
#'''27 June 2026 15:00 UTC''' ([https://zonestamp.toolforge.org/1782572400 timestamp converter]), at a time friendly to the Americas, Africa, and Europe
#'''28 June 2026 03:00 UTC''' ([https://zonestamp.toolforge.org/1782615600 timestamp converter]), at a time friendly to Asia and the Pacific
Please see the Café page for more information, including [https://meta.wikimedia.org/wiki/Wikimedia_Caf%C3%A9#How_to_attend_the_session how to register]!
<br />
[[File:Buntstifte Eberhard Faber crop 64h.jpg|860px|alt=cropped image of colored pencils]]</div>
<span style="white-space:nowrap;">[[User:Pine|<span style="color:#01796f; text-shadow:#00BFFF 0 0 1.0em">↠Pine</span>]] [[User talk:Pine|<span style="color:DeepSkyBlue">(<b style="color:#FFDF00;text-shadow:#FFDF00 0 0 1.0em">✉</b>)</span>]]</span> 03:56, 15 June 2026 (UTC)
oyudlbwurtx0cua4yulspohipujf01l
Category:Test
14
38042
746707
744877
2026-06-14T16:28:12Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
746707
wikitext
text/x-wiki
<noinclude>
{{Категория к переименованию|2026-06-01|Test1}}
</noinclude>
{{#invoke:Kateqoriya daha çox|main}}
{{subst:cfr|Road to ..}}
[[Category:MainCat]]
[[Category:MainCatOlafTest]]
fi3upzh4q3k3fr4pa96zdafqrx5vzya
746710
746707
2026-06-14T16:28:45Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
746710
wikitext
text/x-wiki
{{#invoke:Kateqoriya daha çox|main}}
{{subst:cfr|Road to ..}}
[[Category:MainCat]]
[[Category:MainCatOlafTest]]
1r0l2yvnb5pfmke41boc5lilvzgwkgv
Tto page 6
0
76479
746684
202883
2026-06-14T15:37:32Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746684
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
7werkhyszhw1jqb3cg65y5fttqnwch7
746686
746684
2026-06-14T15:38:02Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К переименованию/14 июня 2026#Tto page 6 → Teest]]
746686
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}
{{к переименованию|2026-06-14|Teest}}
</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
7br09z3cvxo64co4vq8eh2i4exsd2tx
746690
746686
2026-06-14T15:38:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Tto page 6]] — Оставлено
746690
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-14|Teest}}
</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
3hh6w0hdgxy967xc6cmhzr4k9g6wx55
746691
746690
2026-06-14T15:38:43Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746691
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-14|Teest}}
{{к удалению|2026-06-14}}
</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
1nm8vuudznfd1ld7epwlb4vqccira9h
746700
746691
2026-06-14T15:41:29Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Tto page 6]] — Оставлено
746700
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-14|Teest}}
</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
3hh6w0hdgxy967xc6cmhzr4k9g6wx55
746701
746700
2026-06-14T15:41:36Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746701
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-14|Teest}}
{{к удалению|2026-06-14}}
</noinclude>
<!-- Commented out: [[File:Yes i can see tgis clearly.png]] -->
1nm8vuudznfd1ld7epwlb4vqccira9h
746705
746701
2026-06-14T15:43:38Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Tto page 6]] — Заменено перенаправлением на [[Test]]
746705
wikitext
text/x-wiki
#REDIRECT [[Test]]
7b7a93jxadqud0gsdhyim6gnupmymjn
Category:Legend
14
99718
746670
745119
2026-06-14T15:05:43Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на обсуждение
746670
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-06-02}}
{{Обсуждаемая категория|2026-06-14}}
</noinclude>
[[Category:Kakostek]]
r1vgbvtoa03r9ttmqb3tgz9mp9niw3y
746672
746670
2026-06-14T15:05:58Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
746672
wikitext
text/x-wiki
<noinclude>
{{Обсуждаемая категория|2026-06-14}}
</noinclude>
[[Category:Kakostek]]
51y6id7bk1qdgd02zti93mlp9rubbus
746674
746672
2026-06-14T15:06:08Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
746674
wikitext
text/x-wiki
[[Category:Kakostek]]
c7svuis6mihqx1sklq7441b90yzwp7h
Steward requests/Global
0
109970
746720
746614
2026-06-14T23:13:19Z
Angela Criss 55
74427
Replaced content with "Timelash.__TOC__ [[en:OpenVPN]] [[en:Category:Wikipedia_sockpuppets_of_LocoWiki]] [[es:Wikipedia:Tablón de anuncios de los bibliotecarios/Portal/Archivo/Miscelánea/Actual#Caso: LocoWiki]]"
746720
wikitext
text/x-wiki
Timelash.__TOC__
[[en:OpenVPN]]
[[en:Category:Wikipedia_sockpuppets_of_LocoWiki]]
[[es:Wikipedia:Tablón de anuncios de los bibliotecarios/Portal/Archivo/Miscelánea/Actual#Caso: LocoWiki]]
sn1052p3zek5mhdqtcbnhonzspiflj3
746721
746720
2026-06-14T23:13:25Z
AutoModeratorTest
61468
Reverted edit by [[Special:Contributions/Angela Criss 55|Angela Criss 55]] ([[User talk:Angela Criss 55|talk]]) to last revision by [[User:LuniZunie|LuniZunie]]
746614
wikitext
text/x-wiki
{{mbox|type=content|text=Due to persistent vandalism and disruption on this page, this page is currently semi-protected indefinitely. Globally blocked or locked users should appeal to {{nospam|stewards|wikimedia.org}}.<br />If your account was locked with a reason "Compromised account", please contact {{nospam|ca|wikimedia.org}}.}}
{{dynamite|title=:Steward requests/Global/Header}}{{NOINDEX}}__TOC__<br style="clear:both"/>
== Requests for global (un)block ==
<span id="B"/>
{{dynamite|title=:Steward requests/Global/block-header}}
<!-- Your requests go AT THE BOTTOM OF THE SECTION. Copy the request template above and fill in your information. -->
=== Global block for [[Special:Contributions/126.24.177.199|126.24.177.199]] ===
{{Status}}
* {{Luxotool|126.24.177.199}}
:Open proxy - running [[en:OpenVPN]] on port 1767 and 1393. An initial 3-month block should do the job. --[[User:MrClog|MrClog]] ([[User talk:MrClog|talk]]) 08:29, 1 May 2020 (UTC)
{{status}}
* {{LockHide|Awesome Aasim|hidename=yes}}
test [[User:UpsandDowns1234|<span style="color:green;">Ups</span>]] and [[User talk:UpsandDowns1234|<span style="color:red;">Downs</span>]] ([[Special:Contributions/UpsandDowns1234|↕]]) 18:55, 18 May 2020 (UTC)
=== Global lock for [username hidden] ===
{{status}}
* {{LockHide|Awesome Aasim|hidename=yes}}
test [[User:Awesome Aasim|Awesome Aasim]] ([[User talk:Awesome Aasim|talk]]) 19:21, 18 May 2020 (UTC)
== Requests for global (un)lock and (un)hiding ==
<span id="L"/>
{{dynamite|title=:Steward requests/Global/lock-header}}
<!-- Your requests go AT THE BOTTOM OF THE SECTION. Copy the request template above and fill in your information. -->
=== Global lock for [[User:Kshitij pandey 007|Kshitij pandey 007]] ===
{{Status}}
*{{LockHide|Kshitij pandey 007}}
:Constant vandalism. ---'''[[User:J ansari|<span style="background:#5d9731; color:white;padding:2px;">J. Ansari</span>]] [[User talk:J ansari|<span style="background:#1049AB; color:white; padding:2px;">Talk</span>]]''' 09:25, 1 May 2020 (UTC)
::{{re|J ansari}} Could this not be handled locally in Hindi Wikipedia? --[[User:Green Giant|Green Giant]] ([[User talk:Green Giant|talk]]) 18:13, 1 May 2020 (UTC)
===Global lock for LocoWiki===
{{status|notdone}}
*{{lockHide|LocoWiki}}
;Evidence:
*[[en:Category:Wikipedia_sockpuppets_of_LocoWiki]]
*[[es:Wikipedia:Tablón de anuncios de los bibliotecarios/Portal/Archivo/Miscelánea/Actual#Caso: LocoWiki]]
LTA for self-promotion crosswiki
Regards!!!! [[User:Ezarate|Esteban]] ([[User talk:Ezarate|talk]]) 12:56, 1 May 2020 (UTC)
:{{notdone}} No evidence of cross wiki abuse? [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:19, 1 May 2020 (UTC)
=== Global lock for [[User:Sriramakoti|Sriramakoti]] ===
{{Status|done}}
*{{LockHide|Sriramakoti}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 13:56, 1 May 2020 (UTC)
::{{done}} [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:16, 1 May 2020 (UTC)
=== Global lock for [[User:JosePenj|JosePenj]] ===
{{Status|notdone}}
*{{MultiLock|JosePenj|Arman643543|Arman115786}}
:Abusing multiple account. They created same topic article (James Richman) in xwiki and these account created few day before. --[[User:SCP-2000|<span style="color: #383838;">'''SCP'''</span>]][[User talk:SCP-2000|<span style="color: #242424;">'''-20'''</span>]][[Special:Contributions/SCP-2000|<span style="color: #080808;">'''00'''</span>]] 15:22, 1 May 2020 (UTC)
:{{notdone}} No evidence of abuse - accounts are not blocked anywhere. [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:24, 1 May 2020 (UTC)
=== Global lock/unlock for [[User:Minhaj2288|Minhaj2288]] ===
{{status|done}} <!-- do not remove this template -->
* {{LockHide|Minhaj2288}}
* Crosswiki spammer --[[User:Seewolf|Harald Krichel]] ([[User talk:Seewolf|talk]]) 17:45, 1 May 2020 (UTC)
::{{done}} [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:09, 1 May 2020 (UTC)
=== Global lock for [[User:AugustaFeaster7|AugustaFeaster7]] ===
{{Status|done}}
*{{LockHide|AugustaFeaster7}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 21:29, 1 May 2020 (UTC)
::{{done}}--[[User:Sakretsu|Sakretsu]] ([[User talk:Sakretsu|炸裂]]) 21:35, 1 May 2020 (UTC)
=== Global lock for [[User:Autodesk Sketchbook Indonesia|Autodesk Sketchbook Indonesia]] ===
{{Status}}
*{{LockHide|Autodesk Sketchbook Indonesia}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 21:30, 1 May 2020 (UTC)
=== Global lock for [[User:Xiplus|Xiplus]] ===
{{Status}}
*{{LockHide|Xiplus}}
:Test. --[[User:A2093064-test|A2093064-test]] ([[User talk:A2093064-test|talk]]) 01:45, 2 May 2020 (UTC)
=== Global lock for Enbi (script testing) ===
{{Status}}
*{{LockHide|Enbi (script testing)}}
Long-term abuse. <b style="font-family:Trebuchet MS">[[User:enbi|<span style="color:#E28C00">enbi</span>]] [<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>] • [[[User talk:enbi|talk]]]</b> 20:20, 18 April 2026 (UTC)
=== Global lock for Enbi (script testing) ===
{{Status}}
*{{LockHide|Enbi (script testing)}}
Long-term abuse. <b style="font-family:Trebuchet MS">[[User:enbi|<span style="color:#E28C00">enbi</span>]] [<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>] • [[[User talk:enbi|talk]]]</b> 20:20, 18 April 2026 (UTC)
=== Global lock for LuniZunie ===
{{Status}} <!-- Do not remove this template -->
* {{LockHide|LuniZunie}}
Long-term abuse [[User:LuniZunie|LuniZunie]] ([[User talk:LuniZunie|talk]]) 01:38, 13 June 2026 (UTC)
== See also ==
<!-- DO NOT EDIT UNDER THIS LINE -->
{{/Archivefooter}}
{{RF}}
26ozfndfkzll8377lz7z6uqnbh65wbi
746722
746721
2026-06-14T23:22:22Z
Angela Criss 55
74427
Undid revision [[Special:Diff/746721|746721]] by [[Special:Contributions/AutoModeratorTest|AutoModeratorTest]] ([[User talk:AutoModeratorTest|talk]])
746722
wikitext
text/x-wiki
Timelash.__TOC__
[[en:OpenVPN]]
[[en:Category:Wikipedia_sockpuppets_of_LocoWiki]]
[[es:Wikipedia:Tablón de anuncios de los bibliotecarios/Portal/Archivo/Miscelánea/Actual#Caso: LocoWiki]]
sn1052p3zek5mhdqtcbnhonzspiflj3
746724
746722
2026-06-14T23:54:17Z
Elton
19642
Reverted edit by [[Special:Contributions/Angela Criss 55|Angela Criss 55]] ([[User talk:Angela Criss 55|talk]]) to last revision by [[User:AutoModeratorTest|AutoModeratorTest]]
746614
wikitext
text/x-wiki
{{mbox|type=content|text=Due to persistent vandalism and disruption on this page, this page is currently semi-protected indefinitely. Globally blocked or locked users should appeal to {{nospam|stewards|wikimedia.org}}.<br />If your account was locked with a reason "Compromised account", please contact {{nospam|ca|wikimedia.org}}.}}
{{dynamite|title=:Steward requests/Global/Header}}{{NOINDEX}}__TOC__<br style="clear:both"/>
== Requests for global (un)block ==
<span id="B"/>
{{dynamite|title=:Steward requests/Global/block-header}}
<!-- Your requests go AT THE BOTTOM OF THE SECTION. Copy the request template above and fill in your information. -->
=== Global block for [[Special:Contributions/126.24.177.199|126.24.177.199]] ===
{{Status}}
* {{Luxotool|126.24.177.199}}
:Open proxy - running [[en:OpenVPN]] on port 1767 and 1393. An initial 3-month block should do the job. --[[User:MrClog|MrClog]] ([[User talk:MrClog|talk]]) 08:29, 1 May 2020 (UTC)
{{status}}
* {{LockHide|Awesome Aasim|hidename=yes}}
test [[User:UpsandDowns1234|<span style="color:green;">Ups</span>]] and [[User talk:UpsandDowns1234|<span style="color:red;">Downs</span>]] ([[Special:Contributions/UpsandDowns1234|↕]]) 18:55, 18 May 2020 (UTC)
=== Global lock for [username hidden] ===
{{status}}
* {{LockHide|Awesome Aasim|hidename=yes}}
test [[User:Awesome Aasim|Awesome Aasim]] ([[User talk:Awesome Aasim|talk]]) 19:21, 18 May 2020 (UTC)
== Requests for global (un)lock and (un)hiding ==
<span id="L"/>
{{dynamite|title=:Steward requests/Global/lock-header}}
<!-- Your requests go AT THE BOTTOM OF THE SECTION. Copy the request template above and fill in your information. -->
=== Global lock for [[User:Kshitij pandey 007|Kshitij pandey 007]] ===
{{Status}}
*{{LockHide|Kshitij pandey 007}}
:Constant vandalism. ---'''[[User:J ansari|<span style="background:#5d9731; color:white;padding:2px;">J. Ansari</span>]] [[User talk:J ansari|<span style="background:#1049AB; color:white; padding:2px;">Talk</span>]]''' 09:25, 1 May 2020 (UTC)
::{{re|J ansari}} Could this not be handled locally in Hindi Wikipedia? --[[User:Green Giant|Green Giant]] ([[User talk:Green Giant|talk]]) 18:13, 1 May 2020 (UTC)
===Global lock for LocoWiki===
{{status|notdone}}
*{{lockHide|LocoWiki}}
;Evidence:
*[[en:Category:Wikipedia_sockpuppets_of_LocoWiki]]
*[[es:Wikipedia:Tablón de anuncios de los bibliotecarios/Portal/Archivo/Miscelánea/Actual#Caso: LocoWiki]]
LTA for self-promotion crosswiki
Regards!!!! [[User:Ezarate|Esteban]] ([[User talk:Ezarate|talk]]) 12:56, 1 May 2020 (UTC)
:{{notdone}} No evidence of cross wiki abuse? [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:19, 1 May 2020 (UTC)
=== Global lock for [[User:Sriramakoti|Sriramakoti]] ===
{{Status|done}}
*{{LockHide|Sriramakoti}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 13:56, 1 May 2020 (UTC)
::{{done}} [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:16, 1 May 2020 (UTC)
=== Global lock for [[User:JosePenj|JosePenj]] ===
{{Status|notdone}}
*{{MultiLock|JosePenj|Arman643543|Arman115786}}
:Abusing multiple account. They created same topic article (James Richman) in xwiki and these account created few day before. --[[User:SCP-2000|<span style="color: #383838;">'''SCP'''</span>]][[User talk:SCP-2000|<span style="color: #242424;">'''-20'''</span>]][[Special:Contributions/SCP-2000|<span style="color: #080808;">'''00'''</span>]] 15:22, 1 May 2020 (UTC)
:{{notdone}} No evidence of abuse - accounts are not blocked anywhere. [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:24, 1 May 2020 (UTC)
=== Global lock/unlock for [[User:Minhaj2288|Minhaj2288]] ===
{{status|done}} <!-- do not remove this template -->
* {{LockHide|Minhaj2288}}
* Crosswiki spammer --[[User:Seewolf|Harald Krichel]] ([[User talk:Seewolf|talk]]) 17:45, 1 May 2020 (UTC)
::{{done}} [[User:Ruslik0|Ruslik]] ([[User talk:Ruslik0|talk]]) 20:09, 1 May 2020 (UTC)
=== Global lock for [[User:AugustaFeaster7|AugustaFeaster7]] ===
{{Status|done}}
*{{LockHide|AugustaFeaster7}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 21:29, 1 May 2020 (UTC)
::{{done}}--[[User:Sakretsu|Sakretsu]] ([[User talk:Sakretsu|炸裂]]) 21:35, 1 May 2020 (UTC)
=== Global lock for [[User:Autodesk Sketchbook Indonesia|Autodesk Sketchbook Indonesia]] ===
{{Status}}
*{{LockHide|Autodesk Sketchbook Indonesia}}
:Spam / spambot. --[[User:DannyS712|DannyS712]] ([[User talk:DannyS712|talk]]) 21:30, 1 May 2020 (UTC)
=== Global lock for [[User:Xiplus|Xiplus]] ===
{{Status}}
*{{LockHide|Xiplus}}
:Test. --[[User:A2093064-test|A2093064-test]] ([[User talk:A2093064-test|talk]]) 01:45, 2 May 2020 (UTC)
=== Global lock for Enbi (script testing) ===
{{Status}}
*{{LockHide|Enbi (script testing)}}
Long-term abuse. <b style="font-family:Trebuchet MS">[[User:enbi|<span style="color:#E28C00">enbi</span>]] [<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>] • [[[User talk:enbi|talk]]]</b> 20:20, 18 April 2026 (UTC)
=== Global lock for Enbi (script testing) ===
{{Status}}
*{{LockHide|Enbi (script testing)}}
Long-term abuse. <b style="font-family:Trebuchet MS">[[User:enbi|<span style="color:#E28C00">enbi</span>]] [<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>] • [[[User talk:enbi|talk]]]</b> 20:20, 18 April 2026 (UTC)
=== Global lock for LuniZunie ===
{{Status}} <!-- Do not remove this template -->
* {{LockHide|LuniZunie}}
Long-term abuse [[User:LuniZunie|LuniZunie]] ([[User talk:LuniZunie|talk]]) 01:38, 13 June 2026 (UTC)
== See also ==
<!-- DO NOT EDIT UNDER THIS LINE -->
{{/Archivefooter}}
{{RF}}
26ozfndfkzll8377lz7z6uqnbh65wbi
User talk:Rachmat04
3
115969
746657
744863
2026-06-14T14:19:12Z
Rachmat04
26096
Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]]
746657
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
== Warning: disruptive editing ==
Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive.
If you believe this warning has been issued in error, please leave a message on my talk page. '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC)
7b1g19de2q3rz2frrr4ogbrlheqpnaq
746658
746657
2026-06-14T14:19:55Z
Rachmat04
26096
Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]]
746658
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
== Warning: disruptive editing ==
Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive.
If you believe this warning has been issued in error, please leave a message on my talk page. '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC)
== Warning: disruptive editing ==
Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive.
If you believe this warning has been issued in error, please leave a message on my talk page.
Additional information: This is a test '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC)
1cjro7ydf3dvzvfwtjftujj4bhat7zt
746659
746658
2026-06-14T14:32:23Z
Rachmat04
26096
Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]]
746659
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
== Warning: disruptive editing ==
Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive.
If you believe this warning has been issued in error, please leave a message on my talk page. '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC)
== Warning: disruptive editing ==
Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive.
If you believe this warning has been issued in error, please leave a message on my talk page.
Additional information: This is a test '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC)
== Final warning: vandalism ==
Your recent edits to one or more pages appear to constitute [[WP:VAND|vandalism]]. Vandalism is not permitted on this wiki. Please stop making edits that damage or degrade the encyclopaedia. If this behaviour continues, your account may be restricted from editing.
If you believe this warning has been issued in error, please leave a message on my talk page.
Additional information: Test '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:32, 14 June 2026 (UTC)
oplwdnx63cux78bdyksowalxa0hg2ti
Skipple
0
145728
746697
539793
2026-06-14T15:39:41Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Skipple]]
746697
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</noinclude>
Hi! My name is Skipple!
kbrhzkahdzlny62v7kr568g7j5wgoe6
Wikipedia:Village pump/topic list
4
146208
746730
744658
2026-06-15T03:56:21Z
Cewbot
33876
[[User:Cewbot/log/20170915/configuration|Generate topic list: 9 topics]]; new reply: [[Wikipedia:Village pump#June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project|June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project]]
746730
wikitext
text/x-wiki
<!-- This page is auto-generated by bot. Please contact the bot operator to improve the tool. -->
{| class="wikitable sortable mw-collapsible" style="float:left;"
|-
! data-sort-type="number" style="font-weight: normal;" | <small>#</small> !! 💭 Title !! <span title="Count of comments">💬</span> !! <span title="Count of peoples in discussion">👥</span> !! 🙋 Last editor !! data-sort-type="isoDate" | <span title="Date/Time">🕒 <small>(UTC)</small></span>
|-
| style="text-align: right;" | 1
| [[Wikipedia:Village pump#Script|Script]]
| style="text-align: right;" | 8
| style="text-align: right;" | 5
| style="background-color: #bbb;" | [[User:LuniZunie|LuniZunie]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-11-09T16:47:00.000Z" | 2025-11-09 <span style="color: blue;">16:47</span>
|-
| style="text-align: right;" | 2
| [[Wikipedia:Village pump#Report_concerning_Tanbiruzzammn|Report concerning Tanbiruzzammn]]
| style="text-align: right;" | 2
| style="text-align: right;" | 2
| style="background-color: #bbb;" | [[User:Barras|Barras]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-12-09T21:45:00.000Z" | 2025-12-09 <span style="color: blue;">21:45</span>
|-
| style="text-align: right;" | 3
| [[Wikipedia:Village pump#Report_concerning_Bucheon|Report concerning Bucheon]]
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:PieWriter|PieWriter]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-02-19T10:25:00.000Z" | 2026-02-19 <span style="color: blue;">10:25</span>
|-
| style="text-align: right;" | 4
| [[Wikipedia:Village pump#Versions_and_dates|Versions and dates]]
| style="text-align: right;" | 2
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[Special:Contributions/~2026-13668-13|<span style="color: #c20;">~2026-13668-13</span>]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-03T06:17:00.000Z" | 2026-03-03 <span style="color: blue;">06:17</span>
|-
| style="text-align: right;" | 5
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#Upcoming_Wikimedia_Café_meetup_regarding_the_the_2026-2027_Wikimedia_Foundation_Annual_Plan|Upcoming Wikimedia Café meetup regarding the the 2026-2027 Wikimedia Foundation Annual Plan]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:Pine|Pine]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-30T03:46:00.000Z" | 2026-03-30 <span style="color: blue;">03:46</span>
|-
| style="text-align: right;" | 6
| [[Wikipedia:Village pump#Changes_to_electionadmin_userright|Changes to electionadmin userright]]
| style="text-align: right;" | 4
| style="text-align: right;" | 4
| style="background-color: #bbb;" | [[User:Chaotic Enby|Chaotic Enby]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-04-23T15:22:00.000Z" | 2026-04-23 <span style="color: blue;">15:22</span>
|-
| style="text-align: right;" | 7
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#Report_concerning_Princebarackalibarrydaddyobamaiii|Report concerning Princebarackalibarrydaddyobamaiii]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #ddd;" | [[User:MathXplore|MathXplore]]
| style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-05-18T12:23:00.000Z" | 2026-05-18 <span style="color: blue;">12:23</span>
|-
| style="text-align: right;" | 8
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#May_2026_Wikimedia_Café_meetups_regarding_the_Wikimedia_Foundation_Annual_Plan|May 2026 Wikimedia Café meetups regarding the Wikimedia Foundation Annual Plan]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #ddd;" | [[User:Pine|Pine]]
| style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-05-21T19:54:00.000Z" | 2026-05-21 <span style="color: blue;">19:54</span>
|-
| style="text-align: right;" | 9
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#June_2026_Wikimedia_Café_meetups_regarding_the_English_Wikipedia_Editor_Reflections_project|June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #efe;" | [[User:Pine|Pine]]
| style="background-color: #efe;" data-sort-type="isoDate" data-sort-value="2026-06-15T03:56:00.000Z" | 2026-06-15 <span style="color: blue;">03:56</span>
|}
{| class="wikitable mw-collapsible mw-collapsed" style="float: left; margin-left: .5em;;{{#if:{{{no_time_legend|}}}|display:none;|}}"
! title="From the latest bot edit" | Legend
|-
| style="background-color: #efe;" |
* In the last hour
|-
| style="background-color: #eef;" |
* In the last day
|-
| |
* In the last week
|-
| style="background-color: #ddd;" |
* In the last month
|-
| style="background-color: #bbb;" |
* More than one month
|-
! Manual settings
|-
| style="max-width: 12em;" | <small>When exceptions occur,<br />please check [[User:Cewbot/log/20170915/configuration|the setting]] first.</small>
|-
|}
{{Clear}}
blw6l4826b06h6hfpx8qd4txpm0s5tr
746731
746730
2026-06-15T09:56:37Z
Cewbot
33876
[[User:Cewbot/log/20170915/configuration|Generate topic list: 9 topics]]
746731
wikitext
text/x-wiki
<!-- This page is auto-generated by bot. Please contact the bot operator to improve the tool. -->
{| class="wikitable sortable mw-collapsible" style="float:left;"
|-
! data-sort-type="number" style="font-weight: normal;" | <small>#</small> !! 💭 Title !! <span title="Count of comments">💬</span> !! <span title="Count of peoples in discussion">👥</span> !! 🙋 Last editor !! data-sort-type="isoDate" | <span title="Date/Time">🕒 <small>(UTC)</small></span>
|-
| style="text-align: right;" | 1
| [[Wikipedia:Village pump#Script|Script]]
| style="text-align: right;" | 8
| style="text-align: right;" | 5
| style="background-color: #bbb;" | [[User:LuniZunie|LuniZunie]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-11-09T16:47:00.000Z" | 2025-11-09 <span style="color: blue;">16:47</span>
|-
| style="text-align: right;" | 2
| [[Wikipedia:Village pump#Report_concerning_Tanbiruzzammn|Report concerning Tanbiruzzammn]]
| style="text-align: right;" | 2
| style="text-align: right;" | 2
| style="background-color: #bbb;" | [[User:Barras|Barras]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-12-09T21:45:00.000Z" | 2025-12-09 <span style="color: blue;">21:45</span>
|-
| style="text-align: right;" | 3
| [[Wikipedia:Village pump#Report_concerning_Bucheon|Report concerning Bucheon]]
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:PieWriter|PieWriter]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-02-19T10:25:00.000Z" | 2026-02-19 <span style="color: blue;">10:25</span>
|-
| style="text-align: right;" | 4
| [[Wikipedia:Village pump#Versions_and_dates|Versions and dates]]
| style="text-align: right;" | 2
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[Special:Contributions/~2026-13668-13|<span style="color: #c20;">~2026-13668-13</span>]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-03T06:17:00.000Z" | 2026-03-03 <span style="color: blue;">06:17</span>
|-
| style="text-align: right;" | 5
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#Upcoming_Wikimedia_Café_meetup_regarding_the_the_2026-2027_Wikimedia_Foundation_Annual_Plan|Upcoming Wikimedia Café meetup regarding the the 2026-2027 Wikimedia Foundation Annual Plan]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:Pine|Pine]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-30T03:46:00.000Z" | 2026-03-30 <span style="color: blue;">03:46</span>
|-
| style="text-align: right;" | 6
| [[Wikipedia:Village pump#Changes_to_electionadmin_userright|Changes to electionadmin userright]]
| style="text-align: right;" | 4
| style="text-align: right;" | 4
| style="background-color: #bbb;" | [[User:Chaotic Enby|Chaotic Enby]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-04-23T15:22:00.000Z" | 2026-04-23 <span style="color: blue;">15:22</span>
|-
| style="text-align: right;" | 7
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#Report_concerning_Princebarackalibarrydaddyobamaiii|Report concerning Princebarackalibarrydaddyobamaiii]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #ddd;" | [[User:MathXplore|MathXplore]]
| style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-05-18T12:23:00.000Z" | 2026-05-18 <span style="color: blue;">12:23</span>
|-
| style="text-align: right;" | 8
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#May_2026_Wikimedia_Café_meetups_regarding_the_Wikimedia_Foundation_Annual_Plan|May 2026 Wikimedia Café meetups regarding the Wikimedia Foundation Annual Plan]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #ddd;" | [[User:Pine|Pine]]
| style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-05-21T19:54:00.000Z" | 2026-05-21 <span style="color: blue;">19:54</span>
|-
| style="text-align: right;" | 9
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#June_2026_Wikimedia_Café_meetups_regarding_the_English_Wikipedia_Editor_Reflections_project|June 2026 Wikimedia Café meetups regarding the English Wikipedia Editor Reflections project]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #eef;" | [[User:Pine|Pine]]
| style="background-color: #eef;" data-sort-type="isoDate" data-sort-value="2026-06-15T03:56:00.000Z" | 2026-06-15 <span style="color: blue;">03:56</span>
|}
{| class="wikitable mw-collapsible mw-collapsed" style="float: left; margin-left: .5em;;{{#if:{{{no_time_legend|}}}|display:none;|}}"
! title="From the latest bot edit" | Legend
|-
| style="background-color: #efe;" |
* In the last hour
|-
| style="background-color: #eef;" |
* In the last day
|-
| |
* In the last week
|-
| style="background-color: #ddd;" |
* In the last month
|-
| style="background-color: #bbb;" |
* More than one month
|-
! Manual settings
|-
| style="max-width: 12em;" | <small>When exceptions occur,<br />please check [[User:Cewbot/log/20170915/configuration|the setting]] first.</small>
|-
|}
{{Clear}}
8hphpkxk6ofruc3kztmxl4te2lsilb4
Projectwiki
0
153925
746661
745208
2026-06-14T14:53:44Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Projectwiki]]
746661
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</noinclude>
the sample ocr page
Af edit
Normal edit
[https://djfdsjl.com]
Normal edit
[https://jsdkfldsjl.com]
Af edit normal edit
20e0o38kpfkcy127hkowjvnvp97dsi5
746666
746661
2026-06-14T14:57:16Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Projectwiki]] — Заменено перенаправлением на [[Test]]
746666
wikitext
text/x-wiki
#REDIRECT [[Test]]
7b7a93jxadqud0gsdhyim6gnupmymjn
Test
0
155073
746668
745554
2026-06-14T14:57:55Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к переименованию/1 июня 2026#Test → Test 2]] — Не переименовано
746668
wikitext
text/x-wiki
<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.........zzzz
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>.z
== 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|P hcaptcha probeolyGram]] 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
4ffypt2pv4biy4zvvey15h9wux9pf80
746676
746668
2026-06-14T15:06:31Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Test]]
746676
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</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.........zzzz
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>.z
== 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|P hcaptcha probeolyGram]] 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
mwzchgg9nk8ra62x2umrfz1k7ao2nxf
746679
746676
2026-06-14T15:06:46Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Test]] — Снято с удаления
746679
wikitext
text/x-wiki
<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.........zzzz
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>.z
== 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|P hcaptcha probeolyGram]] 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
4ffypt2pv4biy4zvvey15h9wux9pf80
746681
746679
2026-06-14T15:18:12Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Test]]
746681
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</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.........zzzz
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>.z
== 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|P hcaptcha probeolyGram]] 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
mwzchgg9nk8ra62x2umrfz1k7ao2nxf
746714
746681
2026-06-14T20:02:46Z
Trialpears
43074
Nominated for deletion; see [[:Wikipedia:Articles for deletion/Test]].
746714
wikitext
text/x-wiki
<!-- Please do not remove or change this AfD message until the discussion has been closed. -->
{{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}}
<!-- Once discussion is closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} -->
<!-- End of AfD message, feel free to edit beyond this point -->
<noinclude>{{к удалению|2026-06-14}}</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.........zzzz
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>.z
== 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|P hcaptcha probeolyGram]] 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
7djg5gospr82viahcixohcob5wq58c5
Talk:Test
1
155074
746667
745487
2026-06-14T14:57:54Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к переименованию/1 июня 2026#Test → Test 2]] — Не переименовано
746667
wikitext
text/x-wiki
{{Не переименовано|2026-06-01|Test|Test 2}}
Hello! <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:It shows that the comment is made by Example. <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::No it doesn't. <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::: Are you mistaken? I think it does. <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:::: Look in the history, the comment is made by a different user. <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::::: But on this page, it SAYS the latest comment! <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:::::: Look in the history, people can be impersonated by doing a signature like this... <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::::::: Look at my cool signature! <b style="font-family:monospace;color:#E35BD8">[[User:Example3|<b style="color:#029D74">e</b>]]×[[Special:Contribs/Example3|<b style="color: #029D74">ample</b>]][[User talk:Example3|🗯️]]</b> {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
== Test ==
test [[Special:Contributions/~2026-33346-13|~2026-33346-13]] ([[User talk:~2026-33346-13|talk]]) 12:01, 8 June 2026 (UTC)
30gaq5whnemiwidjrnu3pz29z9i6ow5
746678
746667
2026-06-14T15:06:45Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Test]] — Снято с удаления
746678
wikitext
text/x-wiki
{{Снято с удаления|2026-06-14}}
{{Не переименовано|2026-06-01|Test|Test 2}}
Hello! <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:It shows that the comment is made by Example. <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::No it doesn't. <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::: Are you mistaken? I think it does. <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:::: Look in the history, the comment is made by a different user. <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::::: But on this page, it SAYS the latest comment! <span style="color:orange;">'''EXAMPLE'''</span> ([[User talk:Example|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
:::::: Look in the history, people can be impersonated by doing a signature like this... <span style="color:blue;">'''EXAMPLE TWO'''</span> ([[User talk:Example2|talk]]) {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
::::::: Look at my cool signature! <b style="font-family:monospace;color:#E35BD8">[[User:Example3|<b style="color:#029D74">e</b>]]×[[Special:Contribs/Example3|<b style="color: #029D74">ample</b>]][[User talk:Example3|🗯️]]</b> {{CURRENTTIME}}, {{CURRENTDAY}} {{CURRENTMONTHNAME}} {{CURRENTYEAR}} (UTC)
== Test ==
test [[Special:Contributions/~2026-33346-13|~2026-33346-13]] ([[User talk:~2026-33346-13|talk]]) 12:01, 8 June 2026 (UTC)
te898qd6p89jrkdfcrt3ofqva4jb1v0
Drag racing
0
160604
746725
688889
2026-06-15T00:03:19Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
746725
wikitext
text/x-wiki
[[Image:Tree counting down.JPG|thumb|225px|The Christmas tree counting down at [[Saskatchewan International Raceway|SIR]]. Note the blinder, to prevent the driver from being distracted by the light for the other lane.]]
'''Drag racing''' is a competition between specially prepared [[automobile]]s or [[motorcycle|motorcycles]]. Racers compete, two at a time, to be the first to cross a finish line. Races are run from a [[standing start]], in a straight line, over a measured distance, usually a ¼-mile straight track.
The race track, known as a [[dragstrip]], usually uses an electronic timing system to decide the winner.
==Basics of drag racing==
Before each race, each driver is allowed to perform a [[burnout]] by spinning the driving tires. Burnouts heat the driving tires and lay rubber down at the beginning of the track to improve traction. The drivers then line up at the starting line.
Races are started electronically by a system known as a [[Christmas tree (racing)| Christmas tree]].
[[Image:Pro Street Camaro at launch.JPG|thumb|225px|Camaro at launch, with ''Altered Vision'' in the right lane. [[Wheelie|Wheelstand]] means torque has been wasted lifting the front end, rather than moving the vehicle forward.]]
Below the staging lights are three large amber lights, a green light, and a red light. When both drivers are staged, the tree is set to start the race causing the three large amber lights to illuminate, followed by the green light. If the front tires leave the starting line before the green light illuminates, the red light for that driver's lane illuminates instead. This indicates disqualification (unless a more serious violation occurs). Once a driver commits a red-light foul (also known as ''redlighting''), the other driver can also commit a foul start by leaving the line too early but still win, having left later. Should both drivers leave after the green light illuminates, the one leaving first is said to have a ''holeshot advantage''.
The first vehicle to cross the finish line wins the race. The elapsed time is a measure of performance only; it does not determine the winner. Because elapsed time does not include reaction time and each lane is timed separately, a car with a slower elapsed time can actually win if that driver's holeshot advantage exceeds the elapsed time difference. This is known as a ''holeshot win''.<ref>{{cite web |url= http://www.nhra.com/glossary.aspx|title="NHRA Glossary" |website= NHRA.com |accessdate=August 11, 2011}}</ref>
Several measurements are taken for each race: reaction time, elapsed time, and speed. Reaction time is the period from the lighting of the green light to the vehicle leaving the starting line. Elapsed time is the time from the vehicle leaving the starting line to crossing the finish line. Speed is measured through a [[speed trap]] covering the final {{convert|66|ft|m}} to the finish line, indicating the approximate maximum speed of the vehicle during the run.
In the standard racing format, the losing car and driver are removed from the contest and the winner goes on to race other winners. This continues until only one is left.
[[Image:Funny Car.jpg|thumb|right|Flopper with body up.]]
==Racing organization==
[[Image:Delivering timeslips.JPG|thumb|Chief Timer delivering timeslips to competitors after their passes.]]
=== North America ===
The [[NHRA|National Hot Rod Association]] (NHRA) oversees most of drag racing events in North America. The next largest organization, [[Feld Entertainment]]'s [[International Hot Rod Association]] (IHRA), is about one-third the size of NHRA. Nearly all drag strips are associated with one sanctioning body or the other. The NHRA is more popular with large, 1/4-mile, nationally recognized, tracks., The IHRA is a favorite of smaller 1/8-mile local tracks (and offers selected races on their national tour under the 1/8-mile format). One reason for this is the IHRA is less restrictive in its rules, such as rules on nitrous oxide (legal in Pro Modified) and oversized engines.
==References==
{{Reflist}}
*Robert C. Post, High Performance: The Culture and Technology of Drag Racing, 1950 - 2000 ([[Johns Hopkins University Press]], revised edition 2001)
== Other websites ==
{{commons category-inline|Drag racing}}
* [http://www.andra.com.au/ Australian National Drag Racing Association (ANDRA)]
* [http://www.powerracing.eu/ European Championship Drag Racing (FIA/UEM)]
* [http://www.nhra.com/ National Hot Rod Association (NHRA)]
* [http://www.ihra.com/ International Hot Rod Association (IHRA)] {{Webarchive|url=https://web.archive.org/web/20191030232225/http://www.ihra.com/ |date=2019-10-30 }}
* [http://www.racepra.com/ Pro Racing Association - Championship Volkswagen Drag Racing] {{Webarchive|url=https://web.archive.org/web/20120305155414/http://www.racepra.com/ |date=2012-03-05 }}
* [http://www.speedhunters.com Drag Racing News, Cars & Events from around the world] {{Webarchive|url=https://web.archive.org/web/20120215000836/http://speedhunters.com/ |date=2012-02-15 }}
dummy edit
dummy edit 2
[[Category:Motor sports]]
81bm9aoyeoefatd2jaktk6wdu02boi8
Mat datang
0
169468
746693
691169
2026-06-14T15:39:02Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Mat datang]]
746693
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-06-14}}</noinclude>
[[File:Selamat datang.png|thumb|right|300px|Mat Datang ke [[:w:ms:Wikipedia Bahasa Melayu|Wikipedia Bahasa Melayu]]]][[:w:ms:Wikipedia|Wikipedia]] ni [[:w:ms:ensiklopedia|ensiklopedia]] bebas, kerjasama dari orang ramai yang datang kat sini la yang tolong tulis. Laman ni laman [[Wiki]], maksud dia sape je termasuk ''awak'' boleh ubah ke, sunting ke, setiap rencana yang ada dengan tekan pautan ''sunting'' kat sudut atas setiap rencana kat Wikipedia. Tapi ingat [[:w:ms:Wikipedia:Laku musnah|jangan sunting bukan-bukan]].
== Tengok Wikipedia ==
Kat dalam Wikipedia ni ada banyak rencana yang ada banyak bidang sendiri. Nak tengok, kena la rujuk [[:w:ms:Laman Utama|Laman Utama]], cari mende yang awak rasa menarik, dan mula jelajah. Ada jugak kotak gelintar kat sebelah tepi setiap laman.
Kalau awak jumpa benda menarik pastu awak suka, awak boleh letak nota kat [[:w:ms:Wikipedia:Laman perbincangan|laman berbincang]] tu. Mula-mula pilih pautan ''perbincangan'' (ada kat sudut atas laman) nak pegi ke laman perbincangan, pastu pilih '''+''' kat laman perbincangan (terletak pada sudut atas laman, sebelah pautan '''sunting'''. Kitorang selalu suka kalau dapat maklum balas yang positif.
== Sunting ==
Semua orang boleh sunting laman kat Wikipedia (laman ni jugak!). Pilih je pautan '''Sunting laman ni''' kat puncak ke, kat bawah laman ke, kalau awak rasa boleh baiki lagi. Awak tak payah susah-susah jadi ahli khusus; takyah [[:w:ms:Wikipedia:Bagaimana untuk log masuk|log masuk]] (tapi eloknya kena selalu log masuk (''login'').
Cara mudah nak mula tolong: guna Wikipedia macam awak guna ensiklopedia lain, tetapi bila ada masalah seperti — salah eja ke, ayat tak jelas ke — cer klik "Sunting" pastu baiki la yang salah tu.
[[:w:ms:Wikipedia:Berani dalam mengemaskini laman|Awak cer berani skit diri kalau nak kemas kini laman]]. Kalau awak rasa boleh baiki isi kandungan laman, baiki la laman itu. Takyah takut silap (ramai je penyumbang Wikipedia dah buat silap jugak awal-awal kat Wikipedia). Bila awak silap, nanti awak sendiri ke, orang lain ke, yang baiki kesilapan tu. Awak takut ke? Tengok la [[:w:ms:Wikipedia:Jawapan kritikan|Jawapan kritikan kat Wikipedia]] kalau nak penjelasan kenapa sistem ni berjaya.
Walaupun Wikipedia dah ada banyak rencana (dalam {{NUMBEROFARTICLES}} jugak la sekarang ni), Wikipedia terus bekembang sebab orang duk tambah-tambah jumlah rencana yang dah ada. Semua ni orang macam awak la yang tulis. Awak boleh [[:w:ms:Wikipedia:Bagaimana memulakan laman baru|buat rencana baru]], tak pun baiki rencana-rencana yang dah ada.
Kitorang ada la berapa [[:w:ms:Wikipedia:Polisi dan garis panduan|polisi]] yang kena teliti kut, lagi-lagi [[:w:ms:Wikipedia:Pandangan berkecuali|sudut pandangan berkecuali (NPOV)]], makna dia rencana-rencana yang orang tulis kena neutral, pastu takde berat sebelah, ngan kena tunjuk pandangan berbeza secara objektif ngan tertib. Semua sumbangan Wikipedia terbit bawah [[:w:ms:Lesen Dokumentasi Bebas GNU|Lesen Dokumentasi Bebas GNU (GFDL)]]. Lesen GFDL ni nak pastikan Wikipedia dan rencana-rencananya selalu dapat tengok secara bebas (Nak tahu lagi, tengok [[:w:ms:Wikipedia:Hakcipta|Wikipedia:Hakcipta]]).
nzj1iaz48wf0sykf08pu1633xrov0o1
746696
746693
2026-06-14T15:39:25Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Mat datang]] — Снято с удаления
746696
wikitext
text/x-wiki
[[File:Selamat datang.png|thumb|right|300px|Mat Datang ke [[:w:ms:Wikipedia Bahasa Melayu|Wikipedia Bahasa Melayu]]]][[:w:ms:Wikipedia|Wikipedia]] ni [[:w:ms:ensiklopedia|ensiklopedia]] bebas, kerjasama dari orang ramai yang datang kat sini la yang tolong tulis. Laman ni laman [[Wiki]], maksud dia sape je termasuk ''awak'' boleh ubah ke, sunting ke, setiap rencana yang ada dengan tekan pautan ''sunting'' kat sudut atas setiap rencana kat Wikipedia. Tapi ingat [[:w:ms:Wikipedia:Laku musnah|jangan sunting bukan-bukan]].
== Tengok Wikipedia ==
Kat dalam Wikipedia ni ada banyak rencana yang ada banyak bidang sendiri. Nak tengok, kena la rujuk [[:w:ms:Laman Utama|Laman Utama]], cari mende yang awak rasa menarik, dan mula jelajah. Ada jugak kotak gelintar kat sebelah tepi setiap laman.
Kalau awak jumpa benda menarik pastu awak suka, awak boleh letak nota kat [[:w:ms:Wikipedia:Laman perbincangan|laman berbincang]] tu. Mula-mula pilih pautan ''perbincangan'' (ada kat sudut atas laman) nak pegi ke laman perbincangan, pastu pilih '''+''' kat laman perbincangan (terletak pada sudut atas laman, sebelah pautan '''sunting'''. Kitorang selalu suka kalau dapat maklum balas yang positif.
== Sunting ==
Semua orang boleh sunting laman kat Wikipedia (laman ni jugak!). Pilih je pautan '''Sunting laman ni''' kat puncak ke, kat bawah laman ke, kalau awak rasa boleh baiki lagi. Awak tak payah susah-susah jadi ahli khusus; takyah [[:w:ms:Wikipedia:Bagaimana untuk log masuk|log masuk]] (tapi eloknya kena selalu log masuk (''login'').
Cara mudah nak mula tolong: guna Wikipedia macam awak guna ensiklopedia lain, tetapi bila ada masalah seperti — salah eja ke, ayat tak jelas ke — cer klik "Sunting" pastu baiki la yang salah tu.
[[:w:ms:Wikipedia:Berani dalam mengemaskini laman|Awak cer berani skit diri kalau nak kemas kini laman]]. Kalau awak rasa boleh baiki isi kandungan laman, baiki la laman itu. Takyah takut silap (ramai je penyumbang Wikipedia dah buat silap jugak awal-awal kat Wikipedia). Bila awak silap, nanti awak sendiri ke, orang lain ke, yang baiki kesilapan tu. Awak takut ke? Tengok la [[:w:ms:Wikipedia:Jawapan kritikan|Jawapan kritikan kat Wikipedia]] kalau nak penjelasan kenapa sistem ni berjaya.
Walaupun Wikipedia dah ada banyak rencana (dalam {{NUMBEROFARTICLES}} jugak la sekarang ni), Wikipedia terus bekembang sebab orang duk tambah-tambah jumlah rencana yang dah ada. Semua ni orang macam awak la yang tulis. Awak boleh [[:w:ms:Wikipedia:Bagaimana memulakan laman baru|buat rencana baru]], tak pun baiki rencana-rencana yang dah ada.
Kitorang ada la berapa [[:w:ms:Wikipedia:Polisi dan garis panduan|polisi]] yang kena teliti kut, lagi-lagi [[:w:ms:Wikipedia:Pandangan berkecuali|sudut pandangan berkecuali (NPOV)]], makna dia rencana-rencana yang orang tulis kena neutral, pastu takde berat sebelah, ngan kena tunjuk pandangan berbeza secara objektif ngan tertib. Semua sumbangan Wikipedia terbit bawah [[:w:ms:Lesen Dokumentasi Bebas GNU|Lesen Dokumentasi Bebas GNU (GFDL)]]. Lesen GFDL ni nak pastikan Wikipedia dan rencana-rencananya selalu dapat tengok secara bebas (Nak tahu lagi, tengok [[:w:ms:Wikipedia:Hakcipta|Wikipedia:Hakcipta]]).
r115s6ivo3fkqi3wxx69dcpky9uylob
User:Solidest/remover-core.js
2
174846
746660
745998
2026-06-14T14:53:25Z
Solidest
54422
746660
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:block;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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="Удалить фразу">×</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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
1vlklqt1bss7ybfgv6ilrzd295ucdnk
746664
746660
2026-06-14T14:55:36Z
Solidest
54422
746664
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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' : ''
});
syncActionWarnings('rmCloseAction', 'rmCloseActions');
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
jnk4hl5yzbxgas03bnlj0p1rr4kiro3
746669
746664
2026-06-14T15:04:12Z
Solidest
54422
746669
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автоматически нажимать «Обновить страницу» после каждого действия<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Это помешает взаимодействовать с логом действий после завершения.</span>') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'После номинации открывать созданное обсуждение в новой вкладке') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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' : ''
});
syncActionWarnings('rmCloseAction', 'rmCloseActions');
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
o961ad7cacmjb4dc79aan1ttkvxnqyw
746680
746669
2026-06-14T15:18:01Z
Solidest
54422
746680
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после действий<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий после выполнения.</span>') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать обсуждение после номинации') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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' : ''
});
syncActionWarnings('rmCloseAction', 'rmCloseActions');
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
cxjzupgixdlv33c1xtpc6govah81qva
746683
746680
2026-06-14T15:36:17Z
Solidest
54422
746683
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
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);
syncActionWarnings(opts.inputName, opts.listId);
});
}
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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
tizvrm4p714l7umhkbcaaj0s3bgvmr3
746699
746683
2026-06-14T15:41:21Z
Solidest
54422
746699
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
var opened;
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
opened = window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank');
if (!opened) return;
try { opened.blur(); } catch (e) {}
try { opened.opener = null; } catch (e2) {}
try { window.focus(); } catch (e3) {}
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
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);
syncActionWarnings(opts.inputName, opts.listId);
});
}
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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
/**
* 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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
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);
syncActionWarnings(opts.inputName, opts.listId);
});
}
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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
d3xk94dmiqu2twt3i64252fhs7d6yym
746703
746699
2026-06-14T15:43:22Z
Solidest
54422
746703
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var namePattern = escapeRegExp(String(templateName).trim()).replace(/\s+/g, '[ _]*');
var tplRe = new RegExp('\\{\\{\\s*(?:subst\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', 'i');
var tplMatch = source.match(tplRe);
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE, '\n'), status: 'created' };
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
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);
syncActionWarnings(opts.inputName, opts.listId);
});
}
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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
tizvrm4p714l7umhkbcaaj0s3bgvmr3
746706
746703
2026-06-14T16:27:48Z
Solidest
54422
746706
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 DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
var TALK_TEMPLATE_ALIASES = {
'Оставлено': ['Оставлена', 'Kept'],
'Заменено перенаправлением': ['Удалено', 'Перенаправлено'],
'Переименовано': ['Renamed'],
'Обсуждавшаяся категория': ['Обсуждалась', 'Обсуждалось']
};
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 = /^(?:шаблон|template)\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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
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 toTemplateNameList(names) {
if (Array.isArray(names)) return names;
return String(names || '').split('|');
}
function getTalkTemplateAliases(templateName) {
return TALK_TEMPLATE_ALIASES[templateName] || [];
}
function buildTemplateNamePattern(names) {
var seen = {};
var patterns = [];
toTemplateNameList(names).forEach(function (name) {
var normalized = normalizeTemplateName(String(name || '').replace(/^(?:safe)?subst\s*:\s*/i, ''));
if (!normalized || seen[normalized]) return;
seen[normalized] = true;
patterns.push(escapeRegExp(normalized).replace(/\s+/g, '[ _]*'));
});
return patterns.join('|');
}
function buildTemplateTransclusionRegex(templateName, aliases, flags) {
var names = [templateName].concat(getTalkTemplateAliases(templateName), toTemplateNameList(aliases));
var namePattern = buildTemplateNamePattern(names);
return namePattern
? new RegExp('\\{\\{\\s*(?:(?:safe)?(?:subst|подст)\\s*:\\s*)?(?:(?:шаблон|template)\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', flags || 'i')
: null;
}
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 getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(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,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
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,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
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 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 '[[:' + normalizeCategoryPageName(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 validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, 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(/^(?:safe)?(?:subst|подст)\s*:\s*/i, '').replace(RE_TEMPLATE_NS, '').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 pattern = buildTemplateNamePattern(aliases);
if (!pattern) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + pattern + ')');
}
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 getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var tplRe = buildTemplateTransclusionRegex(templateName, opts.aliases);
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\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 + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
function getNamedTemplateParam(params, names) {
var wanted = {};
names.forEach(function (name) { wanted[String(name).toLowerCase()] = true; });
for (var i = 0; i < params.length; i++) {
var m = String(params[i] || '').match(/^\s*([^=]+?)\s*=\s*([\s\S]*)$/);
if (m && wanted[m[1].toLowerCase().trim()]) return m[2].trim();
}
return '';
}
function getSingleResultTemplateDate(paramsText, templateName) {
var params = String(paramsText || '').split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var namedDate = getNamedTemplateParam(params, templateName === 'Переименовано' ? ['date', 'дата'] : ['date']);
var positionalDate = params.filter(function (p) { return p.indexOf('=') === -1; })[0] || '';
return convertToStandardDate(namedDate || positionalDate);
}
function upsertSingleDateResultTemplateOnTalkPage(text, templateName, dateIso, paramsText) {
var source = text || '';
var tplRe = buildTemplateTransclusionRegex(templateName, null, 'ig');
var tplMatch = null;
var match;
var newTpl = T_OPEN + templateName + '|' + dateIso + (paramsText ? '|' + paramsText : '') + T_CLOSE;
var stdDate = convertToStandardDate(dateIso);
var existingDate;
var canonicalTpl;
if (tplRe) {
while ((match = tplRe.exec(source)) !== null) {
existingDate = getSingleResultTemplateDate(match[2], templateName);
if (existingDate === stdDate) { tplMatch = match; break; }
if (match[0].length === 0) tplRe.lastIndex++;
}
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, newTpl, '\n'), status: 'created' };
}
canonicalTpl = T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + T_CLOSE;
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
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 = buildTemplateTransclusionRegex('Условно оставлено');
var tplMatch = tplRe ? source.match(tplRe) : null;
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 .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#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 buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
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();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} 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>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── 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">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</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 syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
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="Удалить фразу">×</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);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#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'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').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', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</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);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
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 = buildTemplateNamePattern(job.sourceTemplate);
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);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
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' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: upsertSingleDateResultTemplateOnTalkPage(sourceTalkText, job.resultTemplate, date[0], tplpar);
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : 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,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
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((meta && meta.successMessage) || ('Шаблон снят со страницы ' + 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 = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
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'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
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());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
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));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
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() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { 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,
redirectTarget: redirectTarget,
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 = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
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>' +
' · <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));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
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);
syncActionWarnings(opts.inputName, opts.listId);
});
}
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 = normalizeCategoryPageName(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('дополнение шаблона объединения ' + formatCatLink(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 talkResult = upsertDateSectionTemplateOnTalkPage(text, 'Обсуждавшаяся категория', templateDate, '', { aliases: cfg.categoryTemplates.discussed });
var newText = talkResult.text;
if (newText === text) { cb(null, { status: talkResult.status || 'no_changes' }); return; }
var summary = talkResult.status === 'created'
? makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
: makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate);
var ep = {
title: talkPage,
text: newText,
summary: summary
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: talkResult.status } : 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) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { 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 };
}());
b5chjdaw2vzfgetrwj03vm9ou78bx96
User:ABx11/common.js
2
175058
746726
745111
2026-06-15T00:18:45Z
ABx11
73590
746726
javascript
text/javascript
mw.loader.load( '//test.wikipedia.org/wiki/User:ABx11/AB11.js?action=raw&ctype=text/javascript' );
aoub9paia0zv4g7lbeqinh34wf1t57j
User:ABx11/AB11.js
2
175448
746727
745113
2026-06-15T00:19:04Z
ABx11
73590
746727
javascript
text/javascript
//<nowiki>
$(function(){
var searching = false;
function openSearch(){
if(searching){
return;
}
searching = true;
let searchWrapper = document.createElement("div");
searchWrapper.id = "search-wrapper";
searchWrapper.style.width = "400px";
searchWrapper.style.padding = "10px";
searchWrapper.style.height = "200px";
searchWrapper.style.color = "rgb(235, 235, 235)";
searchWrapper.style.position = "fixed";
searchWrapper.style.top = "50%";
searchWrapper.style.left = "50%";
searchWrapper.style.transform = "translate(-50%, -50%)";
document.body.appendChild(searchWrapper);
}
document.body.addEventListener("keydown",
function(e) {
console.log(e);
if (e.key == "F" && e.shiftKey && e.ctrlKey) {
alert("Search time!");
openSearch();
e.preventDefault();
}
});
});
//</nowiki>
6rk0lmnrye9lba80fxaw3ohcn1ygd24
746728
746727
2026-06-15T00:21:37Z
ABx11
73590
746728
javascript
text/javascript
//<nowiki>
$(function(){
var searching = false;
function openSearch(){
if(searching){
return;
}
searching = true;
let searchWrapper = document.createElement("div");
searchWrapper.id = "search-wrapper";
searchWrapper.style.width = "400px";
searchWrapper.style.padding = "10px";
searchWrapper.style.height = "200px";
searchWrapper.style.color = "rgb(235, 235, 235)";
searchWrapper.style.position = "fixed";
searchWrapper.style.top = "50%";
searchWrapper.style.left = "50%";
searchWrapper.style.transform = "translate(-50%, -50%)";
searchWrapper.style.zIndex = "10001";
document.body.appendChild(searchWrapper);
}
document.body.addEventListener("keydown",
function(e) {
console.log(e);
if (e.key == "F" && e.shiftKey && e.ctrlKey) {
alert("Search time!");
openSearch();
e.preventDefault();
}
});
});
//</nowiki>
k4hx6b4eix1j44q3tdqx59we1dkm4oh
Википедия:Обсуждение категорий/Июнь 2026
0
175766
746671
745984
2026-06-14T15:05:44Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление обсуждения для категории [[:Category:Legend]]
746671
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 14 июня 2026 ==
=== [[:Category:Legend]] ===
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:05, 14 June 2026 (UTC)
== 11 июня 2026 ==
=== Tests ===
==== [[:Category:Test-D]] → [[:Category:Test-2d]], [[:Category:Test-3d]] ====
==== [[:Category:Testus]] → [[:Category:Testus2]] ====
Comment [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 17:23, 11 June 2026 (UTC)
==== По всем ====
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 17:23, 11 June 2026 (UTC)
== 2 июня 2026 ==
=== Тест ===
==== [[:Category:Kakostek]] ====
comment1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 23:17, 2 June 2026 (UTC)
==== [[:Category:Legend]] ====
comment2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 23:17, 2 June 2026 (UTC)
==== По всем ====
comment3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 23:17, 2 June 2026 (UTC)
== 1 июня 2026 ==
=== Tests ===
==== [[:Category:Testing]] → [[:Category:Tester2]], [[:Category:Tester3]] ====
==== [[:Category:Test]] → [[:Category:Test1]] ====
==== [[:Category:Tes]] → [[:Category:Test2]] ====
==== По всем ====
Test3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 01:18, 1 June 2026 (UTC)
59gz7l70dquwme4y7yy7nj4si3w6gn0
Talk:Projectwiki
1
175870
746665
745999
2026-06-14T14:57:15Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Projectwiki]] — Заменено перенаправлением на [[Test]]
746665
wikitext
text/x-wiki
{{Заменено перенаправлением|2026-06-14}}
{{Снято с удаления|2026-06-04}}
a51o914v9k3v0ah5c4qc5dpfkv2vnwd
File:23-1078-3047. ваава.pdf
6
176199
746647
2026-06-14T12:22:38Z
Kanzat
47979
Завантаження через Вікіархіватор
746647
wikitext
text/x-wiki
=={{int:filedesc}}==
{{Information
|description={{uk|1=Фонд 123, опис 1078, справа 3047. ваава}}
|date=1900
|source={{Archive Ukraine|ЦДІАК|123|1078|3047}}
|author={{author|unknown}}
}}
=={{int:license-header}}==
{{PD-scan|PD-old-assumed-expired}}
ie1gsej5h7soshnpx3x9alpy4apskfk
Архів:ЦДІАК/123/1078/3047
0
176200
746648
2026-06-14T12:22:40Z
Kanzat
47979
Додано посилання на Commons
746648
wikitext
text/x-wiki
{{Архіви/справа
| назва = ваава
| рік = 1900
| link_commons = File:23-1078-3047. ваава.pdf
| примітки =
}}
iknmtgeumqet1k9g8uk7kd3rzhl58eu
Архів:ЦДІАК/123/1078
0
176201
746649
2026-06-14T12:22:41Z
Kanzat
47979
Додано справу 3047
746649
wikitext
text/x-wiki
{{Архіви/опис
| назва =
| рік =
| примітки =
}}
== Справи ==
{| class="wikitable sortable"
!№!!Назва!!Роки!!Сторінки!!Примітки
|-
|[[/1/]]||||||||
|-
|[[/3047/]]||ваава||1900||||
|}
7hp7zxiain4lp7g90hmd3oju9lxvaja
Архів:ЦДІАК/123
0
176202
746650
2026-06-14T12:22:42Z
Kanzat
47979
Додано опис 1078
746650
wikitext
text/x-wiki
{{Архіви/фонд
| назва =
| примітки =
}}
== Описи ==
{| class="wikitable sortable"
!№!!Анотація!!Крайні дати!!Справ
|-
|[[/1/]]||||||
|-
|[[/1078/]]||||||
|}
kvvt5nkuutf1n4x667lvbltwo79f2w7
Архів:ЦДІАК
0
176203
746651
2026-06-14T12:22:43Z
Kanzat
47979
Додано фонд 123
746651
wikitext
text/x-wiki
{{заголовок
| назва = [[../]]
| автор =
| секція = Центральний державний історичний архів України, м. Київ
| попередня =
| наступна =
| примітки =
}}
== Фонди ==
{| class="wikitable sortable"
!№||Назва фонду||Крайні дати||Справ
|-
|[[../1/]]||||||
|-
|[[../123/]]||||||
|}
m7hor1mvsphdb300ct6dvcsy1z53hxo
746656
746651
2026-06-14T12:28:48Z
Kanzat
47979
Додано фонд 127
746656
wikitext
text/x-wiki
{{заголовок
| назва = [[../]]
| автор =
| секція = Центральний державний історичний архів України, м. Київ
| попередня =
| наступна =
| примітки =
}}
== Фонди ==
{| class="wikitable sortable"
!№||Назва фонду||Крайні дати||Справ
|-
|[[../1/]]||||||
|-
|[[../123/]]||||||
|-
|[[../127/]]||||||
|}
cy9z7ax3qcy6xr0i5n2vu7eu6hes4q5
File:27-1078-3047. 1900. dfd.pdf
6
176204
746652
2026-06-14T12:28:45Z
Kanzat
47979
Завантаження через Вікіархіватор
746652
wikitext
text/x-wiki
=={{int:filedesc}}==
{{Information
|description={{uk|1=Фонд 127, опис 1078, справа 3047. dfd}}
|date=1900
|source={{Archive Ukraine|ЦДІАК|127|1078|3047}}
|author={{author|unknown}}
}}
=={{int:license-header}}==
{{PD-scan|PD-old-assumed-expired}}
1j2ogv76dhd3r1ssd6868287hukomzn
Архів:ЦДІАК/127/1078/3047
0
176205
746653
2026-06-14T12:28:46Z
Kanzat
47979
Додано посилання на Commons
746653
wikitext
text/x-wiki
{{Архіви/справа
| назва = dfd
| рік = 1900
| link_commons = File:27-1078-3047. 1900. dfd.pdf
| примітки =
}}
5bw89t047gbuwr4jm5el4zl1tnrom5p
Архів:ЦДІАК/127/1078
0
176206
746654
2026-06-14T12:28:47Z
Kanzat
47979
Додано справу 3047
746654
wikitext
text/x-wiki
{{Архіви/опис
| назва =
| рік =
| примітки =
}}
== Справи ==
{| class="wikitable sortable"
!№!!Назва!!Роки!!Сторінки!!Примітки
|-
|[[/1/]]||||||||
|-
|[[/3047/]]||dfd||1900||||
|}
hdd656vj0j5ou5jx9s8dvtdfffwk83v
Архів:ЦДІАК/127
0
176207
746655
2026-06-14T12:28:48Z
Kanzat
47979
Додано опис 1078
746655
wikitext
text/x-wiki
{{Архіви/фонд
| назва =
| примітки =
}}
== Описи ==
{| class="wikitable sortable"
!№!!Анотація!!Крайні дати!!Справ
|-
|[[/1/]]||||||
|-
|[[/1078/]]||||||
|}
kvvt5nkuutf1n4x667lvbltwo79f2w7
Википедия:К удалению/14 июня 2026
0
176208
746662
2026-06-14T14:53:44Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: автоматическая шапка
746662
wikitext
text/x-wiki
{{КУ-Навигация}}
0i38svaw3w45s8vulws0qczofnxlk6l
746663
746662
2026-06-14T14:53:45Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Projectwiki]]
746663
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
d7us1g8254j2fymrsexkkwz5kzbfwiu
746677
746663
2026-06-14T15:06:32Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Test]]
746677
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
3qv0f25yncrwlz6mms2jh0cvikgv9pz
746682
746677
2026-06-14T15:18:13Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Test]]
746682
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
0afkv91ya0qibq6oerqd1zsl0tc3e48
746685
746682
2026-06-14T15:37:33Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746685
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:37, 14 June 2026 (UTC)
jduo4e7bwff390qmc9f3g3vrpncwyrw
746692
746685
2026-06-14T15:38:44Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746692
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:37, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:38, 14 June 2026 (UTC)
8rzoo1fdtkjjivk3c7kq2yjv960gl27
746694
746692
2026-06-14T15:39:02Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Mat datang]]
746694
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:37, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:38, 14 June 2026 (UTC)
== [[:Mat datang]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:39, 14 June 2026 (UTC)
m1gqoasyv2azfmvzyoz311ho207kd46
746698
746694
2026-06-14T15:39:42Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Skipple]]
746698
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:37, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:38, 14 June 2026 (UTC)
== [[:Mat datang]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:39, 14 June 2026 (UTC)
== [[:Skipple]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:39, 14 June 2026 (UTC)
l2mu1q1ha3oad9yphba2bn0glfn99p4
746702
746698
2026-06-14T15:41:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К удалению/14 июня 2026#Tto page 6]]
746702
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Projectwiki]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 14:53, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:06, 14 June 2026 (UTC)
== [[:Test]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:18, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:37, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:38, 14 June 2026 (UTC)
== [[:Mat datang]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:39, 14 June 2026 (UTC)
== [[:Skipple]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:39, 14 June 2026 (UTC)
== [[:Tto page 6]] ==
test new [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:41, 14 June 2026 (UTC)
2irykn1nftl9kl9lweqf2dwolubhrce
Category talk:Legend
15
176209
746673
2026-06-14T15:05:59Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона [[ш:Обсуждавшаяся категория]] с датой 2026-06-02
746673
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-06-02}}
rvnr497kqmnaody0cr91ytl53qrpo2k
746675
746673
2026-06-14T15:06:09Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата 2026-06-14
746675
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-06-02|2026-06-14}}
j06geqyp763j24s3ue10z5jucrr6ws9
Википедия:К переименованию/14 июня 2026
0
176210
746687
2026-06-14T15:38:02Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: автоматическая шапка
746687
wikitext
text/x-wiki
{{КПМ-Навигация}}
5lhop28vwygvlrh0r5t1cfhn402d6y9
746688
746687
2026-06-14T15:38:03Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К переименованию/14 июня 2026#Tto page 6 → Teest]]
746688
wikitext
text/x-wiki
{{КПМ-Навигация}}
== [[:Tto page 6]] → [[:Teest]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 15:38, 14 June 2026 (UTC)
mmwwuzx3ps7kgx4qvxt3yechsd9ir0h
Talk:Tto page 6
1
176211
746689
2026-06-14T15:38:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Tto page 6]] — Оставлено
746689
wikitext
text/x-wiki
{{Оставлено|2026-06-14}}
qx8zmqfscl85afqsvinmjyqqr00cdd6
746704
746689
2026-06-14T15:43:38Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Tto page 6]] — Заменено перенаправлением на [[Test]]
746704
wikitext
text/x-wiki
{{Заменено перенаправлением|2026-06-14}}
{{Оставлено|2026-06-14}}
bqxx7f1d0n1p85168rf9b6mxpfdh3ei
Talk:Mat datang
1
176212
746695
2026-06-14T15:39:24Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[ВП:к удалению/14 июня 2026#Mat datang]] — Снято с удаления
746695
wikitext
text/x-wiki
{{Снято с удаления|2026-06-14}}
4fd0cn17gsj5rd1os3bh95dfcikqa6b
Category talk:Test
15
176213
746708
2026-06-14T16:28:13Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона [[ш:Обсуждавшаяся категория]] с датой 2026-05-19
746708
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-05-19}}
bksm11cbbiwm23h227chf2r66hszlpg
746709
746708
2026-06-14T16:28:33Z
Solidest
54422
746709
wikitext
text/x-wiki
{{Обсуждалось|2026-05-19}}
pgbbyypygzd11wkef8bxi16y7kmqcca
746711
746709
2026-06-14T16:28:46Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата 2026-06-01
746711
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-05-19|2026-06-01}}
lln0rfgyz4si0neui48uaddtknwedvj
Wikipedia:Election administrators
4
176214
746712
2026-06-14T19:32:29Z
Khaokha
74426
Google account information
746712
wikitext
text/x-wiki
Kokonyana
6dbrfwow4qg1jf4ki8tux5cd2jt2yoz
Wikipedia talk:Election administrators
5
176215
746713
2026-06-14T19:36:06Z
Khaokha
74426
/* About google account information */ new section
746713
wikitext
text/x-wiki
== About google account information ==
I want to make a new google account [[User:Khaokha|Khaokha]] ([[User talk:Khaokha|talk]]) 19:36, 14 June 2026 (UTC)
1a8rjhhgojy51qeb17z0dtb7sod4xp2
Wikipedia:Articles for deletion/Test
4
176216
746715
2026-06-14T20:02:46Z
Trialpears
43074
Creating deletion discussion page for [[:Test]].
746715
wikitext
text/x-wiki
===[[:Test]]===
{{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD|?}}
<noinclude>{{AFD help}}</noinclude>
:{{la|Test}} – (<includeonly>[[Wikipedia:Articles for deletion/Test|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 14#{{anchorencode:Test}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Test Stats]</span>)
:({{Find sources AFD|Test}})
I propose '''merging''' because this is a test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:02, 14 June 2026 (UTC)
m26thzq1ltf3pcd0m5xco685qovxxox
746718
746715
2026-06-14T20:07:24Z
Trialpears
43074
/* Test */ Reply
746718
wikitext
text/x-wiki
===[[:Test]]===
{{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD|?}}
<noinclude>{{AFD help}}</noinclude>
:{{la|Test}} – (<includeonly>[[Wikipedia:Articles for deletion/Test|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 14#{{anchorencode:Test}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Test Stats]</span>)
:({{Find sources AFD|Test}})
I propose '''merging''' because this is a test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:02, 14 June 2026 (UTC)
:I'm testing but I also want to Merge with test2 [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:07, 14 June 2026 (UTC)
8sotwlzn5zexo9id2gj2mmgxd0brqvq
746719
746718
2026-06-14T23:12:19Z
Angela Criss 55
74427
Replaced content with "Timelash."
746719
wikitext
text/x-wiki
Timelash.
4yyyr9bex1gh2zlnluc18thbixf7xeg
746723
746719
2026-06-14T23:54:17Z
Elton
19642
Reverted edit by [[Special:Contributions/Angela Criss 55|Angela Criss 55]] ([[User talk:Angela Criss 55|talk]]) to last revision by [[User:Trialpears|Trialpears]]
746718
wikitext
text/x-wiki
===[[:Test]]===
{{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD|?}}
<noinclude>{{AFD help}}</noinclude>
:{{la|Test}} – (<includeonly>[[Wikipedia:Articles for deletion/Test|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 14#{{anchorencode:Test}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Test Stats]</span>)
:({{Find sources AFD|Test}})
I propose '''merging''' because this is a test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:02, 14 June 2026 (UTC)
:I'm testing but I also want to Merge with test2 [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:07, 14 June 2026 (UTC)
8sotwlzn5zexo9id2gj2mmgxd0brqvq
Wikipedia:Articles for deletion/Log/2026 June 14
4
176217
746716
2026-06-14T20:02:46Z
Trialpears
43074
Adding [[:Wikipedia:Articles for deletion/Test]].
746716
wikitext
text/x-wiki
{{Recent AfDs}}
<div class="boilerplate metadata vfd" style="background-color: #F3F9FF; margin: 0 auto; padding: 0 1px 0 0; border: 1px solid #AAAAAA; font-size:10px">
{| width = "100%"
|-
! width="50%" align="left" | <span style="color:gray"><</span> [[Wikipedia:Articles for deletion/Log/2026 June 13|June 13]]
! width="50%" align="right" | [[Wikipedia:Articles for deletion/Log/2026 June 15|June 15]] <span style="color:gray">></span>
|}
</div>
<div align = "center">'''[[Wikipedia:Guide to deletion|Guide to deletion]]'''</div>
{{Cent}}
<small>{{purge|Purge server cache}}</small>
__TOC__
<!-- Add new entries to the TOP of the following list -->
{{Wikipedia:Articles for deletion/Test}}
lb2x0nk7nycp3a1pf2kk26ny9aq4gre
User talk:Mbreal
3
176218
746717
2026-06-14T20:02:47Z
Trialpears
43074
Notification: [[Wikipedia:Articles for deletion/Test|listing]] of [[:Test]] at [[WP:Articles for deletion]].
746717
wikitext
text/x-wiki
== Nomination of [[:Test]] for deletion ==
<div class="afd-notice">
<div class="floatleft" style="margin-bottom:0;">[[File:Ambox warning orange.svg|48px|alt=|link=]]</div>A discussion is taking place as to whether the article [[:Test]] is suitable for inclusion in Wikipedia according to [[Wikipedia:List of policies and guidelines|Wikipedia's policies and guidelines]] or whether it should be [[Wikipedia:Deletion policy|deleted]].
The article is being discussed at '''[[Wikipedia:Articles for deletion/Test]]''' until a consensus is reached, and anyone, including you, is welcome to contribute to the discussion. The nomination will explain the policies and guidelines which are of concern. The discussion focuses on high-quality evidence and our policies and guidelines.
Users may edit the article during the discussion, including to address concerns raised in the discussion. However, do not remove the AfD notice from the article until the discussion is closed.<!-- Template:Afd notice --></div> [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:02, 14 June 2026 (UTC)
2g5pp8znk5b1jstb73d2wps8fgegmpa