Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.47.0-wmf.4
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
Category:Test
14
38042
744877
743594
2026-06-01T01:17:59Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на переименование
744877
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-19}}
{{Категория к переименованию|2026-06-01|Test1}}
</noinclude>
{{#invoke:Kateqoriya daha çox|main}}
{{subst:cfr|Road to ..}}
[[Category:MainCat]]
[[Category:MainCatOlafTest]]
3e557m2ed2kzr7e9ouxigk4fld9ytmg
Fundamental Rights, Directive Principles and Fundamental Duties of India
0
58413
744883
466244
2026-06-01T10:04:11Z
~2026-23062-17
73557
/* */ showcaptcha
744883
wikitext
text/x-wiki
<div class="metadata topicon" id="protected-icon" style="right:55px;">[[Image:Padlock-silver.svg|20px|link=Wikipedia:Protection policy#semi|This article is semi-protected<nowiki> </nowiki>due to vandalism.|alt=Page semi-protected]]</div>
This is long article title from English Wikipedia.
[[File:Enwiki edit time to block.png|thumb|Time between a reverted edit and a subsequent block on the English Wikipedia.]]
changing stuff.
I'm just here to test adding an image.
af edit
2m0br5u6ioz7yya9c8asraj78lsask9
Category:Tes
14
82729
744878
235749
2026-06-01T01:18:00Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на переименование
744878
wikitext
text/x-wiki
<noinclude>{{Категория к переименованию|2026-06-01|Test2}}</noinclude>
kw9zvai1mge8s5eya60zfdn8k09tnf2
Ancelle
0
87504
744886
744714
2026-06-01T10:23:11Z
~2026-23062-17
73557
showcaptcha
744886
wikitext
text/x-wiki
{{Infobox French commune
|name = Ancelle
|image = Ancelle14.jpg
|caption = The hamlet of St Hillaire, in Ancelle
|image coat of arms = Blason Ancelle.svg
|arrondissement = Gap
|canton = Saint-Bonnet-en-Champsaur
|INSEE = 05004
|postal code = 05260
|mayor = Gilbert Jourdan<ref>{{cite web|title=List of mayors of the Hautes-Alpes department|url=http://www.hautes-alpes.gouv.fr/liste-des-maires-du-departement-des-hautes-alpes-a3958.html|website=Prefecture of Hautes-Alpes|accessdate=20 March 2015|language=French|date=3 June 2014|archive-date=10 January 2015|archive-url=https://web.archive.org/web/20150110014501/http://www.hautes-alpes.gouv.fr/liste-des-maires-du-departement-des-hautes-alpes-a3958.html|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20150110014501/http://www.hautes-alpes.gouv.fr/liste-des-maires-du-departement-des-hautes-alpes-a3958.html |date=10 January 2015 |title=Liste des maires du département des Hautes-Alpes}}</ref>
|term = 2014–2020
|intercommunality =
|longitude = 6.2072
|latitude = 44.6242
|elevation m = 1350
|elevation min m = 1160
|elevation max m = 2779
|area km2 = 50.66
|population = 883
|population date = 2012
|Website = http://www.ancelle.fr/
}}
af edit
'''Ancelle''' is a [[Communes of France|commune]] in the [[Hautes-Alpes]] [[Departments of France|department]] in southeastern [[France]]. The village is a tourist destination for both the summer and winter seasons, offering a range of sporting activities such as hiking, cross country skiing and camping. Ancelle has a small, neighbouring village called Les Taillas, located to the south of the main town centre.
==Population==
{{Historical populations
|align=left
|1962|552
|1968|589
|1975|641
|1982|649
|1990|600
|1999|619
|2008|831
|2012|883
}}
{{clear-left}}
==See also==
*[[Communes of the Hautes-Alpes department]]
==References==
*http://www.ancelle.fr/
{{reflist}}
{{commonscat|Ancelle}}
{{Hautes-Alpes communes}}
{{HautesAlpes-geo-stub}}
test
[[Category:Communes of Hautes-Alpes]]
b9bfqrngmnglfb7h784iw13im18s27g
User:Christoph Jauera (WMDE)/Test
2
94123
744880
674945
2026-06-01T07:51:55Z
Christoph Jauera (WMDE)
29989
744880
wikitext
text/x-wiki
<ref name="N1" details="page 12">Millerw</ref>
{{reflist}}
13e2nl9g1rj1pajt0wwwonqe7f5nnks
Test 1234
0
95192
744882
314298
2026-06-01T10:03:33Z
~2026-23062-17
73557
showcaptcha
744882
wikitext
text/x-wiki
Hello world!
af edit
i9c9tbei7tunr96nb81upcoc44x8zxh
Kekel3
0
100013
744884
353604
2026-06-01T10:04:37Z
~2026-23062-17
73557
showcaptcha
744884
wikitext
text/x-wiki
hello world
af edit
rzcefgm9o8ymdyzpprfm4hi0vbfi4ow
Category:Testing
14
102525
744876
380063
2026-06-01T01:17:58Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на переименование
744876
wikitext
text/x-wiki
<noinclude>{{Категория к переименованию|2026-06-01|Tester2|Tester3}}</noinclude>
Test category
2yly93z9y1plj82agxs2b3753l7zg26
Notre Dame de Lourdes Church (Fall River, Massachusetts)
0
113172
744888
689882
2026-06-01T10:56:44Z
~2026-32727-55
74241
Test
744888
wikitext
text/x-wiki
[[File:NotreDameParishFallRiver.jpg|thumb|Former Notre Dame de Lourdes Church, known as Saint Bernadette Parish from 2012-2018]]
[[File:ND 1911.jpg|thumb|Notre Dame de Lourdes Church (1906-1982)]]
'''Notre Dame de Lourdes''', known from 2012-2018 as '''St. Bernadette Parish''', is a former [[Roman Catholic]] parish in [[Fall River, Massachusetts]]. A part of the [[Roman Catholic Diocese of Fall River]], the parish was established in 1874 to serve the growing [[French-Canadian]] population located in the city's Flint Village test section.<ref>Notre Dame de Lourdes Parish, 125th Anniversary Book, 1999</ref> Since its founding, the parish has occupied three different church buildings; a wooden structure (1874–1893), a spectacular granite church (1906–1982) and the current modern church test (1986-2018). The parish complex over time has also consisted of other multiple buildings, including [[St. Joseph's Orphanage]], The [[Jesus Marie Convent]], [[Notre Dame School (Fall River, Massachusetts)|a school]], the church rectory, the Brothers' residence, and the former Msgr. Prevost High School. The parish also includes Notre Dame [[Cemetery]], located in the city's south end.
In March 2012, the diocese announced that Notre Dame de Lourdes parish would be merging with nearby [[Immaculate Conception]] parish, with the new combined parish renamed '''Saint Bernadette Parish'''.<ref>[http://www.heraldnews.com/news/x1704640458/Merger-of-Notre-Dame-Immaculate-Conception-parishes-is-official Fall River Herald News, March 20, 2012]</ref> This name gives a connection to both parishes as it was [[Bernadette Soubirous|Saint Bernadette]] who was reported to have witnessed the apparition of the Immaculate Conception (the Virgin Mary) at Lourdes, France.
==History==
Notre Dame de Lourdes parish was created in July, 1874 and named after the 1858 [[Our Lady of Lourdes|apparitions of the Blessed Virgin Mary]] at Lourdes, France. The first pastor of the parish was Rev. Pierre Jean-Baptiste Bedard, from [[Montreal]]. The new parish included about 250 [[French-Canadian]] families and about 40 [[Irish American|Irish]] families, who settled to work in the newly established cotton mills located in the east end of [[Fall River, Massachusetts|Fall River]]. The church has been through three buildings in its 144 year history.
==The First Church 1874-1893==
The Parish of Notre Dame de Lourdes first church was a wooden structure located where [[St. Joseph's Orphanage]] is now. The building was erected in just six weeks in the summer of 1874, and measured 45 feet wide by 110 feet long. The parish would remain mixed until 1882, when Immaculate Conception parish was established nearby for the growing [[Irish American|Irish]] population.
By 1888 however, the wooden church was badly dilapidated and no longer fit for use by the large parish, which had grown to 900 families. In 1890, parishioner and noted architect [[Louis G. Destremps]] was asked by Father Prevost to design a new church. The first church was destroyed by fire on November 12, 1893. Masses were then held in a temporary tent until the new church could be used.
==The Second Church 1891-1982==
On May 30, 1891 the cornerstone was laid on what was to be one of the largest and most beautiful churches in New England. In December 1894 the lower church was completed and ready for use, much to the relief of the parish whose wooden church burnt a year earlier. For the new church, Destremps designed "A system of trusses, beams, buttresses and metal rods with turnbuckles" to channel the weight of the roof directly down through the granite walls and leave an unobstructed view of the interior of the church. M.J. Castagnoli, a master sculptor, was commissioned to do the plaster work on the columns, cornices, and bas-reliefs that decorated the church. Master artist Ludovic Cremonini created 18 majestic oil paintings which adorned the walls and ceilings, including his interpretation of Rapheal's "Last Judgement", which was one of the largest paintings in the northeast. In 1906 the giant [[pipe organ]] manufactured by the Brothers Casavant of St. Hyacinthe was installed and in November 29 of that year the church was officially dedicated. At 310 feet, the twin steeples were the [[List of tallest churches in the world|second tallest in the United States]] at the time of their construction. The building of the church cost a total of $315,000.00.{{fact|date=June 2020}} The church building was meant to evoke the [[Notre Dame de Paris]].<ref>{{cite web|url=https://www.metrowestdailynews.com/news/20190416/fire-also-destroyed-fall-rivers-notre-dame-in-1982|title=Fire also destroyed Fall River’s Notre Dame in 1982|newspaper=[[Metrowest Daily News]]|date=2019-04-16|accessdate=2020-06-27|quote=The church, which was dedicated in 1906 and was designed to echo the Parisian landmark,[...]|archive-date=2020-06-29|archive-url=https://web.archive.org/web/20200629003039/https://www.metrowestdailynews.com/news/20190416/fire-also-destroyed-fall-rivers-notre-dame-in-1982|url-status=dead}}</ref>
In 1920 the Sacred Heart Monument was erected on the grounds near the front of the church. It was dedicated to twenty two men from the parish who lost their lives fighting in World War I. Only twenty one names appear on the monument as the twenty second man had enlisted with the Canadian Army. [[Lucien Hippolyte Gosselin]] of Manchester, New Hampshire was chosen as the sculptor of the bronze statues. The bronze work, done in Paris, consisted of The Sacred Heart statue, an Angel cradling a soldier, and three plaques, one for the Army, one for the Navy and the last with the names of the men. The granite base was carved by Honore Savoie of Fall River. The parishioners raised $26,373 to pay for the monument. It is one of two war memorials in the city of Fall River which are not on public land. The other is a bronze plaque on the granite steps of [[St. Patrick's Church (Fall River, Massachusetts)|St. Patrick's Church]] (Good Shepherd Parish).
In 1924 the Carillon bells were installed in south steeple.
On Feb 11, 1934 The Lourdes Grotto is dedicated in the lower church.
In 1938 the hurricane of that year badly shook the steeples and created a danger they might fall. An original plan was to lower them from 310 feet to 160 feet and cut them off just above the belfries. This plan was scrapped and the tall pinnacles were reduced to 235 feet instead; everything above a four-way arch above the clocks was removed and replaced with a shorter, simpler cap with smaller crosses at the top. The simpler octagonal caps were tapered at roughly the same angle as the architecture of the arches, meshing well with them.
The church underwent several more changes over the years until the final restoration in 1982. This restoration was in part to do much needed repair on the building and also to prepare the church to be placed on the [[National Register of Historic Places]]. Unfortunately this never came to be, however [[St. Joseph's Orphanage]], [[Jesus Marie Convent|the JMA]] and [[Notre Dame School (Fall River, Massachusetts)|Notre Dame School]] made it on the register.
==The Fire May 11, 1982==
In 1982 the church enacted a renovation that was to be for $1 million.<ref>{{cite web|url=https://www.nbcboston.com/news/local/fall-river-residents-remember-historic-fire-that-destroyed-city-church/124029/|title=Mass. Residents Remember Historic Fire That Destroyed City's Notre Dame|publisher=[[NBC 10 Boston]]|date=2019-04-16|accessdate=2020-06-27}}</ref> During the restoration of the church while soldering metal gutters,{{fact|date=June 2020}} a workman's blowtorch had accidentally ignited the building,<ref>{{cite web|author=Cullinane, Ashley|url=https://turnto10.com/news/local/fall-rivers-notre-dame-was-also-destroyed-by-fire|title=Fall River's Notre Dame was also destroyed by fire|publisher=[[WJAR]]|date=2019-04-16|accessdate=2020-06-27}}</ref> the roof timbers affected,<ref>{{cite web|author=Marcelo, Philip|url=https://www.boston25news.com/news/construction-fire-also-destroyed-fall-river-s-notre-dame/940674605/|title=Construction fire also destroyed Fall River's Notre Dame|agency=[[Associated Press]]|publisher=[[Boston 25 News]]|date=2019-04-16|accessdate=2020-06-27}}</ref> starting a fire in the south steeple. The dry wood caught quickly and with no fire extinguisher nearby the fire spread in minutes.{{fact|date=June 2020}} The attic first caught fire.<ref name=BostonCBS>{{cite web|url=https://boston.cbslocal.com/2019/04/16/notre-dame-cathedral-fire-fall-river/|title=Notre Dame Cathedral Fire Brings Back Painful Memories In Fall River|publisher=[[Boston CBS]]|date=2019-04-16|accessdate=2020-06-27}}</ref> Carpenters ran to get the Sexton and all raced up the stairs of the tower to try to put the fire out. Running into a wall of thick black smoke they could do nothing. Now it was a race against time to get records, the Eucharist and anything else of value out of the church before the building was engulfed. The fire department responded quickly but their efforts were hampered by strong winds, intense heat from the fire and low water pressure in the hydrants. The fire, fanned by the high wind, spread to nearby buildings and soon engulfed homes and businesses on the next two streets. When the fire finally came under control many buildings were gone. The church was a total loss. The empty granite shell a ruin.
A similar incident occurred in 2006 during restoration of [[Troitsky Cathedral]] in St. Petersburg, Russia. An accident during its restoration caused a fire in which the main dome and a smaller dome burned. Fortunately the main church was saved and the domes were rebuilt.{{fact|date=June 2020}}<!--The fire likely happened, but is there a source saying it is similar? If not, it could be [[WP:ORIGINALRESEARCH]]--> Media outlets compared this fire to the 2019 [[Notre Dame fire]] in [[Paris]].<ref name=BostonCBS/><ref>{{cite web|url=https://www.heraldnews.com/news/20190415/when-fall-rivers-notre-dame-was-also-consumed-by-flames|title=When Fall River’s Notre Dame was also consumed by flames|newspaper=[[The Herald News]]|date=2019-04-15|accessdate=2020-06-26|archive-date=2020-06-28|archive-url=https://web.archive.org/web/20200628224518/https://www.heraldnews.com/news/20190415/when-fall-rivers-notre-dame-was-also-consumed-by-flames|url-status=dead}} - See abreviated version at: "[https://www.southcoasttoday.com/news/20190415/fall-rivers-notre-dame-was-also-consumed-by-flames Fall River’s Notre Dame was also consumed by flames]" ''[[South Coast Today]]'' (same date), from ''[[The Standard-Times]]''</ref>
==The Third Church 1986–2018==
With the loss of the church Masses were moved to [[St. Joseph's Orphanage]] till a new church could be built. Saturday & Sunday masses were held at [[Bishop Connolly High School]]. When the ruins were searched it was found that very little remained from the fire. One stained-glass window, ''The Flight into Egypt'', was spared destruction due to being in Connecticut for repair. The third Notre Dame was designed and built around this window. The three Angels that had adorned the Sanctuary Lamp and two candlesticks, though in the middle of the blaze, managed to survive the inferno. These too are part of the new church. The Sacred Heart Monument also miraculously survived the blaze even though it was in close proximity to the church. It wasn't until 1986 that the new church was built and ready for use. The orientation of the new building is reverse what the old one was. The new churches "front" sits in the footprint of the old churches "rear".{{fact|date=June 2020}}
In 2012, it was revealed that the parish of Notre Dame de Lourdes would be merging with the parish of Immaculate Conception in Fall River. This occurred in June 2012 and they will be using Notre Dame as their church, taking on the name St. Bernadette.<ref>{{cite web|author=Vital, Derek|url=https://www.heraldnews.com/article/20120320/NEWS/303209481|title=Merger of Notre Dame, Immaculate Conception parishes is official|newspaper=[[The Herald News]]|date=2012-03-20|accessdate=2020-06-26}}</ref>
==School==
[[File:Notre Dame School FR.jpg|thumb|[[Notre Dame School (Fall River, Massachusetts)|Notre Dame School]]]]
{{mainarticle|Notre Dame School (Fall River, Massachusetts)}}
Located in the Flint neighborhood, Notre Dame School opened in 1876, with the final building opening in 1890. Its enrollment declined from 1,616 in 1920 to 145 in 2008. It closed in 2008.<ref>{{cite web|author=Allard, Deborah|url=https://www.heraldnews.com/x1743974574/After-132-years-Notre-Dame-School-shuts-its-doors|title=After 132 years, Notre Dame School shuts its doors|newspaper=[[The Herald News]]|date=2008-06-16|accessdate=2020-06-26}}</ref>
==Parish Closing==
In May 2018, only 6 years after the merger, the church faced declining mass attendance and increasing debt.<ref>{{cite web|author=Fraga, Brian|url=https://www.heraldnews.com/news/20180319/facing-mounting-debt-st-bernadette-church-may-be-next-to-close|title=Facing mounting debt, St. Bernadette Church may be next to close|newspaper=[[The Herald News]]|date=2018-03-19|accessdate=2020-06-26|archive-date=2020-06-27|archive-url=https://web.archive.org/web/20200627045929/https://www.heraldnews.com/news/20180319/facing-mounting-debt-st-bernadette-church-may-be-next-to-close|url-status=dead}}</ref> It was announced that St. Bernadette Parish would close and the final Mass was held on August 5, 2018 by Bishop [[Edgar M. da Cunha]].<ref>{{cite web|author=Fraga, Brian|url=https://www.heraldnews.com/news/20180804/st-bernadette-church-prepares-for-final-mass|title=St. Bernadette Church prepares for final Mass|newspaper=[[The Herald News]]|date=2018-08-04|accessdate=2020-06-26|archive-date=2020-06-28|archive-url=https://web.archive.org/web/20200628123245/https://www.heraldnews.com/news/20180804/st-bernadette-church-prepares-for-final-mass|url-status=dead}}</ref>
== See also ==
* [[St. Anne's Church and Parish Complex]]
* [[St. Patrick's Church (Fall River, Massachusetts)]]
== References ==
Notre Dame de Lourdes Memorial Book
A Call to Save - Author - Msgr. Thomas J. Harrington
Fall River Herald News{{which|date=June 2020}}
{{Reflist}}
==Further reading==
* {{cite web|author=Cronin, Daniel A.|authorlink=Daniel Anthony Cronin|url=https://www.heraldnews.com/news/20190415/in-this-hour-of-sorrow-and-distress-what-bishop-of-fall-river-said-after-notre-dame-de-lourdes-fire|title=‘In this hour of sorrow and distress:’ What the Bishop of Fall River said after the Notre Dame de Lourdes fire|newspaper=[[The Herald News]]|date=1982-05-18|access-date=2020-07-06|archive-date=2020-06-29|archive-url=https://web.archive.org/web/20200629085537/https://www.heraldnews.com/news/20190415/in-this-hour-of-sorrow-and-distress-what-bishop-of-fall-river-said-after-notre-dame-de-lourdes-fire|url-status=dead}} - Reposted on April 15, 2019 in the wake of the Paris fire. The author was the bishop of Fall River.
* {{cite web|author=Marcelo, Philip|url=https://www.southcoasttoday.com/news/20190416/fall-river-resident-flashes-back-to-notre-dame-fire|title=Fall River resident flashes back to Notre Dame fire|newspaper=[[South Coast Today]]|date=2019-04-16|access-date=2020-07-06|archive-date=2020-06-29|archive-url=https://web.archive.org/web/20200629230458/https://www.southcoasttoday.com/news/20190416/fall-river-resident-flashes-back-to-notre-dame-fire|url-status=dead}}
== External links ==
* {{webarchive|url=http://web.archive.org/*/http://saintbernadettefallriver.com/|title=St. Bernadette Fall River}}<!--Source:http://web.archive.org/web/20150219050709/http://www.fallriverdiocese.org/parishes.asp?display=All -->
* https://web.archive.org/web/20110727231352/http://www.sailsinc.org/durfee/notredame.pdf
* [https://www.digitalcommonwealth.org/search/commonwealth:00000365f Notre Dame Church, Fall River, Massachusetts] - [[Digital Commonwealth]]
{{Fall River, Massachusetts}}
{{Roman Catholic Diocese of Fall River}}
{{coord|41.6901|-71.1318|display=title}}
[[Category:Roman Catholic churches in Fall River, Massachusetts]]
[[Category:French-Canadian culture in Massachusetts]]
<noinclude>
<small>This page was moved from [[:en:Notre Dame de Lourdes Church (Fall River, Massachusetts)]]. Its edit history can be viewed at [[Notre Dame de Lourdes Church (Fall River, Massachusetts)/edithistory]]</small></noinclude>
qct04rlu48rzj2nq1ui74a4qqj3ztig
User talk:Rachmat04
3
115969
744861
496830
2026-05-31T22:21:02Z
Rachmat04
26096
/* Test for future date */ new section
744861
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
== Test for future date ==
Test. '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 22:21, 31 May 2026 (UTC)
6ykmljkjus4smtomtx4ify92kwd24sr
744862
744861
2026-05-31T22:21:15Z
Rachmat04
26096
/* Test for future date */
744862
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
== Test for future date ==
Test. '''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 22:21, 31 May 2029 (UTC)
eq0gta35fm4kp1dka88dpl2uk6rh2rr
744863
744862
2026-05-31T22:22:32Z
Rachmat04
26096
Restored revision 496830 by [[Special:Contributions/MediaWiki message delivery|MediaWiki message delivery]] ([[User talk:MediaWiki message delivery|talk]])
744863
wikitext
text/x-wiki
== I have sent you a note about a page you reviewed ==
{{subst:Sentnote-NPF|1=Gado-gado|2=Rachmat04|3=Thanks for contributing.}}
'''···''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''·''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 15:35, 26 September 2020 (UTC)
== How we will see unregistered users ==
<section begin=content/>
Hi!
You get this message because you are an admin on a Wikimedia wiki.
When someone edits a Wikimedia wiki without being logged in today, we show their IP address. As you may already know, we will not be able to do this in the future. This is a decision by the Wikimedia Foundation Legal department, because norms and regulations for privacy online have changed.
Instead of the IP we will show a masked identity. You as an admin '''will still be able to access the IP'''. There will also be a new user right for those who need to see the full IPs of unregistered users to fight vandalism, harassment and spam without being admins. Patrollers will also see part of the IP even without this user right. We are also working on [[m:IP Editing: Privacy Enhancement and Abuse Mitigation/Improving tools|better tools]] to help.
If you have not seen it before, you can [[m:IP Editing: Privacy Enhancement and Abuse Mitigation|read more on Meta]]. If you want to make sure you don’t miss technical changes on the Wikimedia wikis, you can [[m:Global message delivery/Targets/Tech ambassadors|subscribe]] to [[m:Tech/News|the weekly technical newsletter]].
We have [[m:IP Editing: Privacy Enhancement and Abuse Mitigation#IP Masking Implementation Approaches (FAQ)|two suggested ways]] this identity could work. '''We would appreciate your feedback''' on which way you think would work best for you and your wiki, now and in the future. You can [[m:Talk:IP Editing: Privacy Enhancement and Abuse Mitigation|let us know on the talk page]]. You can write in your language. The suggestions were posted in October and we will decide after 17 January.
Thank you.
/[[m:User:Johan (WMF)|Johan (WMF)]]<section end=content/>
18:19, 4 January 2022 (UTC)
<!-- Message sent by User:Johan (WMF)@metawiki using the list at https://meta.wikimedia.org/w/index.php?title=User:Johan_(WMF)/Target_lists/Admins2022(7)&oldid=22532681 -->
2vl2rggow7nrvq7c9sf6jiqs1z47yuj
User:Tahmid/sandbox
2
119791
744890
744312
2026-06-01T11:32:24Z
Tahmid
37872
/* */
744890
wikitext
text/x-wiki
__NOTOC__
<div style="margin: 0; padding: 0; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; background-color: #f8f9fa; color: #202124; line-height: 1.6;">
<!-- Top Accent Bar -->
<div style="background-color: #3b82f6; height: 6px; width: 100%;"></div>
<!-- Main Content Container -->
<div style="max-width: 1000px; margin: 40px auto; padding: 0 20px; box-sizing: border-box;">
<!-- Portfolio Hero Banner -->
<div id="overview" style="background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%); border-radius: 24px; padding: 48px; color: #ffffff; margin-bottom: 40px; box-shadow: 0 10px 25px rgba(59, 130, 246, 0.15); position: relative; overflow: hidden;">
<div style="max-width: 700px; position: relative; z-index: 10;">
[[File:Tahmid from Rajshahi (cropped).jpg|thumb]]
<h1 style="font-size: 36px; font-weight: 800; margin: 0 0 16px 0; line-height: 1.2; letter-spacing: -0.5px;">
Md. Tahmid Hossain
</h1>
<p style="font-size: 18px; line-height: 1.6; margin: 0; color: #e0f2fe; text-align: justify;">
Hi, my name is Md. Tahmid Hossain and I hail from Bangladesh. I am an Wikimedian since 2017. I actively work to bridge the global digital divide by making historical, linguistic, and cultural knowledge free and accessible.
</p>
</div>
</div>
<!-- Section: Leadership Roles -->
<div id="roles" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 32px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
Community Leadership & Activism
</h2>
</div>
<!-- Role Card: Wikimedia Bangladesh -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 24px;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Member of Wikimedia Bangladesh
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my ongoing association with Wikimedia Bangladesh, I support the chapter’s active mission of creating deep awareness among the citizens of Bangladesh regarding the presence and value of open knowledge platforms. I work to show individuals how to effectively use open-source content, integrate reliable research tools, and securely navigate these platforms to enhance their academic and personal discovery.
</p>
</div>
<!-- Role Card: Rajshahi Chief Coordinator -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 0;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Chief Coordinator of the Rajshahi Wikimedia Community
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my capacity as Chief Coordinator, I guide our regional team in arranging localized workshops, interactive educational seminars, regular study circles, and technical training programs. We design these activities in active partnerships and strategic cooperation with regional academic institutes, research teams, and interested organizations to systematically expand, share, and promote the use of free, open-source educational resources.
</p>
</div>
</div>
<!-- Section: GLAM Initiative -->
<div id="preservation" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 0; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 24px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
GLAM Preservation & Digitization Initiatives
</h2>
</div>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0 0 20px 0;">
My practical dedication to open archival materials also extends to physical digitization projects. I have worked directly on the preservation of historical out-of-print literature, scanning more than 25,000 pages of copyright-free books. This effort guarantees that critical regional publications are shielded from deterioration and remain permanently accessible to the world.
</p>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
These scanned documents are made available on Wikisource, allowing students, researchers, and book-lovers worldwide to discover and preserve heritage from remote locations. By turning print into search-friendly digital formats, we ensure that Bengali literary resources endure and continue to instruct generation after generation.
</p>
</div>
</div>
</div>
caz3ffxyh53uq7os3yq4d62zrua3ok6
744891
744890
2026-06-01T11:38:41Z
Tahmid
37872
/* */
744891
wikitext
text/x-wiki
__NOTOC__
<div style="margin: 0; padding: 0; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; background-color: #f8f9fa; color: #202124; line-height: 1.6;">
<!-- Top Accent Bar -->
<div style="background-color: #3b82f6; height: 6px; width: 100%;"></div>
<!-- Main Content Container -->
<div style="max-width: 1000px; margin: 40px auto; padding: 0 20px; box-sizing: border-box;">
<!-- Portfolio Hero Banner -->
<div id="overview" style="background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%); border-radius: 24px; padding: 48px; color: #ffffff; margin-bottom: 40px; box-shadow: 0 10px 25px rgba(59, 130, 246, 0.15); position: relative; overflow: hidden;">
<div style="max-width: 700px; position: relative; z-index: 10;">
[[File:Tahmid from Rajshahi (cropped).jpg|thumb]]
<h1 style="font-size: 36px; font-weight: 800; margin: 0 0 16px 0; line-height: 1.2; letter-spacing: -0.5px;">
Md. Tahmid Hossain
</h1>
<div style="font-size: 18px; line-height: 1.6; margin: 0; color: #e0f2fe; text-align: justify;">
<p>I am Md. Tahmid Hossain and I hail from Bangladesh.</p>
<p>I am a dedicated advocate for free knowledge and an enthusiastic contributor to open access resources. I deeply value collaboration and firmly believe in the transforative power of shared information. The Wikimedia movement resonates strongly with my principles of inclusivity, cooperation, and lifelong learning, making it the right platform where I can contribute my skills and time.</p>
<p>I have been actively contributing to Wikimedia projects since 2017, focusing primarily on Bangla Wikipedia, Bangla Wikisource, Wikimedia Commons, and Wikidata. Beyond my contributions, I actively promote the Wikimedia movement and inspire others to participate. I am a member of Wikimedia Bangladesh and currently serve as the Chief Coordinator of the Rajshahi Wikimedia Community.</p>
</div>
</div>
</div>
<!-- Section: Leadership Roles -->
<div id="roles" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 32px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
Community Leadership & Activism
</h2>
</div>
<!-- Role Card: Wikimedia Bangladesh -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 24px;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Member of Wikimedia Bangladesh
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my ongoing association with Wikimedia Bangladesh, I support the chapter’s active mission of creating deep awareness among the citizens of Bangladesh regarding the presence and value of open knowledge platforms. I work to show individuals how to effectively use open-source content, integrate reliable research tools, and securely navigate these platforms to enhance their academic and personal discovery.
</p>
</div>
<!-- Role Card: Rajshahi Chief Coordinator -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 0;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Chief Coordinator of the Rajshahi Wikimedia Community
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my capacity as Chief Coordinator, I guide our regional team in arranging localized workshops, interactive educational seminars, regular study circles, and technical training programs. We design these activities in active partnerships and strategic cooperation with regional academic institutes, research teams, and interested organizations to systematically expand, share, and promote the use of free, open-source educational resources.
</p>
</div>
</div>
<!-- Section: GLAM Initiative -->
<div id="preservation" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 0; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 24px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
GLAM Preservation & Digitization Initiatives
</h2>
</div>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0 0 20px 0;">
My practical dedication to open archival materials also extends to physical digitization projects. I have worked directly on the preservation of historical out-of-print literature, scanning more than 25,000 pages of copyright-free books. This effort guarantees that critical regional publications are shielded from deterioration and remain permanently accessible to the world.
</p>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
These scanned documents are made available on Wikisource, allowing students, researchers, and book-lovers worldwide to discover and preserve heritage from remote locations. By turning print into search-friendly digital formats, we ensure that Bengali literary resources endure and continue to instruct generation after generation.
</p>
</div>
</div>
</div>
7qkex9zc1hoin8io7fz8lp7ffikdilk
744892
744891
2026-06-01T11:39:50Z
Tahmid
37872
/* */
744892
wikitext
text/x-wiki
__NOTOC__
<div style="background-color: #f8f9fa; color: #202124; line-height: 1.6;">
<!-- Top Accent Bar -->
<div style="background-color: #3b82f6; height: 6px; width: 100%;"></div>
<!-- Main Content Container -->
<div style="max-width: 1000px; margin: 40px auto; padding: 0 20px; box-sizing: border-box;">
<!-- Portfolio Hero Banner -->
<div id="overview" style="background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%); border-radius: 24px; padding: 48px; color: #ffffff; margin-bottom: 40px; box-shadow: 0 10px 25px rgba(59, 130, 246, 0.15); position: relative; overflow: hidden;">
<div style="max-width: 700px; position: relative; z-index: 10;">
[[File:Tahmid from Rajshahi (cropped).jpg|thumb]]
<h1 style="font-size: 36px; font-weight: 800; margin: 0 0 16px 0; line-height: 1.2; letter-spacing: -0.5px;">
Md. Tahmid Hossain
</h1>
<div style="font-size: 18px; line-height: 1.6; margin: 0; color: #e0f2fe; text-align: justify;">
<p>I am Md. Tahmid Hossain and I hail from Bangladesh.</p>
<p>I am a dedicated advocate for free knowledge and an enthusiastic contributor to open access resources. I deeply value collaboration and firmly believe in the transforative power of shared information. The Wikimedia movement resonates strongly with my principles of inclusivity, cooperation, and lifelong learning, making it the right platform where I can contribute my skills and time.</p>
<p>I have been actively contributing to Wikimedia projects since 2017, focusing primarily on Bangla Wikipedia, Bangla Wikisource, Wikimedia Commons, and Wikidata. Beyond my contributions, I actively promote the Wikimedia movement and inspire others to participate. I am a member of Wikimedia Bangladesh and currently serve as the Chief Coordinator of the Rajshahi Wikimedia Community.</p>
</div>
</div>
</div>
<!-- Section: Leadership Roles -->
<div id="roles" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 32px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
Community Leadership & Activism
</h2>
</div>
<!-- Role Card: Wikimedia Bangladesh -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 24px;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Member of Wikimedia Bangladesh
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my ongoing association with Wikimedia Bangladesh, I support the chapter’s active mission of creating deep awareness among the citizens of Bangladesh regarding the presence and value of open knowledge platforms. I work to show individuals how to effectively use open-source content, integrate reliable research tools, and securely navigate these platforms to enhance their academic and personal discovery.
</p>
</div>
<!-- Role Card: Rajshahi Chief Coordinator -->
<div style="background-color: #f8f9fa; border-radius: 16px; border: 1px solid #e5e7eb; padding: 28px; margin-bottom: 0;">
<h3 style="font-size: 20px; font-weight: 700; margin: 0 0 12px 0; color: #111827; display: flex; align-items: center; gap: 8px;">
<span style="color: #3b82f6;">◆</span> Chief Coordinator of the Rajshahi Wikimedia Community
</h3>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
In my capacity as Chief Coordinator, I guide our regional team in arranging localized workshops, interactive educational seminars, regular study circles, and technical training programs. We design these activities in active partnerships and strategic cooperation with regional academic institutes, research teams, and interested organizations to systematically expand, share, and promote the use of free, open-source educational resources.
</p>
</div>
</div>
<!-- Section: GLAM Initiative -->
<div id="preservation" style="background-color: #ffffff; border-radius: 20px; border: 1px solid #e5e7eb; padding: 40px; margin-bottom: 0; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
<div style="border-left: 4px solid #3b82f6; padding-left: 16px; margin-bottom: 24px;">
<h2 style="font-size: 24px; font-weight: 700; margin: 0; color: #1e3a8a; letter-spacing: -0.5px;">
GLAM Preservation & Digitization Initiatives
</h2>
</div>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0 0 20px 0;">
My practical dedication to open archival materials also extends to physical digitization projects. I have worked directly on the preservation of historical out-of-print literature, scanning more than 25,000 pages of copyright-free books. This effort guarantees that critical regional publications are shielded from deterioration and remain permanently accessible to the world.
</p>
<p style="font-size: 16px; color: #4a5568; text-align: justify; margin: 0;">
These scanned documents are made available on Wikisource, allowing students, researchers, and book-lovers worldwide to discover and preserve heritage from remote locations. By turning print into search-friendly digital formats, we ensure that Bengali literary resources endure and continue to instruct generation after generation.
</p>
</div>
</div>
</div>
1fkde5t2jcxz76esrixsrtvcx04upiy
Léon Kauffman
0
121870
744885
485702
2026-06-01T10:11:35Z
~2026-23062-17
73557
showcaptcha
744885
wikitext
text/x-wiki
{{Short description|12th Prime Minister of Luxembourg}}af edit{{Infobox officeholder
|name = Léon Kauffman
|image =
|imagesize = 250px
|office1 = 12th [[List of Prime Ministers of Luxembourg|Prime Minister of Luxembourg]]
|term_start1 = 18 June 1917
|term_end1 = 28 September 1918
|monarch1 = [[Marie-Adélaïde, Grand Duchess of Luxembourg|Marie-Adélaïde]]
|predecessor1 = [[Victor Thorn]]
|successor1 = [[Émile Reuter]]
|birth_date = 16 August 1869
|birth_place = [[Luxembourg (city)|Luxembourg]], [[Luxembourg]]
|death_date ={{dda|1952|3|25|1869|8|16|df=yes}}
|death_place = [[Luxembourg (city)|Luxembourg]], [[Luxembourg]]
|party = [[Independent (politician)|Independent]]
|spouse = [[Madeleine Joséphine Franck|Madeleine Franck]]
}}
'''Léon Kauffman''' (16 August 1869 – 25 March 1952)<ref name=":0">Thewes (2011), p. 75</ref> was a [[Luxembourg]]ish [[politician]]. He was the 12th [[Prime Minister of Luxembourg]], serving for one year, from 18 June 1917 until 28 September 1918.<ref name=":0" />
After studying law, in 1893 Kauffman was appointed an ''Attaché'' of the ''Parquet Général'', and then was a justice of the peace in [[Echternach]] from 1898 to 1900. Then he was a senior civil servant from 1902 to 1910. In 1910 he became director of the tax administration<ref name=":0" /> and president of the ''Assurances sociales''. In 1916 he became [[Minister for Finances of Luxembourg|Director-General (Minister) of Finance]], until 1918.<ref name=":0" /> In 1917 there was a crisis within the [[Thorn Ministry]], as the [[Chamber of Deputies (Luxembourg)|Chamber of Deputies]] had withdrawn confidence from agriculture minister [[Michel Welter]].<ref>Thewes (2011), p. 69</ref>
On 19 June 1917 Kauffman put together a Right-Liberal government, in which he was prime minister, as well as the Foreign and Finance Minister.<ref name=":0" /><ref name=":1">Thewes (2011), p. 72</ref> Under this government, changes to the constitution were put into motion which were to introduce [[universal suffrage]]. There were disagreements, however, as the government refused, as the Chamber demanded, to establish the origins of sovereign power "in the nation," instead of "in the person of the Grand Duke", as hitherto.<ref name=":1" /> When it became known that the prime minister had been present at a private visit of the German chancellor [[Georg von Hertling]] to [[Grand Duchess Marie-Adélaïde]] on 16 August 1918, the government was reformed.<ref name=":1" /> Léon Kauffman resigned as prime minister on 28 September 1918. From 1915 to 1945 he was a member, and from 1945 to 1952 he was president of the [[Council of State of Luxembourg|Council of State]].<ref name=":0" /> From 1923 to 1952, he was president of the executive board of the [[Banque Internationale à Luxembourg]].<ref name=":0" />
He died in 1952 in Luxembourg City.<ref name=":0" />
He was married to Madeleine Joséphine Franck, and had one son.
== Footnotes ==
<references />
== References ==
* {{Cite book|title = Les gouvernements du Grand-Duché de Luxembourg depuis 1848|last = Thewes|first = Guy|publisher = Service Information et Presse|year = 2011|isbn = 978-2-87999-212-9|location = Luxembourg}}
{{S-start}}
{{S-off}}
{{S-bef|before=[[Edmond Reiffers]]}}
{{S-ttl|title=[[Minister for Finances of Luxembourg|Director-General for Finances]]
|years=1916–1918}}
{{S-aft|after=[[Alphonse Neyens]]}}
|-
{{S-bef|before=[[Victor Thorn]]|rows=2}}
{{S-ttl|title=[[Prime Minister of Luxembourg]]
|years=1917–1918}}
{{S-aft|after=[[Émile Reuter]]|rows=2}}
|-
{{S-ttl|title=[[Minister for Foreign Affairs of Luxembourg|Director-General for Foreign Affairs]]
|years=1917–1918}}
|-
{{S-bef|before=[[Ernest Hamélius]]}}
{{S-ttl|title=[[President of the Council of State of Luxembourg|President of the Council of State]]
|years=1945–1952}}
{{S-aft|after=[[Félix Welter]]}}
{{End}}
{{Luxembourg Prime Ministers}}
{{Authority control}}
{{DEFAULTSORT:Kauffman, Leon}}
[[Category:Ministers for Finances of Luxembourg]]
[[Category:Prime Ministers of Luxembourg]]
[[Category:Ministers for Foreign Affairs of Luxembourg]]
[[Category:Presidents of the Council of State of Luxembourg]]
[[Category:Members of the Chamber of Deputies of Luxembourg]]
[[Category:Members of the Council of State of Luxembourg]]
[[Category:Party of the Right (Luxembourg) politicians]]
[[Category:Luxembourgian bankers]]
[[Category:Luxembourgian people of World War I]]
[[Category:Luxembourgian Roman Catholics]]
[[Category:1869 births]]
[[Category:1952 deaths]]
[[Category:People from Luxembourg City]]
{{Luxembourg-politician-stub}}
2h6wyly7zj98ixu8ts38gu3ls9te7n0
User:Plantaest/common.js
2
123090
744856
741904
2026-05-31T16:35:06Z
Plantaest
37055
Blanked the page
744856
javascript
text/javascript
phoiac9h4m842xq45sp7s6u21eteeq1
User:SongVĩ.Bot II
2
124239
744857
744800
2026-05-31T17:00:16Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1616 ngày...
744857
wikitext
text/x-wiki
Cập nhật lần cuối: 01-06-2026
Đã 1616 ngày...
ks43ceob0u4e1932ak9owxezu4lymgk
744859
744857
2026-05-31T20:05:32Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1617 ngày...
744859
wikitext
text/x-wiki
Cập nhật lần cuối: 01-06-2026
Đã 1617 ngày...
2n468oehks50c9knwzjjf2iysr1nqae
Edamame beans
0
126203
744871
511489
2026-06-01T01:14:07Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К переименованию/1 июня 2026#Testtt]]
744871
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-01|Edamame beans 2||Edamame beans 3}}</noinclude>
edamame beans are beans.
bean
''bean''
<u>bean</u>
'''bean'''
bean.<syntaxhighlight>
bean = new Bean;
function Bean = {
echo Beans are made from the corn plant.
bean++;
}
</syntaxhighlight>
cksaxjfy8eimpss8cpzc1m4mmq0ktvy
Test
0
155073
744872
744602
2026-06-01T01:14:08Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К переименованию/1 июня 2026#Testtt]]
744872
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-06-01|Test 2}}</noinclude>
{{Info/Música/artista
<nowiki>|</nowiki> nome = Alvin L
<nowiki>|</nowiki> fundo = cantodr_solo
<nowiki>|</nowiki> imagem = Alvin L.webp
<nowiki>|</nowiki> nome completo = Arnaldo Jose Lima Santos
<nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d
| nascimento_cidade = [[Salvador]], [[Bahia]]
| nascimento_país = [[BraTestcomposicoes|titulo=Sob encomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro.
Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test
https://example.url
https://examples.url
https://examplez.url
O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.
== Carreira ==
Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace.
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
bpdhy7erhofpsn0ev6kpa8dibl2fqbo
User:Chaotic Enby
2
168744
744849
680315
2026-05-31T12:39:30Z
Chaotic Enby
58843
+alt
744849
wikitext
text/x-wiki
Test page for things that don't work on subpages!
If you need a user to block for testing purposes, my alt [[User:Lawful Binary]] is right there.
nfu0oq1xn0dc6fna605rqjhaisv5dyi
User:কমলেশ মন্ডল/common.js
2
171554
744881
741650
2026-06-01T09:28:26Z
কমলেশ মন্ডল
72403
744881
javascript
text/javascript
/**
* তালিকা থেকে টেবিল তৈরির সরঞ্জাম
*
* ব্যবহারের নিয়ম:
* ১. যেকোনো পাতা উৎস সম্পাদনা মোডে খুলুন।
* ২. একটি বা একাধিক লাইন নির্বাচন করুন এই ফরম্যাটে:
* পাঠ। পাঠ ← দারি (।) এবং তারপর ২ বা তার বেশি স্পেস
* যেমন: জনতা। ১ চামচ
* ৩. টুলবারে তালিকা→টেবিল বোতামে ক্লিক করুন।
* ৪. পপআপে:
* • উপকরণ উৎস হিসেবে বাম বা ডান বেছে নিন। ডিফল্ট হিসেবে ডান সিলেক্ট করা থাকবে।
* • চাইলে কলামের নামও বদলানো যাবে।
* ৫. ঢোকান ক্লিক করুন — নির্বাচিত টেক্সটের জায়গায় উইকিটেবিল আসবে।
*
* ডিফল্ট আচরণ (ডান নির্বাচিত থাকলে):
* উৎস লাইন: জনতা। ১ চামচ
* ↑ বাম ↑ ডান
*
* তৈরি হওয়া টেবিল:
* | উপকরণ | পরিমাণ |
* | ১ চামচ | জনতা | ← বাম টেক্সট → পরিমাণ কলামে (ডানে)
* ডান টেক্সট → উপকরণ কলামে (বামে)
*
* উৎস টেক্সটে উপকরণের নাম বামে ও পরিমাণ ডানে থাকলে বাম বেছে নিন।
*/
( function () {
'use strict';
if ( ![ 'edit', 'submit' ].includes( mw.config.get( 'wgAction' ) ) ) {
return;
}
var SEP_RE = /।[ \t]{2,}/;
var DEFAULT_COL_UK = 'উপকরণ';
var DEFAULT_COL_POR = 'পরিমাণ';
function getSelection( $ta ) {
var el = $ta[ 0 ];
return el.value.substring( el.selectionStart, el.selectionEnd );
}
function replaceSelection( $ta, newText ) {
var el = $ta[ 0 ];
var start = el.selectionStart;
var end = el.selectionEnd;
el.value = el.value.substring( 0, start ) + newText + el.value.substring( end );
el.selectionStart = start;
el.selectionEnd = start + newText.length;
el.focus();
$( el ).trigger( 'input' );
}
function parseLines( rawText ) {
return rawText.split( '\n' )
.map( function ( l ) { return l.trim(); } )
.filter( function ( l ) { return l && SEP_RE.test( l ); } )
.map( function ( l ) {
var p = l.split( SEP_RE );
return { left: p[ 0 ].trim(), right: ( p[ 1 ] || '' ).trim() };
} );
}
function buildTable( rows, ukFromLeft, colUk, colPor ) {
var lh = ( colUk || DEFAULT_COL_UK ).trim() || DEFAULT_COL_UK;
var rh = ( colPor || DEFAULT_COL_POR ).trim() || DEFAULT_COL_POR;
var lines = [
'{| class="wikitable"',
'|-',
'! ' + lh + ' !! ' + rh
];
rows.forEach( function ( row ) {
var leftCell = ukFromLeft ? row.left : row.right;
var rightCell = ukFromLeft ? row.right : row.left;
lines.push( '|-' );
lines.push( '| ' + leftCell + ' || ' + rightCell );
} );
lines.push( '|}' );
return lines.join( '\n' );
}
function injectStyles() {
if ( $( '#btg-css' ).length ) { return; }
$( '<style id="btg-css">' ).text( [
'#btg-ov{',
'position:fixed;inset:0;',
'background:rgba(0,0,0,.54);',
'z-index:10000;',
'display:flex;align-items:center;justify-content:center;',
'animation:btg-fadein .15s ease;}',
'@keyframes btg-fadein{from{opacity:0}to{opacity:1}}',
'#btg-dlg{',
'background:#ffffff;',
'border-radius:8px;',
'padding:24px 26px 20px;',
'width:400px;max-width:96vw;',
'box-shadow:0 8px 36px rgba(0,0,0,.32);',
'font-family:"Noto Serif Bengali","SolaimanLipi","Kalpurush",serif;',
'animation:btg-slidein .17s cubic-bezier(.22,.61,.36,1);}',
'@keyframes btg-slidein{from{transform:translateY(12px);opacity:0}',
'to{transform:translateY(0);opacity:1}}',
'#btg-dlg h4{',
'margin:0 0 4px;font-size:1.08em;font-weight:700;',
'color:#1a1a2e;letter-spacing:.01em;}',
'.btg-info{font-size:.78em;color:#72777d;margin:0 0 16px}',
'.btg-div{border:none;border-top:1px solid #eaecf0;margin:0 0 16px}',
'.btg-r{display:flex;align-items:center;gap:12px;margin-bottom:14px}',
'.btg-r .lbl{',
'font-size:.88em;font-weight:600;color:#3c4043;',
'min-width:88px;flex-shrink:0;}',
'.btg-tgl{',
'display:flex;',
'border:1.5px solid #c8ccd1;',
'border-radius:20px;overflow:hidden;',
'background:#f8f9fa;}',
'.btg-tgl button{',
'padding:5px 22px;border:none;background:transparent;',
'cursor:pointer;font-size:.84em;',
'font-family:inherit;color:#555;',
'transition:background .15s,color .15s,transform .1s;}',
'.btg-tgl button.on{',
'background:#3366cc;color:#fff;',
'border-radius:20px;',
'box-shadow:0 1px 4px rgba(51,102,204,.4);}',
'.btg-tgl button:hover:not(.on){background:#e0e7ff;color:#3366cc}',
'.btg-inp{',
'padding:5px 9px;',
'border:1.5px solid #c8ccd1;',
'border-radius:5px;',
'font-size:.86em;',
'font-family:inherit;',
'width:100px;',
'color:#202122;',
'transition:border-color .15s,box-shadow .15s;}',
'.btg-inp:focus{',
'outline:none;',
'border-color:#3366cc;',
'box-shadow:0 0 0 2.5px rgba(51,102,204,.2);}',
'.btg-pv-lbl{font-size:.76em;color:#888;margin-bottom:5px;letter-spacing:.03em}',
'.btg-pv{',
'background:#f5f6f7;',
'border:1px solid #dde1e4;',
'border-radius:5px;',
'padding:9px 12px;',
'max-height:128px;overflow:auto;',
'margin-bottom:16px;}',
'.btg-pv pre{',
'margin:0;font-size:.73em;color:#3c4043;',
'font-family:"Courier New",Courier,monospace;',
'white-space:pre;line-height:1.55;}',
'.btg-acts{display:flex;gap:10px;justify-content:flex-end}',
'.btg-acts button{',
'padding:7px 22px;border-radius:6px;cursor:pointer;',
'font-size:.88em;font-family:inherit;',
'font-weight:600;letter-spacing:.02em;',
'border:1.5px solid transparent;',
'transition:background .14s,transform .1s,box-shadow .14s;}',
'.btg-acts button:active{transform:scale(.97)}',
'.btg-ok{',
'background:#3366cc;color:#fff;',
'border-color:#3366cc !important;',
'box-shadow:0 2px 6px rgba(51,102,204,.35);}',
'.btg-ok:hover{background:#2a55b0;box-shadow:0 3px 10px rgba(51,102,204,.45)}',
'.btg-cl{',
'background:#f8f9fa;color:#3c4043;',
'border-color:#c8ccd1 !important;}',
'.btg-cl:hover{background:#eaecf0}'
].join( '' ) ).appendTo( 'head' );
}
function openDialog( $textarea, rows ) {
injectStyles();
$( '#btg-ov' ).remove();
var ukFromLeft = false;
var $ov = $( '<div id="btg-ov">' );
var $dlg = $( '<div id="btg-dlg">' );
$dlg.append( $( '<h4>' ).html( 'তালিকা → উইকিটেবিল' ) );
$dlg.append( $( '<p class="btg-info">' ).text( rows.length + 'টি সারি পাওয়া গেছে' ) );
$dlg.append( $( '<hr class="btg-div">' ) );
var $bam = $( '<button>' ).text( 'বাম' );
var $dan = $( '<button>' ).text( 'ডান' ).addClass( 'on' );
$dlg.append(
$( '<div class="btg-r">' )
.append( $( '<span class="lbl">' ).text( 'উপকরণ উৎস' ) )
.append( $( '<div class="btg-tgl">' ).append( $bam ).append( $dan ) )
);
var $ukName = $( '<input class="btg-inp">' )
.val( DEFAULT_COL_UK )
.attr( 'placeholder', DEFAULT_COL_UK )
.attr( 'title', 'উপকরণ কলামের নাম পরিবর্তন করুন' );
var $porName = $( '<input class="btg-inp">' )
.val( DEFAULT_COL_POR )
.attr( 'placeholder', DEFAULT_COL_POR )
.attr( 'title', 'পরিমাণ কলামের নাম পরিবর্তন করুন' );
$dlg.append(
$( '<div class="btg-r">' )
.append( $( '<span class="lbl">' ).text( 'কলামের নাম' ) )
.append( $ukName )
.append( $porName )
);
var $pre = $( '<pre>' );
function refresh() {
$pre.text( buildTable( rows, ukFromLeft, $ukName.val(), $porName.val() ) );
}
refresh();
$dlg.append( $( '<p class="btg-pv-lbl">' ).text( 'প্রিভিউ (উইকিটেক্সট):' ) );
$dlg.append( $( '<div class="btg-pv">' ).append( $pre ) );
$bam.on( 'click', function () {
ukFromLeft = true;
$bam.addClass( 'on' );
$dan.removeClass( 'on' );
refresh();
} );
$dan.on( 'click', function () {
ukFromLeft = false;
$dan.addClass( 'on' );
$bam.removeClass( 'on' );
refresh();
} );
$ukName.add( $porName ).on( 'input', refresh );
var $ins = $( '<button class="btg-ok">' ).text( 'করুন' );
var $cls = $( '<button class="btg-cl">' ).text( 'বাতিল' );
$ins.on( 'click', function () {
var table = buildTable( rows, ukFromLeft, $ukName.val(), $porName.val() );
replaceSelection( $textarea, table );
$ov.remove();
mw.notify( 'সফলভাবে টেবিল যোগ করা হয়েছে', { type: 'success' } );
} );
$cls.on( 'click', function () { $ov.remove(); } );
$dlg.append( $( '<div class="btg-acts">' ).append( $cls ).append( $ins ) );
$ov.append( $dlg );
$ov.on( 'click', function ( e ) {
if ( e.target === this ) { $ov.remove(); }
} );
$( document ).one( 'keydown.btg', function ( e ) {
if ( e.key === 'Escape' ) { $ov.remove(); }
} );
$( 'body' ).append( $ov );
setTimeout( function () { $bam.trigger( 'focus' ); }, 50 );
}
var ICON_SVG = 'data:image/svg+xml;utf8,' + encodeURIComponent( [
'<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">',
'<rect x="1.5" y="1.5" width="19" height="19" rx="2.5"',
' fill="none" stroke="#555" stroke-width="1.4"/>',
'<rect x="2" y="2" width="18" height="5.5" rx="1.5" fill="#cde"/>',
'<line x1="2" y1="7.5" x2="20" y2="7.5" stroke="#888" stroke-width=".9"/>',
'<line x1="2" y1="13" x2="20" y2="13" stroke="#ccc" stroke-width=".7"/>',
'<line x1="11" y1="7.5" x2="11" y2="20" stroke="#888" stroke-width=".9"/>',
'</svg>'
].join( '' ) );
function addToolbarButton( $textarea ) {
try {
$textarea.wikiEditor( 'addToToolbar', {
section: 'main',
group: 'insert',
tools: {
'btg-list-to-table': {
label: 'তালিকা → টেবিল',
type: 'button',
icon: ICON_SVG,
action: {
type: 'callback',
execute: function ( context ) {
var $ta = context.$textarea;
var sel = getSelection( $ta );
if ( !sel.trim() ) {
mw.notify(
'প্রথমে টেক্সট নির্বাচন করুন। (ফরম্যাট: পাঠ। পাঠ)',
{ type: 'warn' }
);
return;
}
var rows = parseLines( sel );
if ( !rows.length ) {
mw.notify(
'নির্বাচিত টেক্সটে বৈধ বিভাজক (দারি + দুই বা বেশি স্পেস) পাওয়া যায়নি।',
{ type: 'error' }
);
return;
}
openDialog( $ta, rows );
}
}
}
}
} );
} catch ( err ) {
mw.log.warn( '[BTG] টুলবার বোতাম যোগ করা যায়নি:', err );
}
}
mw.loader.using( 'ext.wikiEditor', function () {
mw.hook( 'wikiEditor.toolbarReady' ).add( function ( $textarea ) {
addToolbarButton( $textarea );
} );
} );
}() );
2ojuu2v6vgmhapz7tlnz1ifel9z44o3
User:Solidest/remover-core.js
2
174846
744860
744651
2026-05-31T20:07:30Z
Solidest
54422
744860
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 RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function getCategoryNamespaceLabel() {
return mwCfg.wgFormattedNamespaces[14] || 'Категория';
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К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 '[[:Категория:' + 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 buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
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';
if (comment) hasPageComments = true;
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageSections + '\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 hasPageComments = false;
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : 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 upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
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 = opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : '');
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemPrep = isArticle ? 'статье' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemPrep,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemPrep,
commentPlaceholder: 'Комментарий только для этой ' + itemPrep + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion' || catType === 'rename';
var renameMultiMode = catType === 'rename' && multiMode;
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
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,
deletionMulti: catType === 'deletion',
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 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 notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
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: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон {{ОБК-Навигация}}.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
l47iv15anzk5zswf6it8u44z2wegga1
744870
744860
2026-06-01T01:13:11Z
Solidest
54422
744870
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 RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function getCategoryNamespaceLabel() {
return mwCfg.wgFormattedNamespaces[14] || 'Категория';
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К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 '[[:Категория:' + 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 buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
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';
if (comment) hasPageComments = true;
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageSections + '\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 hasPageComments = false;
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : 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 upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
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 = opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : '');
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemPrep = isArticle ? 'статье' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemPrep,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemPrep,
commentPlaceholder: 'Комментарий только для этой ' + itemPrep + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion' || catType === 'rename';
var renameMultiMode = catType === 'rename' && multiMode;
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
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,
deletionMulti: catType === 'deletion',
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 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 notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
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: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
60mz3f72pp2dxg5qurcdhx3urp0u37m
744875
744870
2026-06-01T01:16:27Z
Solidest
54422
744875
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 RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function getCategoryNamespaceLabel() {
return mwCfg.wgFormattedNamespaces[14] || 'Категория';
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К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 '[[:Категория:' + 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 buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
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';
if (comment) hasPageComments = true;
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageSections + '\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 hasPageComments = false;
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : 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 upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
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 = opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : '');
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemDat = isArticle ? 'статье' : 'категории';
var itemGen = isArticle ? 'статьи' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemDat,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat,
commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion' || catType === 'rename';
var renameMultiMode = catType === 'rename' && multiMode;
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
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,
deletionMulti: catType === 'deletion',
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 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 notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
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: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
5tnkfahv1fly3jak1rvo6c1ldz4dnu7
User:Cryptocurrency777
2
175011
744869
744675
2026-06-01T01:04:10Z
Cryptocurrency777
73698
744869
wikitext
text/x-wiki
{{DISPLAYTITLE:User:<span style="color:Lavender;">'''Cryptocurrency777'''</span>}}
{{Precious topicon}}
[[User:Cryptocurrency777/sandbox|😤]]
[[User:Cryptocurrency777/sandbox1|😤]]
[[User:Cryptocurrency777/sandbox2|😤]]
[[User:Cryptocurrency777/sandbox3|😤]]
[[User:Cryptocurrency777/sandbox4|😤]]
[[User:Cryptocurrency777/sandbox5|😤]]
[[User:Cryptocurrency777/sandbox6|😤]]
[[User:Cryptocurrency777/sandbox7|o]]
[[User:Cryptocurrency777/sandbox8|o]]
50b5az6emyoj4qp4g7bp8zq8xxqin1r
User:MrJaroslavik/ExperimentsWarning.js
2
175380
744887
741964
2026-06-01T10:47:41Z
MrJaroslavik
44012
- [[]]
744887
javascript
text/javascript
// ExperimentsWarning.js
// -------------------------------------------------------
// Vkládá varování na diskusní stránky uživatelů - s možností odeslat jedním kliknutím nebo před odesláním přidat název stránky
// -------------------------------------------------------
// <nowiki>
$(function() {
const EW = {};
window.ExperimentsWarning = EW;
// Spustit pouze ve jmenném prostoru Diskuse s wikipedistou (NS 3)
if (mw.config.get('wgNamespaceNumber') !== 3) return;
const wgPageName = mw.config.get('wgPageName');
EW.summarySuffix = '';
// Seznam varovacích šablon rozdělený do kategorií
const warningGroups = [{
category: 'Experimenty:',
items: [{
id: 'exp1',
label: 'Experimenty',
template: 'Experimenty',
summary: 'Varování: Experimenty'
}, {
id: 'exp2',
label: 'Experimenty2',
template: 'Experimenty2',
summary: 'Varování: Experimenty2'
}, {
id: 'exp3',
label: 'Experimenty3',
template: 'Experimenty3',
summary: 'Varování: Experimenty3'
}]
}, {
category: 'Odstraňování obsahu:',
items: [{
id: 'exp-o',
label: 'Experimenty-o',
template: 'Experimenty-o',
summary: 'Varování: Experimenty-o'
}, {
id: 'exp2o',
label: 'Experimenty2o',
template: 'Experimenty2o',
summary: 'Varování: Experimenty2o'
}, {
id: 'exp3o',
label: 'Experimenty3o',
template: 'Experimenty3o',
summary: 'Varování: Experimenty3o',
nopagetitle: true
}]
}, {
category: 'Odstraňování urg. šablon:',
items: [{
id: 'exp-u',
label: 'Experimenty-u',
template: 'Experimenty-u',
summary: 'Varování: Experimenty-u',
nopagetitle: true
}, {
id: 'exp2u',
label: 'Experimenty2u',
template: 'Experimenty2u',
summary: 'Varování: Experimenty2u',
nopagetitle: true
}, {
id: 'exp3u',
label: 'Experimenty3u',
template: 'Experimenty3u',
summary: 'Varování: Experimenty3u',
nopagetitle: true
}]
}, {
category: 'Další varování:',
items: [{
id: 'vulgarity',
label: 'Vulgarity',
template: 'Vulgarity',
summary: 'Varování: Vulgarity'
}, {
id: 'kompov',
label: 'KomentářPOV',
template: 'KomentářPOV',
summary: 'Varování: KomentářPOV'
}, {
id: 'nahled',
label: 'Náhled',
template: 'Náhled',
summary: 'Varování: Náhled',
nopagetitle: true
}, {
id: 'shrnuti',
label: 'Shrnutí',
template: 'Shrnutí',
summary: 'Varování: Shrnutí',
nopagetitle: true
}, {
id: 'welcome',
label: 'Vítejte',
template: 'Vítejte',
summary: 'Vítejte na Wikipedii!',
sectiontitle: 'Vítejte',
nopagetitle: true
}, {
id: 'welcomeanon',
label: 'Vítejte-anonym',
template: 'Vítejte|a',
summary: 'Vítejte na Wikipedii!',
sectiontitle: 'Vítejte',
nopagetitle: true
}]
}, {
category: 'Po smazání stránky:',
items: [{
id: 'exp0',
label: 'Experimenty0',
template: 'Experimenty0',
summary: 'Varování: Experimenty0'
}, {
id: 'exp0B',
label: 'Experimenty0B',
template: 'Experimenty0B',
summary: 'Varování: Experimenty0B'
}, {
id: 'expup',
label: 'ExperimentyUP',
template: 'ExperimentyUP',
summary: 'Varování: ExperimentyUP'
}, {
id: 'nevyznamne',
label: 'Nevýznamné',
template: 'Nevýznamné',
summary: 'Varování: Nevýznamné'
}]
}, {
category: 'Odložené smazání:',
items: [{
id: 'vyznamnostautor',
label: 'Významnost',
template: 'Významnost autor',
summary: 'Varování: Významnost',
requirePage: true
}, {
id: 'uoveritautor',
label: 'Urgentně Ověřit',
template: 'Urgentně ověřit autor',
summary: 'Varování: Urgentně ověřit',
requirePage: true
}, {
id: 'copyvioautor',
label: 'Copyvio',
template: 'Copyvio autor',
summary: 'Varování: Copyvio',
requirePage: true
}, {
id: 'subpahylautor',
label: 'Subpahýl',
template: 'Subpahýl autor',
summary: 'Varování: Subpahýl',
requirePage: true
}, {
id: 'reklamaautor',
label: 'Reklama',
template: 'Reklama autor',
summary: 'Varování: Reklama',
requirePage: true
}, {
id: 'prelozitautor',
label: 'Přeložit',
template: 'Přeložit autor',
summary: 'Varování: Přeložit',
requirePage: true
}, {
id: 'strojovyprekladautor',
label: 'Stroj. překlad',
template: 'Strojový překlad autor',
summary: 'Varování: Strojový překlad',
requirePage: true
}]
}];
// Funkce pro získání českého formátu "Měsíc Rok" (např. "Květen 2026")
EW.getMonthYear = function() {
const months = ['Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec'];
const d = new Date();
return months[d.getMonth()].charAt(0).toUpperCase() + months[d.getMonth()].slice(1) + ' ' + d.getFullYear();
};
EW.setup = function() {
const $toolbar = $('<div>', {
id: 'ExperimentsWarning-toolbar',
css: {
margin: '10px 0',
padding: '8px',
border: '1px solid #a2a9b1',
backgroundColor: '#f8f9fa',
fontWeight: 'bold',
borderRadius: '2px'
}
});
// Přidání nadpisu a legendy
$toolbar.append($('<div>', {
css: {
borderBottom: '1px solid #a2a9b1',
marginBottom: '10px',
paddingBottom: '6px',
color: '#202122'
}
}).html(
'<span style="font-size: 1.1em; font-weight: bold;">Vložení varovných šablon</span>' +
'<span style="margin-left: 15px; font-size: 0.85em; font-weight: normal; color: #54595d;">' +
'Legenda: <b>(S/K)</b> = stránka a/nebo komentář, <b>(K)</b> = pouze komentář | ' +
'<span style="color: #d33; font-weight: bold;">Červené položky vyžadují název stránky</span>' +
'</span>'
));
warningGroups.forEach(group => {
const $row = $('<div>', {
css: {
marginBottom: '4px'
}
});
// Název kategorie s fixní šířkou pro zarovnání sloupců (bez zalamování)
$row.append($('<span>', {
text: group.category,
css: {
display: 'inline-block',
width: '230px',
color: '#54595d',
whiteSpace: 'nowrap'
}
}));
group.items.forEach((warn, index) => {
if (index > 0) $row.append(' | ');
// Zkrácené popisky pro čistší vzhled
const labelSK = warn.nopagetitle ? ' (K)' : ' (S/K)';
const titleSK = warn.nopagetitle ? 'Vložit s komentářem' : 'Vložit s názvem stránky a komentářem';
if (warn.requirePage) {
// STAV 1: Šablony vyžadující stránku (červené, jeden odkaz)
$row.append($('<a>', {
href: 'javascript:void(0)',
text: warn.label + labelSK,
title: titleSK + ' (vyžaduje název stránky)',
css: { color: '#d33' },
click: function() {
EW.openDialog(warn);
}
}));
} else {
// STAV 2: Klasické modré tlačítko pro rychlou akci
$row.append($('<a>', {
href: 'javascript:void(0)',
text: warn.label,
title: `Vložit {{subst:${warn.template}}}`,
click: function() {
if (!confirm(`Opravdu chcete vložit šablonu {{${warn.template}}}?`)) return;
$(this).text('Odesílám...');
EW.doWarn(warn, null, null);
}
}));
// STAV 3: Šedé doplňkové tlačítko pro dialog
$row.append($('<a>', {
href: 'javascript:void(0)',
text: labelSK,
title: titleSK,
css: {
color: '#72777d', // Šedá pro nerušivý vzhled
fontSize: '0.9em',
marginLeft: '2px'
},
click: function() {
EW.openDialog(warn);
}
}));
}
});
$toolbar.append($row);
});
// Zavěsíme na úplný začátek hlavního obsahu (zaručeně funguje i v action=edit)
$('#bodyContent').prepend($toolbar);
};
EW.doWarn = function(warn, pageName, comment) {
let headingText = '';
if (Object.prototype.hasOwnProperty.call(warn, 'sectiontitle')) {
headingText = warn.sectiontitle;
} else {
headingText = pageName ? `${pageName}` : EW.getMonthYear();
}
let headingLine = headingText !== '' ? `== ${headingText} ==\n` : '';
let templateCall = pageName ? `{{subst:${warn.template}|${pageName}}}` : `{{subst:${warn.template}}}`;
// Sestavení textu komentáře (pokud existuje, přidá se na nový řádek)
let commentLine = comment ? `\n${comment}` : '';
let prefix = mw.config.get('wgArticleId') === 0 ? '' : '\n\n';
// Finální wikitext: šablona, pak případný komentář, pak podpis
let wikitext = `${prefix}${headingLine}${templateCall}${commentLine} --~~~~`;
new mw.Api().postWithEditToken({
action: 'edit',
title: wgPageName,
appendtext: wikitext,
summary: warn.summary + EW.summarySuffix,
minor: false
}).done(() => location.reload());
};
EW.openDialog = function(warn) {
function WarnDialog(config) {
WarnDialog.super.call(this, config);
}
OO.inheritClass(WarnDialog, OO.ui.ProcessDialog);
WarnDialog.static.name = "WarnDialog";
WarnDialog.static.title = `Varování: ${warn.label}`;
WarnDialog.static.actions = [{
label: 'Zrušit',
flags: 'safe'
}, {
action: 'submit',
label: 'Odeslat',
flags: ['primary', 'progressive']
}];
WarnDialog.prototype.initialize = function() {
WarnDialog.super.prototype.initialize.call(this);
var rootPanel = new OO.ui.PanelLayout({ padded: true, expanded: false });
// Pole pro komentář (vždy přítomno)
this.commentInput = new OO.ui.MultilineTextInputWidget({
rows: 3,
placeholder: 'případný komentář, přidá se pod šablonu'
});
// Pokud šablona podporuje stránky, přidáme i pole pro stránku
if (!warn.nopagetitle) {
this.pageInput = new OO.ui.TextInputWidget({
placeholder: 'Např. Praha (bez závorek [[ ]])'
});
rootPanel.$element.append(
new OO.ui.FieldLayout(this.pageInput, {
label: `Název článku pro šablonu {{${warn.template}}}:`,
align: 'top'
}).$element
);
}
// Přidáme komentář
rootPanel.$element.append(
new OO.ui.FieldLayout(this.commentInput, {
label: 'Komentář:',
align: 'top'
}).$element
);
this.$body.append(rootPanel.$element);
};
WarnDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'submit') {
// Přečteme stránku jen pokud input existuje, jinak null
var pageName = dialog.pageInput ? dialog.pageInput.getValue().trim() : null;
var comment = dialog.commentInput.getValue().trim();
if (warn.requirePage && !pageName) {
alert('U této šablony musíte uvést název stránky!');
return new OO.ui.Process();
}
dialog.actions.setAbilities({ submit: false, cancel: false });
dialog.pushPending();
EW.doWarn(warn, pageName || null, comment || null);
}
return WarnDialog.super.prototype.getActionProcess.call(this, action);
};
var dialog = new WarnDialog({ size: "medium" });
var windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog);
};
// Načtení potřebných knihoven a spuštění
mw.loader.using(['mediawiki.api', 'oojs-ui-core', 'oojs-ui-windows'], () => $(EW.setup));
});
// </nowiki>
534ggplvxg90cudw2ut15g6c3ljm10h
User talk:Lawful Binary
3
175760
744850
2026-05-31T12:48:10Z
Chaotic Enby
58843
You have been indefinitely blocked from editing because your account is being used in violation of [[WP:PAID|Wikipedia policy on undisclosed paid advocacy]].
744850
wikitext
text/x-wiki
== May 2026 ==
{{subst:uw-upeblock|ticket=67|indef=yes|sig=yes}}
av1i8r2cg0rucuxe0nxos33joji69x9
744851
744850
2026-05-31T13:18:25Z
Chaotic Enby
58843
You have been blocked from editing for repeatedly misusing a [[WP:Disruptive editing#Persistent LLM use|large language model]].
744851
wikitext
text/x-wiki
== May 2026 ==
{{subst:uw-upeblock|ticket=67|indef=yes|sig=yes}}
{{subst:uw-llmpblock|indef=yes|area=certain [[Wikipedia:Namespace|namespaces]] ((Article))|sig=yes}}
2m864j19ycjrklnnfvb7bbc96id8glv
User:Mr. Ibrahem/updater
2
175764
744864
2026-05-31T22:52:06Z
Mr. Ibrahem
19267
Created page with "!"
744864
wikitext
text/x-wiki
!
192soe6wrkijukn00i5cl32fiq70enx
744865
744864
2026-05-31T22:52:52Z
Mr. Ibrahem
19267
Med updater.
744865
wikitext
text/x-wiki
!
!
ixs3dq987q9tv45nrl6vzi2inv18nps
744866
744865
2026-05-31T22:53:44Z
Mr. Ibrahem
19267
Med updater.
744866
wikitext
text/x-wiki
!
!
!
k75bh4nkhr2exslm7x6zo0ujgsu4ifo
744867
744866
2026-05-31T22:53:57Z
Mr. Ibrahem
19267
Med updater.
744867
wikitext
text/x-wiki
!
!
!
!
cbzzpywsm9i9jtal66g83worqmhakud
744868
744867
2026-05-31T23:17:37Z
Mr. Ibrahem
19267
Med updater.
744868
wikitext
text/x-wiki
!
!
!
!
!
dj4pkwqagtgkpl7pcv9prnd6x2iar4m
Википедия:К переименованию/1 июня 2026
0
175765
744873
2026-06-01T01:14:08Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: автоматическая шапка
744873
wikitext
text/x-wiki
{{КПМ-Навигация}}
5lhop28vwygvlrh0r5t1cfhn402d6y9
744874
744873
2026-06-01T01:14:09Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: номинация [[Википедия:К переименованию/1 июня 2026#Testtt]]
744874
wikitext
text/x-wiki
{{КПМ-Навигация}}
== Testtt ==
* [[:Edamame beans]] → [[:Edamame beans 2]], [[:Edamame beans 3]]
* [[:Test]] → [[:Test 2]]
*: Text text [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 01:14, 1 June 2026 (UTC)
Text ! [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 01:14, 1 June 2026 (UTC)
6yxhmbwf6g84s2amgii98s3n9hmannn
Википедия:Обсуждение категорий/Июнь 2026
0
175766
744879
2026-06-01T01:18:01Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление обсуждения для категорий [[:Category:Testing]], [[:Category:Test]] и [[:Category:Tes]]
744879
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 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)
dpf1o9h0gk41z0ay1ztauktpilqjwml
Talk:Port Royal/edithistory
1
175767
744889
2026-06-01T10:59:19Z
~2026-23062-17
73557
/* af topic */ new section
744889
wikitext
text/x-wiki
== af topic ==
af edit [[Special:Contributions/~2026-23062-17|~2026-23062-17]] ([[User talk:~2026-23062-17|talk]]) 10:59, 1 June 2026 (UTC)
e0c3h1qc85yapcz4q52gybvm4f2syoc