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/&#126;2026-13668-13|&#126;2026-13668-13]] ([[User talk:&#126;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/&#126;2026-13668-13|&#126;2026-13668-13]] ([[User talk:&#126;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>]] &#91;<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>&#93; • &#91;[[User talk:enbi|talk]]&#93;</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>]] &#91;<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>&#93; • &#91;[[User talk:enbi|talk]]&#93;</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>]] &#91;<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>&#93; • &#91;[[User talk:enbi|talk]]&#93;</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>]] &#91;<span style="color:#9C59D1">they</span>/<span style="color:#FFC107">them</span>&#93; • &#91;[[User talk:enbi|talk]]&#93;</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.}} '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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.}} '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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 '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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.}} '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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 '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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 '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[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/&#126;2026-33346-13|&#126;2026-33346-13]] ([[User talk:&#126;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/&#126;2026-33346-13|&#126;2026-33346-13]] ([[User talk:&#126;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 &mdash; salah eja ke, ayat tak jelas ke &mdash; 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 &mdash; salah eja ke, ayat tak jelas ke &mdash; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons); $('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || ''); $('#rmSettingsSignatureSeparator').val(data.signatureSeparator || ''); $('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', ')); $('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', ')); setQuickPhraseEditorState(data.quickPhrases || [], -1); $('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing'); clearQuickPhraseDropState(); applyMenuTitlePresetControls(menuTitleValue); updateSignatureSeparatorPreview(data.signatureSeparator || ''); } function collectSettingsFormValues(options) { var opts = options || {}; var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val()); var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val()); var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle'); if (namespaces.invalid.length) { return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' }; } if (disabledItems.invalid.length) { return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' }; } if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput(); return { value: normalizeRemoverSettings({ notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'), subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'), showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'), menuTitle: forcePresetOnly ? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string' ? storedMenuTitle : (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset())) : (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()), signatureSeparator: $('#rmSettingsSignatureSeparator').val(), excludedNamespaces: namespaces.values, disabledItems: disabledItems.values, quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues() }) }; } function updateSettingsSubmitReadyState(baselineSettings) { var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true }); var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings))); $('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError')); $('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none'); } function bindSettingsSubmitReadyState(baselineSettings) { var update = function () { setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0); }; $('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update); $('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on( 'click.rmSettingsReady keyup.rmSettingsReady', '.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput', update ); $('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update); updateSettingsSubmitReadyState(baselineSettings); } function buildSettingsFormHtml(menuLabelsHint) { var menuFields = buildSettingsFieldHtml('Заголовок отдельного меню', '<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(), getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) + buildSettingsFieldHtml('Визуальное оформление меню', '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>'); var messageFields = buildSettingsFieldHtml('Префикс перед подписью', '<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>', { forId: 'rmSettingsSignatureSeparator' }) + buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(), 'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' }); var defaultFields = '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') + buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>'; var disableFields = buildSettingsFieldHtml('Скрыть пункты меню', '<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint, { forId: 'rmSettingsDisabledItems' }) + buildSettingsFieldHtml('Не показывать в пространствах имён', '<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.', { forId: 'rmSettingsExcludedNamespaces' }); return joinHtml([ '<div id="rmSettingsForm" style="max-width:100%;">', '<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>', buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'), buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'), buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'), buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'), '</div>' ]); } function buildSettingsFooterLeftHtml() { return joinHtml([ '<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">', '<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>', '<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ', 'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ', 'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>', '</div>' ]); } function openSettings() { var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults); var menuLabelsHint = buildSettingsMenuItemsHint(); var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $(); var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice(); function restorePreviousModal() { closeModal(); if ($previousModal.length) { $('#content').prepend($previousModal); modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice(); if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); syncModalLayout(); syncLinkWidths(); } } createModal({ title: 'Конфигурация', width: 'compact', showSettingsButton: false }); $('#removerModal').addClass('rmModalSettings'); $('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←')); $('#rmSettingsBack').on('click', restorePreviousModal); $('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint)); fillSettingsFormValues(currentSettings); bindMenuTitlePresetControls(); bindSignatureSeparatorPreview(); bindQuickPhrasesEditor(); renderModalFooter('submit', { showCheckbox: false, submitText: 'Сохранить', onSubmit: function () { var collected = collectSettingsFormValues(); var shouldReset; var saveFn; if (collected.error) { alert(collected.error); return false; } shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults); saveFn = shouldReset ? function (callback) { resetSettingsOnServer(callback); } : function (callback) { saveSettingsToServer(collected.value, callback); }; saveFn(function (err) { if (err) { alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.'); unlockModalSubmit(); return; } location.reload(); }); } }); var $settingsActions = $('#rmFooterActionButtons'); $settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>'); $settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>'); bindSettingsSubmitReadyState(currentSettings); $('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml()); $('#rmSettingsResetFooter').on('click', function () { fillSettingsFormValues(settingsDefaults); updateSettingsSubmitReadyState(currentSettings); $('#removerSubmit').trigger('focus'); }); } // ─── Завершение обработки ──────────────────────────────────────────────── function finalizeSuccess(nominationInfo, usePageReload) { if (isError) { var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>'); markSubmitError(); return; } renderModalFooter('reload'); if (nominationInfo && nominationInfo.pageTitle) { appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle); } if (!usePageReload && !nominationInfo) location.reload(); } function finalizeFastRemoval(notifiedPages, summary) { if (isError || !setAlert || !notifiedPages || !notifiedPages.length) { finalizeSuccess(null, false); return; } notifyAuthorsForPages(notifiedPages, { summary: summary, actionText: 'к быстрому удалению' }, function () { finalizeSuccess(null, false); }); } // ─── Общий runner ──────────────────────────────────────────────────────── /** * Универсальный запуск полного пайплайна номинации. * @param {Object} o * templateStep — функция (next) → обработка шаблонов на статьях * nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle}) * notifyStep — функция (nominationInfo, next) * skipNotify — boolean * skipLink — boolean, не показывать ссылку на номинацию */ function runFlow(o) { runNominationPipeline({ templateStep: o.templateStep, nominationStep: o.nominationStep, notifyStep: o.notifyStep || function (info, next) { next(); }, skipNotify: o.skipNotify, onSuccess: function (ctx) { if (isError) { markSubmitError(); return; } renderModalFooter('reload'); if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) { appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle); } }, onFailure: function () { markSubmitError(); } }); } // ═══════════════════════════════════════════════════════════════════════════ // ЯДРО: обработка статей (apply template + nomination page) // ═══════════════════════════════════════════════════════════════════════════ /** * Применяет шаблон к одной статье/категории. * Понимает режим inArticle (вставка через <noinclude>), * режим closeAction (снятие шаблона + запись на СО), * режим cleanupAction (снятие КБУ/КУЛ). * * @param {string} pg — название страницы * @param {Object} job — параметры задания (см. buildJob) * @param {function} callback(err, meta) */ function applyTemplateToPage(pg, job, callback) { var mode = job.mode; // ── Снятие КБУ/КУЛ ────────────────────────────────────────────────── if (mode === 'cleanup') { var tm = job.transferMode || 'none'; if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; } editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var local = removeTransferTemplatesLocal(article, tm); removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) { if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; } done({ text: updated.text }); }); }, function (err) { callback(err); }); return; } // ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ─────────────── if (mode === 'denom') { getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; } if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; } if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; } var tplPattern = job.sourceTemplate.split('|').map(function (alias) { return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tpl = findTemplateByPattern(article, tplPattern); if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; } var normalizedTplDate = convertToStandardDate(tpl.params[0]); var tplExtraParams = tpl.params.slice(1); var tplExtra = tplExtraParams.join('|').trim(); var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams); 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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons); $('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || ''); $('#rmSettingsSignatureSeparator').val(data.signatureSeparator || ''); $('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', ')); $('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', ')); setQuickPhraseEditorState(data.quickPhrases || [], -1); $('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing'); clearQuickPhraseDropState(); applyMenuTitlePresetControls(menuTitleValue); updateSignatureSeparatorPreview(data.signatureSeparator || ''); } function collectSettingsFormValues(options) { var opts = options || {}; var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val()); var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val()); var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle'); if (namespaces.invalid.length) { return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' }; } if (disabledItems.invalid.length) { return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' }; } if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput(); return { value: normalizeRemoverSettings({ notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'), subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'), showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'), menuTitle: forcePresetOnly ? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string' ? storedMenuTitle : (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset())) : (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()), signatureSeparator: $('#rmSettingsSignatureSeparator').val(), excludedNamespaces: namespaces.values, disabledItems: disabledItems.values, quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues() }) }; } function updateSettingsSubmitReadyState(baselineSettings) { var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true }); var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings))); $('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError')); $('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none'); } function bindSettingsSubmitReadyState(baselineSettings) { var update = function () { setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0); }; $('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update); $('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on( 'click.rmSettingsReady keyup.rmSettingsReady', '.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput', update ); $('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update); updateSettingsSubmitReadyState(baselineSettings); } function buildSettingsFormHtml(menuLabelsHint) { var menuFields = buildSettingsFieldHtml('Заголовок отдельного меню', '<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(), getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) + buildSettingsFieldHtml('Визуальное оформление меню', '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>'); var messageFields = buildSettingsFieldHtml('Префикс перед подписью', '<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>', { forId: 'rmSettingsSignatureSeparator' }) + buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(), 'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' }); var defaultFields = '<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') + buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>'; var disableFields = buildSettingsFieldHtml('Скрыть пункты меню', '<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint, { forId: 'rmSettingsDisabledItems' }) + buildSettingsFieldHtml('Не показывать в пространствах имён', '<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">', 'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.', { forId: 'rmSettingsExcludedNamespaces' }); return joinHtml([ '<div id="rmSettingsForm" style="max-width:100%;">', '<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>', buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'), buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'), buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'), buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'), '</div>' ]); } function buildSettingsFooterLeftHtml() { return joinHtml([ '<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">', '<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>', '<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ', 'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ', 'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>', '</div>' ]); } function openSettings() { var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults); var menuLabelsHint = buildSettingsMenuItemsHint(); var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $(); var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice(); function restorePreviousModal() { closeModal(); if ($previousModal.length) { $('#content').prepend($previousModal); modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice(); if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout); syncModalLayout(); syncLinkWidths(); } } createModal({ title: 'Конфигурация', width: 'compact', showSettingsButton: false }); $('#removerModal').addClass('rmModalSettings'); $('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←')); $('#rmSettingsBack').on('click', restorePreviousModal); $('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint)); fillSettingsFormValues(currentSettings); bindMenuTitlePresetControls(); bindSignatureSeparatorPreview(); bindQuickPhrasesEditor(); renderModalFooter('submit', { showCheckbox: false, submitText: 'Сохранить', onSubmit: function () { var collected = collectSettingsFormValues(); var shouldReset; var saveFn; if (collected.error) { alert(collected.error); return false; } shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults); saveFn = shouldReset ? function (callback) { resetSettingsOnServer(callback); } : function (callback) { saveSettingsToServer(collected.value, callback); }; saveFn(function (err) { if (err) { alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.'); unlockModalSubmit(); return; } location.reload(); }); } }); var $settingsActions = $('#rmFooterActionButtons'); $settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>'); $settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>'); bindSettingsSubmitReadyState(currentSettings); $('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml()); $('#rmSettingsResetFooter').on('click', function () { fillSettingsFormValues(settingsDefaults); updateSettingsSubmitReadyState(currentSettings); $('#removerSubmit').trigger('focus'); }); } // ─── Завершение обработки ──────────────────────────────────────────────── function finalizeSuccess(nominationInfo, usePageReload) { if (isError) { var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent'); $box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>'); markSubmitError(); return; } renderModalFooter('reload'); if (nominationInfo && nominationInfo.pageTitle) { appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle); } if (!usePageReload && !nominationInfo) location.reload(); } function finalizeFastRemoval(notifiedPages, summary) { if (isError || !setAlert || !notifiedPages || !notifiedPages.length) { finalizeSuccess(null, false); return; } notifyAuthorsForPages(notifiedPages, { summary: summary, actionText: 'к быстрому удалению' }, function () { finalizeSuccess(null, false); }); } // ─── Общий runner ──────────────────────────────────────────────────────── /** * Универсальный запуск полного пайплайна номинации. * @param {Object} o * templateStep — функция (next) → обработка шаблонов на статьях * nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle}) * notifyStep — функция (nominationInfo, next) * skipNotify — boolean * skipLink — boolean, не показывать ссылку на номинацию */ function runFlow(o) { runNominationPipeline({ templateStep: o.templateStep, nominationStep: o.nominationStep, notifyStep: o.notifyStep || function (info, next) { next(); }, skipNotify: o.skipNotify, onSuccess: function (ctx) { if (isError) { markSubmitError(); return; } renderModalFooter('reload'); if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) { appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle); } }, onFailure: function () { markSubmitError(); } }); } // ═══════════════════════════════════════════════════════════════════════════ // ЯДРО: обработка статей (apply template + nomination page) // ═══════════════════════════════════════════════════════════════════════════ /** * Применяет шаблон к одной статье/категории. * Понимает режим inArticle (вставка через <noinclude>), * режим closeAction (снятие шаблона + запись на СО), * режим cleanupAction (снятие КБУ/КУЛ). * * @param {string} pg — название страницы * @param {Object} job — параметры задания (см. buildJob) * @param {function} callback(err, meta) */ function applyTemplateToPage(pg, job, callback) { var mode = job.mode; // ── Снятие КБУ/КУЛ ────────────────────────────────────────────────── if (mode === 'cleanup') { var tm = job.transferMode || 'none'; if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; } editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' }, function (article, done) { var local = removeTransferTemplatesLocal(article, tm); removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) { if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; } done({ text: updated.text }); }); }, function (err) { callback(err); }); return; } // ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ─────────────── if (mode === 'denom') { getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) { if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; } if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; } if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; } var tplPattern = job.sourceTemplate.split('|').map(function (alias) { return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*'); }).join('|'); var tpl = findTemplateByPattern(article, tplPattern); if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; } var normalizedTplDate = convertToStandardDate(tpl.params[0]); var tplExtraParams = tpl.params.slice(1); var tplExtra = tplExtraParams.join('|').trim(); var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams); 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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText); renderModalFooter('submit', { showCheckbox: opts.showCheckbox, submitText: 'Выполнить', onSubmit: function () { var sel = getSelectedAction(opts.inputName, actionMap); if (!sel) return false; startProcessing(); return opts.onSubmit(sel, pageName, pageText, actionMap) !== false; } }); if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText); }); } function runPageEditWithStatus(opts) { var o = opts || {}; var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false }); editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) { if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; } logStatus(o.successText, null, { statusId: statusId, trackError: false }); if (o.onSuccess) o.onSuccess(meta || null); }); } function removeKuFromCategory(pageName) { runPageEditWithStatus({ pageName: pageName, pendingText: 'Снимается шаблон КУ...', errorText: 'Снятие шаблона КУ.', successText: 'Шаблон КУ снят.', editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' }, buildFn: function (text) { var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)'); if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } }; return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') }; }, onSuccess: function () { logStatus('Шаблон на СО не устанавливался.', null, { trackError: false }); renderModalFooter('reload'); } }); } // ── Категории: добавление шаблона ──────────────────────────────────────── function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) { var cb = callback || function () {}; var cfgByType = { discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss }, rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename }, merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge } }; var typeCfg = cfgByType[type]; if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; } var dateStr = getDate()[0]; var parts = [dateStr]; if (typeCfg.needsMain) { if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; } parts.push(mainName); if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames); } var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE; editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' }, function (text) { if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) { return { skip: true, meta: { status: 'already_present' } }; } return { text: wrapInNoinclude(text, tplText) }; }, function (err, meta) { if (!err && meta && meta.status === 'already_present') { logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false }); } else { logPageEdit(pageName, err); } if (err) { cb(err); return; } if (type === 'merge') { addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); }); return; } cb(null); } ); } function collectCategoryPageInputValues(selector) { var pages = []; $(selector).each(function () { var title = normalizeCategoryPageName($(this).val() || ''); if (title && pages.indexOf(title) === -1) pages.push(title); }); return pages; } function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) { var cb = callback || function () {}; var opts = options || {}; var processedPages = []; eachSequential(pages || [], function (pageName, next) { var pageMainName = mainName; var pageAdditionalNames = additionalNames; var pageTargets; if (type === 'rename' && opts.renameTemplateTargetsByPage) { pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)]; if (Array.isArray(pageTargets)) { pageMainName = pageTargets[0] || mainName; pageAdditionalNames = pageTargets.slice(1); } else if (pageTargets) { pageMainName = pageTargets; pageAdditionalNames = []; } } addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) { if (!err) processedPages.push(pageName); next(err || null); }); }, function (err) { cb(err, processedPages); }); } function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) { var cb = callback || function () {}; var currentCatName = normTitle(stripCatPrefix(sourcePage)); var targets = [mainName].concat(additionalNames || []); if (!targets.length) { cb(); return; } eachSequential(targets, function (target, next) { var targetPage = 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); $('#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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); $('#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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); $('#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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function getDate(dateString) { var d = dateString ? new Date(dateString) : new Date(); var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate()); var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear(); return [iso, rus]; } function convertToStandardDate(dateStr) { var value = String(dateStr || '').replace(/\s+/g, ' ').trim(); var m; var mo; var normalized; m = value.match(RE_DATE_ISO); if (m) return normalizeIsoDate(value) || ''; m = value.match(RE_DATE_RUSSIAN); if (m) { mo = monthToNumber(m[2]); normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null; return normalized || ''; } m = value.match(RE_DATE_DASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } m = value.match(RE_DATE_DOT); if (m) { normalized = makeStandardDate(m[3], m[2], m[1]); return normalized || ''; } m = value.match(RE_DATE_SLASH); if (m) { normalized = makeStandardDate(m[1], m[2], m[3]); return normalized || ''; } return value; } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); $('#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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); } function joinHtml(parts) { return parts.join(''); } function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function padTwo(n) { return n < 10 ? '0' + n : String(n); } function expandTwoDigitYear(value) { return 2000 + parseInt(value, 10); } function monthToNumber(name) { var lower = name.toLowerCase().replace(/\.$/, ''); var idx = MONTHS_GEN.indexOf(lower); if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower); if (idx === -1 && lower.length >= 3) { for (var i = 0; i < MONTHS_GEN.length; i++) { if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1; } } return idx + 1; } function makeStandardDate(yearValue, monthValue, dayValue) { var yearText = String(yearValue || '').trim(); var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10); var month = parseInt(monthValue, 10); var day = parseInt(dayValue, 10); var maxDay; if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null; maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); if (day > maxDay) return null; return year + '-' + padTwo(month) + '-' + padTwo(day); } function normalizeIsoDate(value) { var m = String(value || '').trim().match(RE_DATE_ISO); return m ? makeStandardDate(m[1], m[2], m[3]) : null; } function normalizeTemplateName(name) { return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); } function 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="Удалить фразу">&times;</button>', '</div>' ]); }).join('')); } else { $list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>'); } $input .attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...') .toggleClass('is-editing', state.editingIndex >= 0); $meta .text('') .hide(); } function notifyQuickPhraseEditorChanged() { var $editor = getQuickPhraseEditor(); if ($editor.length) $editor.trigger('rmQuickPhrasesChanged'); } function startQuickPhraseEdit(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length || !$input.length) return; state.editor.data('rmQuickPhraseEditingIndex', index); $input.val(state.phrases[index]); renderQuickPhraseEditor(); $input.trigger('focus'); if ($input[0] && typeof $input[0].select === 'function') $input[0].select(); } function cancelQuickPhraseEdit() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); renderQuickPhraseEditor(); } function saveQuickPhraseInput() { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); var value = normalizeQuickPhraseValue($input.val()); var next = []; if (!$input.length || !value) return false; if (state.editingIndex >= 0) { state.phrases.forEach(function (phrase, index) { if (index === state.editingIndex) { next.push(value); return; } if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase); }); } else { next = state.phrases.slice(); if (next.indexOf(value) === -1) next.push(value); } state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, [])); state.editor.data('rmQuickPhraseEditingIndex', -1); $input.val('').removeClass('is-editing'); clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); return true; } function removeQuickPhrase(index) { var state = getQuickPhraseEditorState(); var $input = $('#rmSettingsQuickPhraseInput'); if (index < 0 || index >= state.phrases.length) return; state.phrases.splice(index, 1); state.editor.data('rmQuickPhrases', state.phrases); if (state.editingIndex === index) { state.editor.data('rmQuickPhraseEditingIndex', -1); if ($input.length) $input.val('').removeClass('is-editing'); } else if (state.editingIndex > index) { state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1); } clearQuickPhraseDropState(); renderQuickPhraseEditor(); notifyQuickPhraseEditorChanged(); } function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) { var result = phrases.slice(); var insertIndex = toIndex + (placeAfter ? 1 : 0); var item; if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result; item = result.splice(fromIndex, 1)[0]; if (fromIndex < insertIndex) insertIndex--; result.splice(insertIndex, 0, item); return result; } function getQuickPhraseDropPointer(evt) { var originalEvent = evt && (evt.originalEvent || evt); if (!originalEvent) return null; if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null; return { x: originalEvent.clientX, y: originalEvent.clientY }; } function getQuickPhraseDropTarget($list, pointer, dragIndex) { var candidates = []; var rowCandidates; var minRowDistance = Infinity; var bestBoundary = null; var bestBoundaryDistance = Infinity; if (!$list || !$list.length || !pointer) return null; $list.children('.rmQuickPhraseChip').each(function () { var index = parseInt($(this).attr('data-rm-quick-index'), 10); var rect; var rowDistance; if (isNaN(index) || index === dragIndex) return; rect = this.getBoundingClientRect(); if (!rect.width || !rect.height) return; rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0); candidates.push({ node: this, index: index, left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, midX: rect.left + rect.width / 2, rowDistance: rowDistance }); if (rowDistance < minRowDistance) minRowDistance = rowDistance; }); if (!candidates.length) return null; rowCandidates = candidates .filter(function (candidate) { return candidate.rowDistance === minRowDistance; }) .sort(function (a, b) { if (a.left !== b.left) return a.left - b.left; return a.index - b.index; }); if (!rowCandidates.length) return null; if (pointer.x <= rowCandidates[0].left) { return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node }; } if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) { return { index: rowCandidates[rowCandidates.length - 1].index, placeAfter: true, node: rowCandidates[rowCandidates.length - 1].node }; } for (var i = 0; i < rowCandidates.length; i++) { var candidate = rowCandidates[i]; if (pointer.x >= candidate.left && pointer.x <= candidate.right) { return { index: candidate.index, placeAfter: pointer.x > candidate.midX, node: candidate.node }; } } rowCandidates.forEach(function (candidate) { var leftDistance = Math.abs(pointer.x - candidate.left); var rightDistance = Math.abs(pointer.x - candidate.right); if (leftDistance < bestBoundaryDistance) { bestBoundaryDistance = leftDistance; bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node }; } if (rightDistance < bestBoundaryDistance) { bestBoundaryDistance = rightDistance; bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node }; } }); return bestBoundary; } function bindQuickPhrasesEditor() { var $editor = getQuickPhraseEditor(); var $list = $('#rmSettingsQuickPhrasesList'); var $input = $('#rmSettingsQuickPhraseInput'); function updateQuickPhraseDropTarget(evt) { var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var pointer = getQuickPhraseDropPointer(evt); var target; if (isNaN(dragIndex) || !pointer) return null; target = getQuickPhraseDropTarget($list, pointer, dragIndex); if (!target) return null; $editor.data('rmQuickPhraseDropIndex', target.index); $editor.data('rmQuickPhraseDropAfter', !!target.placeAfter); $editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after'); $(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before'); return target; } function applyQuickPhraseDrop() { var state = getQuickPhraseEditorState(); var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10); var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10); var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true; var changed = false; if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) { state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter)); if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1); changed = true; } clearQuickPhraseDropState(); renderQuickPhraseEditor(); if (changed) notifyQuickPhraseEditorChanged(); } if (!$editor.length || !$list.length || !$input.length) return; $editor.off('.rmQuickPhraseEditor'); $list.off('.rmQuickPhraseEditor'); $input.off('.rmQuickPhraseEditor'); $input.on('keydown.rmQuickPhraseEditor', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); saveQuickPhraseInput(); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); cancelQuickPhraseEdit(); } }); $editor .on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () { var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); startQuickPhraseEdit(index); }) .on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) { var index; e.preventDefault(); e.stopPropagation(); index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10); removeQuickPhrase(index); }) .on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { var index = parseInt($(this).attr('data-rm-quick-index'), 10); if (isNaN(index)) return; $editor.data('rmQuickPhraseDragIndex', index); $(this).addClass('is-dragging'); if (e.originalEvent && e.originalEvent.dataTransfer) { e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', String(index)); } }) .on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }) .on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () { clearQuickPhraseDropState(); }); $list .on('dragover.rmQuickPhraseEditor', function (e) { if ($editor.data('rmQuickPhraseDragIndex') === undefined) return; e.preventDefault(); updateQuickPhraseDropTarget(e); if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move'; }) .on('drop.rmQuickPhraseEditor', function (e) { e.preventDefault(); updateQuickPhraseDropTarget(e); applyQuickPhraseDrop(); }); renderQuickPhraseEditor(); } function collectQuickPhraseValues() { return getQuickPhraseEditorState().phrases; } function collectQuickPhraseValuesSnapshot() { var state = getQuickPhraseEditorState(); var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val()); var next = state.phrases.slice(); if (!value) return next; if (state.editingIndex >= 0) { next[state.editingIndex] = value; } else if (next.indexOf(value) === -1) { next.push(value); } return normalizeQuickPhrasesList(next, []); } function isMenuTitlePresetValue(value) { return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS; } function isMenuTitlePresetOnlySkin() { return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless'; } function getDefaultMenuTitlePreset() { return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS; } function shouldPreserveStoredMenuTitleOnSave() { return mwCfg.skin === 'minerva'; } function isAvailableMenuTitlePresetValue(value) { return getMenuTitlePresetOptions().some(function (option) { return option.value === value; }); } function getMenuTitlePresetOptions() { if (mwCfg.skin === 'minerva') { return [ { value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' } ]; } if (isVector22) { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' } ]; } if (mwCfg.skin === 'timeless') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' } ]; } if (mwCfg.skin === 'monobook') { return [ { value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } return [ { value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' }, { value: MENU_TITLE_PRESET_PAGE, label: 'Страница' }, { value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' } ]; } function getMenuTitlePresetHintText() { var base = (mwCfg.skin === 'vector' || isVector22) ? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.' : 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.'; if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».'; else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».'; else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».'; else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».'; else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.'; return base; } function getSignatureSeparatorPreviewText(value) { var separator = String(value || '').trim(); return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~'); } function updateSignatureSeparatorPreview(value) { var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || ''); var $code = $('#rmSettingsSignaturePreviewCode'); if (!$code.length) return; $code.text(getSignatureSeparatorPreviewText(previewValue)); } function bindSignatureSeparatorPreview() { var $input = $('#rmSettingsSignatureSeparator'); if (!$input.length) return; $input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () { updateSignatureSeparatorPreview($(this).val()); }); updateSignatureSeparatorPreview($input.val()); } function buildMenuTitlePresetButtonsHtml() { return joinHtml([ '<div class="rmSettingsMenuPresetWrap">', '<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>', '<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">', getMenuTitlePresetOptions().map(function (option) { return joinHtml([ '<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="', option.value, '" aria-pressed="false">', escapeHtml(option.label), '</button>' ]); }).join(''), '</div>', '</div>' ]); } function applyMenuTitlePresetControls(presetValue) { var preset = isMenuTitlePresetValue(presetValue) ? presetValue : ''; var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); var forcePresetOnly = isMenuTitlePresetOnlySkin(); if (!$bar.length || !$input.length) return; $bar.data('rmPreset', preset); $bar.find('.rmSettingsMenuPresetBtn').each(function () { var isActive = $(this).data('rmMenuPreset') === preset; $(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false'); }); $input.prop('disabled', forcePresetOnly || !!preset); } function bindMenuTitlePresetControls() { var $bar = $('#rmSettingsMenuPresetBar'); var $input = $('#rmSettingsMenuTitle'); if (!$bar.length || !$input.length) return; $input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () { if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val()); }); $bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () { var preset = $(this).data('rmMenuPreset'); var currentPreset = $bar.data('rmPreset') || ''; if (currentPreset === preset) { if (isMenuTitlePresetOnlySkin()) return; applyMenuTitlePresetControls(''); $input.val($input.data('rmCustomValue') || ''); $input.trigger('focus'); return; } $input.data('rmCustomValue', $input.val()); applyMenuTitlePresetControls(preset); }); } function fillSettingsFormValues(settings) { var data = normalizeRemoverSettings(settings); var forcePresetOnly = isMenuTitlePresetOnlySkin(); var menuTitleValue = data.menuTitle || ''; var storedMenuTitleValue = menuTitleValue; if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset(); var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue); $('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || ''); $('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor); $('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic); $('#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>' + ' &nbsp;·&nbsp; <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>' : '<span id="rmProtectLinkWrap"></span>' }); $('#removerModalContent').html(buildReportFormHtml(ctx, isZka)); if (!isZka) { $('#removerModalContent') .on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); }) .on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); }) .on('click', '.rmProtectOptBtn', function () { $(this).toggleClass('is-active'); if (protectMode === 'install') { var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active'); if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active'); } if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) { $('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active'); } } applyGeneratedText($('#rmReportText'), buildProtectText(protectMode)); }); $(document) .off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () { var id = 'rmProtectPage' + pageCounter++; $('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false)); updateProtectMultiUi(); }) .off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () { $(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi(); }); applyProtectMode('install'); } setupResizableModal('rmReportText'); renderModalFooter('submit', { showCheckbox: false, showSubscribe: true, submitText: 'Отправить', onSubmit: function () { doReport(ctx, false, protectMode); return true; } }); if (isZka) { $('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit'); $('#rmReportFast').click(function () { if ($('#removerSubmit').data('rmSubmitInProgress')) return; $('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true); $('#rmReportFast').prop('disabled', true); doReport(ctx, true, protectMode); }); } } }; // ═══════════════════════════════════════════════════════════════════════════ // ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории // ═══════════════════════════════════════════════════════════════════════════ function getFastRemoveCriteriaAnchorFromConfig(templateName) { var anchors = cfg.fastRemoveCriteriaAnchors || {}; var template = String(templateName || '').trim(); var lower = template.toLowerCase(); var key; if (!template) return ''; if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template]; for (key in anchors) { if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) { return anchors[key]; } } return ''; } function getFastRemoveCriteriaAnchor(reason) { var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : ''; var template, label, m; if (configured) return configured; template = String(reason && reason[0] || ''); label = String(reason && reason[1] || ''); if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1'; m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/); return m ? m[1] : ''; } function buildFastRemoveCriteriaLinkHtml(reason) { var anchor = getFastRemoveCriteriaAnchor(reason); var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : ''); var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor); return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>'; } function getFastRemoveReasons() { var reasons = cfg.fastRemoveReasons; var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || ''); var all = []; if (isCategory && reasons.categories) all = all.concat(reasons.categories); ['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); }); if (!isCategory && reasons.categories) all = all.concat(reasons.categories); return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; }); } function showCloseActionsModal(opts) { createModal({ title: 'Снятие шаблонов номинаций', inline: true }); $('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>'); var pageName = normTitle(mwCfg.wgPageName); getText(pageName, function (pageText, readErr) { if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; } if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; } var actions = opts.getActions(pageText); if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; } var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {}); $('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId)); $('#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">&lt;</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">&gt;</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