Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.46.0-wmf.24
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:Authority control/using NOTES
14
44674
738046
118690
2026-04-15T14:25:20Z
~2026-23347-42
73585
738046
wikitext
text/x-wiki
__HIDDENCAT__
'''Exceptions:''' WM LT mapping
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=gndCorporate gndCorporate] • '''gndCorporate''' → '''GKD''' • [[:category:Authority control/using GKD|/using GKD]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=pndTn pndTn] • '''pndTn''' → [[:category:Authority control/using NOTES/GND Tn|/using NOTES/GND Tn]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=pndTp pndTp] • '''pndTp''' → [[:category:Authority control/using NOTES/GND Tp|/using NOTES/GND Tp]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=gndAdditional gndAdditional] • '''GND additional(s)''' → [[:category:Authority control/using_PND2|/using_PND2]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=gndIncluded gndIncluded] • '''GND included(s)'''
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=viafCorporate viafCorporate] • '''VIAF corporate'''' → [[:category:Authority control/using GKD|/using GKD]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=viafAdditional viafAdditional] '''VIAF additional(s)'''' → [[:category:Authority control/using VIAF2|/using VIAF2]]
* LT: [http://www.librarything.com/commonknowledge/search.php?f=13&exact=0&q=viafIncluded viafIncluded] '''VIAF included(s)''' → [[:category:Authority control/using VIAF-1|/using VIAF-1]], [[:category:Authority control/using VIAF-2|/using VIAF-2]]
* [https://duckduckgo.com]
----
to dos: AC additional(s) → [[:category:Authority control/using LCCN2|/using LCCN2]], [[:category:Authority control/using SELIBR2|/using SELIBR2]], AC included(s)
----
[[category:Authority control|NOTES {{FULLPAGENAME}}]]
knh2sludk2xps3l65ukg9a1nhh5l11j
Page358
0
49429
738110
372773
2026-04-16T07:47:24Z
Dwalden test acctcreation28
73580
738110
wikitext
text/x-wiki
'''Page358'''
This in test page No. 358
<ref>{{cite web|url=http://www.cnn.com}}</ref>
<references/>this is an edit
[[Category:Category with multiple files]]
h4im2kgeukk3tbz1gmyxgy93mtedtwi
Page459
0
49530
738040
372870
2026-04-15T14:03:51Z
~2026-23292-93
73584
738040
wikitext
text/x-wiki
'''Page459'''
This in test page No. 459
[https://duckduckgo.com]
[[Category:Category with multiple files]]
o5302nncqg9bdx7wweb9d2e6hnmj532
Page604
0
49674
738021
373008
2026-04-15T13:33:49Z
~2026-23142-66
73571
showcaptcha
738021
wikitext
text/x-wiki
'''Page604'''
This in test page No. 604
this is an edit
[[Category:Category with multiple files]]
8xitc1ly99hi6w6wxo8uswvhztzm03z
Page298
0
59400
738132
372716
2026-04-16T09:23:55Z
Dwalden test acctcreation28
73580
738132
wikitext
text/x-wiki
[[File:Lead_Photo_For_Page2980-0749150502961129.JPG|thumbnail|Volcano]]
'''Page298'''
This in test page No. 298
this is an edit
[[Category:Category with multiple files]]
0wesqacd865kuwmedlra37y9hp4oyt8
Tnxrename2
0
76194
738052
199752
2026-04-15T14:45:09Z
~2026-23347-42
73585
showcaptcha
738052
wikitext
text/x-wiki
Testpage Tnxrename 2
edit
To remove to [[Tnxrename3]]
iqnd1yb8o6ww3rwb41flbyabpjntep7
Good redir
0
79145
738018
737183
2026-04-15T13:25:39Z
~2026-23302-47
73582
738018
wikitext
text/x-wiki
{{sil|İ1}}
#REDIRECT [[Test]]
#test
#This is a test
mato1srnyywj96g4n4pqt2zm505y2zi
738019
738018
2026-04-15T13:26:24Z
~2026-23302-47
73582
738019
wikitext
text/x-wiki
{{sil|İ1}}
#REDIRECT [[Test]]
#test
#This is a test
#[https://duckduckgo.com]
aqb629a7hd3i7lg4xzprmfywqytynkv
738020
738019
2026-04-15T13:32:11Z
~2026-23302-47
73582
showcaptcha
738020
wikitext
text/x-wiki
{{sil|İ1}}
#REDIRECT [[Test]]
#test
#This is a test
#[https://duckduckgo.com]
#this is an edit
nvgkafk478p03eaisb1rlhigupj0ykg
738022
738020
2026-04-15T13:34:35Z
~2026-23302-47
73582
738022
wikitext
text/x-wiki
{{sil|İ1}}
#REDIRECT [[Test]]
csd1w7nlsnkx1o110iwra63i3q8zamj
738053
738022
2026-04-15T14:52:02Z
~2026-23302-47
73582
738053
wikitext
text/x-wiki
{{sil|İ1}}
#REDIRECT [[Test]]
#this is an edit
8cxdjwmzyd0o3mey023x0o73ojynxod
Sitic Test Page
0
81073
738144
685978
2026-04-16T10:37:32Z
Dwalden test acctcreation28
73580
738144
wikitext
text/x-wiki
Foo Bar Baz
this is an edit
[https://duckduckgo.com]
8iy45r4vi9fy9rywyshjshj5sef5308
Summary:Talk:ET1/Topic 1
92
81945
738047
232484
2026-04-15T14:26:39Z
~2026-23347-42
73585
738047
wikitext
text/x-wiki
Summary is done.
this is an edit
m7y5i0bce4lh6zmckvj60fxtjp37oha
Destpêk
0
85201
738032
246885
2026-04-15T13:46:06Z
~2026-23292-93
73584
738032
wikitext
text/x-wiki
__NOEDITSECTION____NOTOC__
Ev malper ji bo ku [[mw:|nivîsbariya MediaWiki'yê]] û versiyonên wî bê testkirin hatiye vekirin. Guhertinên nivîsbariyê berî ku di wîkîyên din bên bi kar anîn ji vir dikare were dîtin. Naveroka vê rûpelê dikare winda bibe an jî çewtîyan bihundirîne. Ji ber vê yekê '''KOD'''ên ku li vir in ji bo ku di wîkî'yên xwe de bi kar bînin bi van kodan ewle nebin...!
Guhertoya guncav ê ko vêga tê şixulîn û tetbîqkirin: {{CURRENTVERSION}}.
{{Wikipedia:What we do on this wiki}}
== Ji kerema xwe hêj ko we guherandinan nekir bixwînin ==
{{Wikipedia:What Test Wiki is not}}
=== Destûrên Xwestî ===
{{Wikipedia:Administrators}}
{{Wikipedia:Bureaucrats}}
== Projektên Bira ==
<div class="plainlinks" id="mp-spr" title="Sister Projects">
{| class="plainlinks" style="background-color: transparent; text-align: left; width: 100%;"
| align="center" | [[File:Wikipedia-logo-v2.svg|35x35px|link=http://en.wikipedia.org|logo Wikipedia]]
| '''[http://en.wikipedia.org Wikipedia]'''<br />Ensîklopediya Azad
| align="center" | [[File:Wiktionary-logo-en.png|35x35px|link=http://www.wiktionary.org|logo Wiktionary]]
| '''[http://www.wiktionary.org Wiktionary]'''<br />Ferhenga Azad
| align="center" | [[File:Wikinews-logo.svg|35x35px|link=http://www.wikinews.org|logo Wikinews]]
| '''[http://www.wikinews.org Wikinews]'''<br />Nûçe
| align="center" | [[File:Wikiquote-logo.svg|35x35px|link=http://www.wikiquote.org|logo Wikiquote]]
| '''[http://www.wikiquote.org Wikiquote]'''<br />Gotinên Kesan
|-
| align="center" | [[File:Wikibooks-logo.svg|35x35px|link=http://www.wikibooks.org|logo Wikibooks]]
| '''[http://www.wikibooks.org Wikibooks]'''<br />Pirtûkên Azad
| align="center" | [[File:Wikispecies-logo.svg|35x35px|link=http://species.wikimedia.org|logo Wikispecies]]
| '''[http://species.wikimedia.org Wikispecies]'''<br />Rêbernameya Cureyan
| align="center" | [[File:Wikisource-logo.svg|35x35px|link=http://wikisource.org|logo Wikisource]]
| '''[http://wikisource.org/ Wikisource]'''<br />Kitêbxaneya Azad
| align="center" | [[File:MediaWiki-logo.svg|35x35px|link=http://mediawiki.org|logo MediaWiki]]
| '''[http://mediawiki.org/ MediaWiki]'''<br />Nivîsbariya MediaWiki'yê
|-
| align="center" | [[File:Wikiversity-logo.svg|35x35px|link=http://www.wikiversity.org|logo Wikiversity]]
| '''[http://www.wikiversity.org Wikiversity]'''<br />Zankoya Azad
| align="center" | [[File:Commons-logo.svg|35x35px|link=http://commons.wikimedia.org|logo Commons]]
| '''[http://commons.wikimedia.org Commons]'''<br />Wêne û Mêdyayên Din
| align="center" | [[File:Wikimedia Community Logo.svg|35x35px|link=http://meta.wikimedia.org|logo Meta-Wiki]]
| '''[http://meta.wikimedia.org Meta-Wiki]'''<br />Wîkîya Navend
|}</div>
{{HomePages}}
[[Category:Article Feedback Pilot]]
[[en:Main Page]]
60kfp0lqfz0vk4hpyrfm6p43dx76dv1
738033
738032
2026-04-15T13:46:22Z
~2026-23292-93
73584
738033
wikitext
text/x-wiki
__NOEDITSECTION____NOTOC__
Ev malper ji bo ku [[mw:|nivîsbariya MediaWiki'yê]] û versiyonên wî bê testkirin hatiye vekirin. Guhertinên nivîsbariyê berî ku di wîkîyên din bên bi kar anîn ji vir dikare were dîtin. Naveroka vê rûpelê dikare winda bibe an jî çewtîyan bihundirîne. Ji ber vê yekê '''KOD'''ên ku li vir in ji bo ku di wîkî'yên xwe de bi kar bînin bi van kodan ewle nebin...!
thisis an edit
Guhertoya guncav ê ko vêga tê şixulîn û tetbîqkirin: {{CURRENTVERSION}}.
{{Wikipedia:What we do on this wiki}}
== Ji kerema xwe hêj ko we guherandinan nekir bixwînin ==
{{Wikipedia:What Test Wiki is not}}
=== Destûrên Xwestî ===
{{Wikipedia:Administrators}}
{{Wikipedia:Bureaucrats}}
== Projektên Bira ==
<div class="plainlinks" id="mp-spr" title="Sister Projects">
{| class="plainlinks" style="background-color: transparent; text-align: left; width: 100%;"
| align="center" | [[File:Wikipedia-logo-v2.svg|35x35px|link=http://en.wikipedia.org|logo Wikipedia]]
| '''[http://en.wikipedia.org Wikipedia]'''<br />Ensîklopediya Azad
| align="center" | [[File:Wiktionary-logo-en.png|35x35px|link=http://www.wiktionary.org|logo Wiktionary]]
| '''[http://www.wiktionary.org Wiktionary]'''<br />Ferhenga Azad
| align="center" | [[File:Wikinews-logo.svg|35x35px|link=http://www.wikinews.org|logo Wikinews]]
| '''[http://www.wikinews.org Wikinews]'''<br />Nûçe
| align="center" | [[File:Wikiquote-logo.svg|35x35px|link=http://www.wikiquote.org|logo Wikiquote]]
| '''[http://www.wikiquote.org Wikiquote]'''<br />Gotinên Kesan
|-
| align="center" | [[File:Wikibooks-logo.svg|35x35px|link=http://www.wikibooks.org|logo Wikibooks]]
| '''[http://www.wikibooks.org Wikibooks]'''<br />Pirtûkên Azad
| align="center" | [[File:Wikispecies-logo.svg|35x35px|link=http://species.wikimedia.org|logo Wikispecies]]
| '''[http://species.wikimedia.org Wikispecies]'''<br />Rêbernameya Cureyan
| align="center" | [[File:Wikisource-logo.svg|35x35px|link=http://wikisource.org|logo Wikisource]]
| '''[http://wikisource.org/ Wikisource]'''<br />Kitêbxaneya Azad
| align="center" | [[File:MediaWiki-logo.svg|35x35px|link=http://mediawiki.org|logo MediaWiki]]
| '''[http://mediawiki.org/ MediaWiki]'''<br />Nivîsbariya MediaWiki'yê
|-
| align="center" | [[File:Wikiversity-logo.svg|35x35px|link=http://www.wikiversity.org|logo Wikiversity]]
| '''[http://www.wikiversity.org Wikiversity]'''<br />Zankoya Azad
| align="center" | [[File:Commons-logo.svg|35x35px|link=http://commons.wikimedia.org|logo Commons]]
| '''[http://commons.wikimedia.org Commons]'''<br />Wêne û Mêdyayên Din
| align="center" | [[File:Wikimedia Community Logo.svg|35x35px|link=http://meta.wikimedia.org|logo Meta-Wiki]]
| '''[http://meta.wikimedia.org Meta-Wiki]'''<br />Wîkîya Navend
|}</div>
{{HomePages}}
[[Category:Article Feedback Pilot]]
[[en:Main Page]]
14re9vog3xvbucdi7ugysqh3lxsm7yh
738034
738033
2026-04-15T13:55:16Z
~2026-23292-93
73584
738034
wikitext
text/x-wiki
__NOEDITSECTION____NOTOC__
Ev malper ji bo ku [[mw:|nivîsbariya MediaWiki'yê]] û versiyonên wî bê testkirin hatiye vekirin. Guhertinên nivîsbariyê berî ku di wîkîyên din bên bi kar anîn ji vir dikare were dîtin. Naveroka vê rûpelê dikare winda bibe an jî çewtîyan bihundirîne. Ji ber vê yekê '''KOD'''ên ku li vir in ji bo ku di wîkî'yên xwe de bi kar bînin bi van kodan ewle nebin...!
Guhertoya guncav ê ko vêga tê şixulîn û tetbîqkirin: {{CURRENTVERSION}}.
{{Wikipedia:What we do on this wiki}}
== Ji kerema xwe hêj ko we guherandinan nekir bixwînin ==
{{Wikipedia:What Test Wiki is not}}
=== Destûrên Xwestî ===
{{Wikipedia:Administrators}}
{{Wikipedia:Bureaucrats}}
== Projektên Bira ==
<div class="plainlinks" id="mp-spr" title="Sister Projects">
{| class="plainlinks" style="background-color: transparent; text-align: left; width: 100%;"
| align="center" | [[File:Wikipedia-logo-v2.svg|35x35px|link=http://en.wikipedia.org|logo Wikipedia]]
| '''[http://en.wikipedia.org Wikipedia]'''<br />Ensîklopediya Azad
| align="center" | [[File:Wiktionary-logo-en.png|35x35px|link=http://www.wiktionary.org|logo Wiktionary]]
| '''[http://www.wiktionary.org Wiktionary]'''<br />Ferhenga Azad
| align="center" | [[File:Wikinews-logo.svg|35x35px|link=http://www.wikinews.org|logo Wikinews]]
| '''[http://www.wikinews.org Wikinews]'''<br />Nûçe
| align="center" | [[File:Wikiquote-logo.svg|35x35px|link=http://www.wikiquote.org|logo Wikiquote]]
| '''[http://www.wikiquote.org Wikiquote]'''<br />Gotinên Kesan
|-
| align="center" | [[File:Wikibooks-logo.svg|35x35px|link=http://www.wikibooks.org|logo Wikibooks]]
| '''[http://www.wikibooks.org Wikibooks]'''<br />Pirtûkên Azad
| align="center" | [[File:Wikispecies-logo.svg|35x35px|link=http://species.wikimedia.org|logo Wikispecies]]
| '''[http://species.wikimedia.org Wikispecies]'''<br />Rêbernameya Cureyan
| align="center" | [[File:Wikisource-logo.svg|35x35px|link=http://wikisource.org|logo Wikisource]]
| '''[http://wikisource.org/ Wikisource]'''<br />Kitêbxaneya Azad
| align="center" | [[File:MediaWiki-logo.svg|35x35px|link=http://mediawiki.org|logo MediaWiki]]
| '''[http://mediawiki.org/ MediaWiki]'''<br />Nivîsbariya MediaWiki'yê
|-
| align="center" | [[File:Wikiversity-logo.svg|35x35px|link=http://www.wikiversity.org|logo Wikiversity]]
| '''[http://www.wikiversity.org Wikiversity]'''<br />Zankoya Azad
| align="center" | [[File:Commons-logo.svg|35x35px|link=http://commons.wikimedia.org|logo Commons]]
| '''[http://commons.wikimedia.org Commons]'''<br />Wêne û Mêdyayên Din
| align="center" | [[File:Wikimedia Community Logo.svg|35x35px|link=http://meta.wikimedia.org|logo Meta-Wiki]]
| '''[http://meta.wikimedia.org Meta-Wiki]'''<br />Wîkîya Navend
|}</div>
{{HomePages}}
[[Category:Article Feedback Pilot]]
[[en:Main Page]]
60kfp0lqfz0vk4hpyrfm6p43dx76dv1
738035
738034
2026-04-15T13:55:31Z
~2026-23292-93
73584
738035
wikitext
text/x-wiki
__NOEDITSECTION____NOTOC__
Ev malper ji bo ku [[mw:|nivîsbariya MediaWiki'yê]] û versiyonên wî bê testkirin hatiye vekirin. Guhertinên nivîsbariyê berî ku di wîkîyên din bên bi kar anîn ji vir dikare were dîtin. Naveroka vê rûpelê dikare winda bibe an jî çewtîyan bihundirîne. Ji ber vê yekê '''KOD'''ên ku li vir in ji bo ku di wîkî'yên xwe de bi kar bînin bi van kodan ewle nebin...!
this is an edit
Guhertoya guncav ê ko vêga tê şixulîn û tetbîqkirin: {{CURRENTVERSION}}.
{{Wikipedia:What we do on this wiki}}
== Ji kerema xwe hêj ko we guherandinan nekir bixwînin ==
{{Wikipedia:What Test Wiki is not}}
=== Destûrên Xwestî ===
{{Wikipedia:Administrators}}
{{Wikipedia:Bureaucrats}}
== Projektên Bira ==
<div class="plainlinks" id="mp-spr" title="Sister Projects">
{| class="plainlinks" style="background-color: transparent; text-align: left; width: 100%;"
| align="center" | [[File:Wikipedia-logo-v2.svg|35x35px|link=http://en.wikipedia.org|logo Wikipedia]]
| '''[http://en.wikipedia.org Wikipedia]'''<br />Ensîklopediya Azad
| align="center" | [[File:Wiktionary-logo-en.png|35x35px|link=http://www.wiktionary.org|logo Wiktionary]]
| '''[http://www.wiktionary.org Wiktionary]'''<br />Ferhenga Azad
| align="center" | [[File:Wikinews-logo.svg|35x35px|link=http://www.wikinews.org|logo Wikinews]]
| '''[http://www.wikinews.org Wikinews]'''<br />Nûçe
| align="center" | [[File:Wikiquote-logo.svg|35x35px|link=http://www.wikiquote.org|logo Wikiquote]]
| '''[http://www.wikiquote.org Wikiquote]'''<br />Gotinên Kesan
|-
| align="center" | [[File:Wikibooks-logo.svg|35x35px|link=http://www.wikibooks.org|logo Wikibooks]]
| '''[http://www.wikibooks.org Wikibooks]'''<br />Pirtûkên Azad
| align="center" | [[File:Wikispecies-logo.svg|35x35px|link=http://species.wikimedia.org|logo Wikispecies]]
| '''[http://species.wikimedia.org Wikispecies]'''<br />Rêbernameya Cureyan
| align="center" | [[File:Wikisource-logo.svg|35x35px|link=http://wikisource.org|logo Wikisource]]
| '''[http://wikisource.org/ Wikisource]'''<br />Kitêbxaneya Azad
| align="center" | [[File:MediaWiki-logo.svg|35x35px|link=http://mediawiki.org|logo MediaWiki]]
| '''[http://mediawiki.org/ MediaWiki]'''<br />Nivîsbariya MediaWiki'yê
|-
| align="center" | [[File:Wikiversity-logo.svg|35x35px|link=http://www.wikiversity.org|logo Wikiversity]]
| '''[http://www.wikiversity.org Wikiversity]'''<br />Zankoya Azad
| align="center" | [[File:Commons-logo.svg|35x35px|link=http://commons.wikimedia.org|logo Commons]]
| '''[http://commons.wikimedia.org Commons]'''<br />Wêne û Mêdyayên Din
| align="center" | [[File:Wikimedia Community Logo.svg|35x35px|link=http://meta.wikimedia.org|logo Meta-Wiki]]
| '''[http://meta.wikimedia.org Meta-Wiki]'''<br />Wîkîya Navend
|}</div>
{{HomePages}}
[[Category:Article Feedback Pilot]]
[[en:Main Page]]
1ze4yd22fis2p0m3xh1bgbg42o2xs42
738036
738035
2026-04-15T13:55:46Z
~2026-23292-93
73584
738036
wikitext
text/x-wiki
__NOEDITSECTION____NOTOC__
Ev malper ji bo ku [[mw:|nivîsbariya MediaWiki'yê]] û versiyonên wî bê testkirin hatiye vekirin. Guhertinên nivîsbariyê berî ku di wîkîyên din bên bi kar anîn ji vir dikare were dîtin. Naveroka vê rûpelê dikare winda bibe an jî çewtîyan bihundirîne. Ji ber vê yekê '''KOD'''ên ku li vir in ji bo ku di wîkî'yên xwe de bi kar bînin bi van kodan ewle nebin...!
Guhertoya guncav ê ko vêga tê şixulîn û tetbîqkirin: {{CURRENTVERSION}}.
{{Wikipedia:What we do on this wiki}}
== Ji kerema xwe hêj ko we guherandinan nekir bixwînin ==
{{Wikipedia:What Test Wiki is not}}
=== Destûrên Xwestî ===
{{Wikipedia:Administrators}}
{{Wikipedia:Bureaucrats}}
== Projektên Bira ==
<div class="plainlinks" id="mp-spr" title="Sister Projects">
{| class="plainlinks" style="background-color: transparent; text-align: left; width: 100%;"
| align="center" | [[File:Wikipedia-logo-v2.svg|35x35px|link=http://en.wikipedia.org|logo Wikipedia]]
| '''[http://en.wikipedia.org Wikipedia]'''<br />Ensîklopediya Azad
| align="center" | [[File:Wiktionary-logo-en.png|35x35px|link=http://www.wiktionary.org|logo Wiktionary]]
| '''[http://www.wiktionary.org Wiktionary]'''<br />Ferhenga Azad
| align="center" | [[File:Wikinews-logo.svg|35x35px|link=http://www.wikinews.org|logo Wikinews]]
| '''[http://www.wikinews.org Wikinews]'''<br />Nûçe
| align="center" | [[File:Wikiquote-logo.svg|35x35px|link=http://www.wikiquote.org|logo Wikiquote]]
| '''[http://www.wikiquote.org Wikiquote]'''<br />Gotinên Kesan
|-
| align="center" | [[File:Wikibooks-logo.svg|35x35px|link=http://www.wikibooks.org|logo Wikibooks]]
| '''[http://www.wikibooks.org Wikibooks]'''<br />Pirtûkên Azad
| align="center" | [[File:Wikispecies-logo.svg|35x35px|link=http://species.wikimedia.org|logo Wikispecies]]
| '''[http://species.wikimedia.org Wikispecies]'''<br />Rêbernameya Cureyan
| align="center" | [[File:Wikisource-logo.svg|35x35px|link=http://wikisource.org|logo Wikisource]]
| '''[http://wikisource.org/ Wikisource]'''<br />Kitêbxaneya Azad
| align="center" | [[File:MediaWiki-logo.svg|35x35px|link=http://mediawiki.org|logo MediaWiki]]
| '''[http://mediawiki.org/ MediaWiki]'''<br />Nivîsbariya MediaWiki'yê
|-
| align="center" | [[File:Wikiversity-logo.svg|35x35px|link=http://www.wikiversity.org|logo Wikiversity]]
| '''[http://www.wikiversity.org Wikiversity]'''<br />Zankoya Azad
| align="center" | [[File:Commons-logo.svg|35x35px|link=http://commons.wikimedia.org|logo Commons]]
| '''[http://commons.wikimedia.org Commons]'''<br />Wêne û Mêdyayên Din
| align="center" | [[File:Wikimedia Community Logo.svg|35x35px|link=http://meta.wikimedia.org|logo Meta-Wiki]]
| '''[http://meta.wikimedia.org Meta-Wiki]'''<br />Wîkîya Navend
|}</div>
{{HomePages}}
[[Category:Article Feedback Pilot]]
[[en:Main Page]]
60kfp0lqfz0vk4hpyrfm6p43dx76dv1
Inserting graph into article test
0
87710
738031
689342
2026-04-15T13:43:44Z
Dwalden test acctcreation28
73580
738031
wikitext
text/x-wiki
{{Infobox
|name = Infobox/doc
|bodystyle =
|title = Test Infobox [[:en:AFL-CIO]]
|titlestyle =
|image = [[File:example.png|200px|alt=Example alt text]]
|imagestyle =
|caption = Caption for example.png
|captionstyle =
|headerstyle = background:#ccf;
|labelstyle = background:#ddf;
|datastyle =
|header1 = Header defined alone
|label1 =
|data1 =
|header2 =
|label2 = Label defined alone
|data2 =
|header3 =
|label3 =
|data3 = Data defined alone
|header4 = All three defined (header)
|label4 = All three defined (label)
|data4 = All three defined (data)
|header5 =
|label5 = Label and data defined (label)
|data5 = Label and data defined (data)
|belowstyle = background:#ddf;
|below = Below text
}}
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus auctor enim eget sem. Vivamus posuere, ante eu tempor dictum, felis nibh facilisis sem, eu auctor metus nulla non lorem. Proin dolor sapien, adipiscing id, sagittis eu, molestie viverra, mauris. Nulla sed lacus. Donec diam eros, tristique sit amet, pretium vel, pellentesque ut, neque. Maecenas justo. Morbi turpis arcu, egestas congue, condimentum quis, tristique cursus, leo. Pellentesque viverra dolor non nunc. Nunc metus. Donec ut urna. Aliquam justo lectus, iaculis a, auctor sed, congue in, nisl. Aenean luctus vulputate turpis.
Aliquam sed erat. Proin lectus orci, venenatis pharetra, egestas id, tincidunt vel, eros. Praesent lacus. Maecenas convallis dui. Nam rhoncus, lectus vel hendrerit congue, nisl lorem feugiat ante, in fermentum erat nulla tristique arcu. Phasellus hendrerit. Donec nonummy lacinia leo. Fusce venenatis ligula in pede.
this is an edit
==Fringilla==
<graph>{
"version": 1,
"width": 400,
"height": 200,
"data": [
{
"name": "table",
"values": [
{
"x": 2000,
"y": 13228033
},
{
"x": 2001,
"y": 13250198
},
{
"x": 2002,
"y": 13140393
},
{
"x": 2003,
"y": 13198357
},
{
"x": 2004,
"y": 13031914
},
{
"x": 2005,
"y": 13705284
},
{
"x": 2006,
"y": 9875234
},
{
"x": 2007,
"y": 10028904
},
{
"x": 2008,
"y": 11013317
},
{
"x": 2009,
"y": 11357015
},
{
"x": 2010,
"y": 11717230
},
{
"x": 2011,
"y": 11601104
},
{
"x": 2012,
"y": 11525023
},
{
"x": 2013,
"y": 11615520
},
{
"x": 2014,
"y": 12741859
}
]
}
],
"scales": [
{
"name": "x",
"type": "linear",
"range": "width",
"zero": false,
"domain": {
"data": "table",
"field": "data.x"
}
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true,
"domain": {
"data": "table",
"field": "data.y"
}
}
],
"axes": [
{
"type": "x",
"scale": "x"
},
{
"type": "y",
"scale": "y"
}
],
"marks": [
{
"type": "area",
"from": {
"data": "table"
},
"properties": {
"enter": {
"x": {
"scale": "x",
"field": "data.x"
},
"y": {
"scale": "y",
"field": "data.y"
},
"y2": {
"scale": "y",
"value": 0
},
"fill": {
"value": "steelblue"
},
"interpolate": {
"value": "monotone"
}
}
}
}
]
}</graph>
Integer fringilla. Sed dolor. In hac habitasse platea dictumst. Morbi pulvinar nulla sit amet nisl.
Pellentesque viverra dolor non nunc. Nam massa turpis, nonummy et, consectetuer id, placerat ac, ante. Cras gravida. Sed at turpis vitae velit euismod aliquet. Fusce consectetuer tellus ut nisl. Morbi urna. Curabitur risus urna, placerat et, luctus pulvinar, auctor vel, orci. Aliquam vel nibh. Nam a nunc. Praesent aliquet, neque pretium congue mattis, ipsum augue dignissim ante, ac pretium nisl lectus at magna. Phasellus lacinia iaculis mi. Phasellus nisi metus, tempus sit amet, ultrices ac, porta nec, felis. Aliquam velit dui, commodo quis, porttitor eget, convallis et, nisi. Pellentesque suscipit accumsan massa. Ut eu metus id lectus vestibulum ultrices. Aenean id purus. Fusce nonummy commodo dui. Morbi turpis arcu, egestas congue, condimentum quis, tristique cursus, leo. Aenean ligula. Maecenas viverra.
Integer accumsan. Nam nisl quam, posuere non, volutpat sed, semper vitae, magna. Fusce nonummy commodo dui. Curabitur tincidunt tellus nec purus. Sed at turpis vitae velit euismod aliquet. Donec nonummy lacinia leo. Nulla facilisi. Praesent a lacus vitae turpis consequat semper. Sed quis elit. Aenean justo ipsum, luctus ut, volutpat laoreet, vehicula in, libero. Phasellus nisi metus, tempus sit amet, ultrices ac, porta nec, felis.
===Consectetuer===
{{center|'''Total membership (US records; ×1000)'''<ref name="OLMS">{{Cite OLMS|filenum=000-106}}</ref>}}{{Line chart
| padding_top = 5 | padding_bottom = 15
| padding_right = 10 | padding_left = 50
| width = 250 | height = 150
| number_of_x-values = 15
| y_max = 14000 | y_min = 0
| number_of_series = 1
| interval_primary_scale = 2000
| interval_secondary_scale = 500
| label_x1 = 2000 | label_x2 = | label_x3 = | label_x4 = | label_x5 =
| label_x6 = 2005 | label_x7 = | label_x8 = | label_x9 = | label_x10 =
| label_x11 = 2010 | label_x12 = | label_x13 = | label_x14 = | label_x15 = 2014
| S01V01 = 13228 | S01V02 = 13250 | S01V03 = 13140 | S01V04 = 13198 | S01V05 = 13032
| S01V06 = 13705 | S01V07 = 9875 | S01V08 = 10029 | S01V09 = 11013 | S01V10 = 11357
| S01V11 = 11717 | S01V12 = 11601 | S01V13 = 11525 | S01V14 = 11616 | S01V15 = 12742
<!-- Values are currently rounded up/down to nearest 1000 for legibility of y-axis, which otherwise uses scientific notation(!) -->
}}
----
{{center|'''Finances (US records; ×$1000)'''<ref name="OLMS"/>}}{{Line chart
| padding_top = 5 | padding_bottom = 15
| padding_right = 10 | padding_left = 50
| width = 250 | height = 150
| number_of_x-values = 15
| y_max = 250000 | y_min = 0
| number_of_series = 4
| interval_primary_scale = 50000
| label_x1 = 2000 | label_x2 = | label_x3 = | label_x4 = | label_x5 =
| label_x6 = 2005 | label_x7 = | label_x8 = | label_x9 = | label_x10 =
| label_x11 = 2010 | label_x12 = | label_x13 = | label_x14 = | label_x15 = 2014
<!-- Assets | Liabilities | Receipts | Disbursements -->
| S01V01 = 95360 | S02V01 = 50005 | S03V01 = 183932 | S04V01 = 190912
| S01V02 = 92996 | S02V02 = 55566 | S03V02 = 161316 | S04V02 = 163997
| S01V03 = 94580 | S02V03 = 53752 | S03V03 = 79738 | S04V03 = 76570
| S01V04 = 96244 | S02V04 = 56708 | S03V04 = 168384 | S04V04 = 168974
| S01V05 = 92419 | S02V05 = 63337 | S03V05 = 171999 | S04V05 = 174282
| S01V06 = 102658 | S02V06 = 84460 | S03V06 = 189889 | S04V06 = 180748
| S01V07 = 96045 | S02V07 = 75392 | S03V07 = 157259 | S04V07 = 156541
| S01V08 = 84914 | S02V08 = 80994 | S03V08 = 207252 | S04V08 = 217613
| S01V09 = 88355 | S02V09 = 90675 | S03V09 = 189632 | S04V09 = 186161
| S01V10 = 82646 | S02V10 = 103665 | S03V10 = 193680 | S04V10 = 189389
| S01V11 = 99288 | S02V11 = 107657 | S03V11 = 177457 | S04V11 = 166203
| S01V12 = 100554 | S02V12 = 99037 | S03V12 = 199392 | S04V12 = 200556
| S01V13 = 95180 | S02V13 = 91696 | S03V13 = 184674 | S04V13 = 191255
| S01V14 = 93894 | S02V14 = 91625 | S03V14 = 196281 | S04V14 = 192155
| S01V15 = 83348 | S02V15 = 88056 | S03V15 = 183908 | S04V15 = 192370
<!-- Values are currently rounded up to nearest $1000 for legibility of y-axis, which otherwise uses scientific notation(!) -->
}}|caption={{legend0|red|Assets}} {{legend0|blue|Liabilities}} {{legend0|green|Receipts}} {{legend0|yellow|Disbursements}}}}
Nam pharetra. In consectetuer, lorem eu lobortis egestas, velit odio imperdiet eros, sit amet sagittis nunc mi ac neque. Pellentesque et arcu. Mauris et dolor. Fusce consectetuer tellus ut nisl. Aliquam sed erat. Phasellus auctor enim eget sem. Aenean ligula. Pellentesque sit amet dui vel justo gravida auctor. Nullam libero nunc, tristique eget, laoreet eu, sagittis id, ante. Quisque arcu ante, cursus in, ornare quis, viverra ut, justo. Sed non ipsum.
==Curae==
Sed quis elit. Donec sit amet enim. Fusce venenatis ligula in pede. Suspendisse molestie sem. Quisque arcu ante, cursus in, ornare quis, viverra ut, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec gravida, ante vel ornare lacinia, orci enim porta est, eget sollicitudin lectus lectus eget lacus. Nam molestie nisl at metus. Sed at turpis vitae velit euismod aliquet. In tempus urna. Donec sit amet enim. Phasellus lacinia iaculis mi. Nullam venenatis gravida orci. Maecenas viverra. Nam rhoncus, lectus vel hendrerit congue, nisl lorem feugiat ante, in fermentum erat nulla tristique arcu. Quisque arcu ante, cursus in, ornare quis, viverra ut, justo. Aenean turpis ipsum, rhoncus vitae, posuere vitae, euismod sed, ligula.
In tempus urna. Donec tempus quam quis neque. Suspendisse lectus. Quisque pretium rutrum ligula. Fusce consectetuer tellus ut nisl. Suspendisse potenti. Quisque vehicula porttitor odio. Nam sed nisl nec elit suscipit ullamcorper. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Maecenas rhoncus rhoncus ipsum. Morbi pulvinar nulla sit amet nisl. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla blandit justo a metus. Suspendisse viverra placerat tortor. Nam malesuada sapien eu nibh. Donec sit amet enim. Pellentesque viverra dolor non nunc. Pellentesque sit amet dui vel justo gravida auctor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec gravida, ante vel ornare lacinia, orci enim porta est, eget sollicitudin lectus lectus eget lacus. Nullam venenatis gravida orci. Ut eu metus id lectus vestibulum ultrices. Etiam non neque ac mi vestibulum placerat.
==Quisque facilisis==
Nam consectetuer mollis dolor. Quisque facilisis, urna sit amet pulvinar mollis, purus arcu adipiscing velit, non condimentum diam purus eu massa. Suspendisse potenti. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec gravida, ante vel ornare lacinia, orci enim porta est, eget sollicitudin lectus lectus eget lacus. Maecenas rhoncus rhoncus ipsum. Mauris tempor ultrices justo. Aliquam velit dui, commodo quis, porttitor eget, convallis et, nisi. Phasellus nisi metus, tempus sit amet, ultrices ac, porta nec, felis.
==Elit==
* Aliquam metus. Phasellus magna sem, vulputate eget, ornare sed, dignissim sit amet, pede. Phasellus auctor enim eget sem. Suspendisse molestie sem.
* In hac habitasse platea dictumst. Vestibulum non arcu a ante feugiat vestibulum. Nunc in turpis ac lacus eleifend sagittis. Morbi volutpat. Proin diam augue, semper vitae, varius et, viverra id, felis. Maecenas viverra. Donec interdum vestibulum libero. Nam id neque.
* Nam molestie nisl at metus. Pellentesque suscipit accumsan massa. Integer porta. Sed at turpis vitae velit euismod aliquet. Mauris tempor ultrices justo. Pellentesque sit amet dui vel justo gravida auctor. Quisque facilisis, urna sit amet pulvinar mollis, purus arcu adipiscing velit, non condimentum diam purus eu massa. In hac habitasse platea dictumst. Sed fringilla.
* Fusce venenatis ligula in pede. Nulla sagittis condimentum ligula. Sed elementum, felis quis porttitor sollicitudin, augue nulla sodales sapien, sit amet posuere quam purus at lacus. Donec ut urna. Proin dolor sapien, adipiscing id, sagittis eu, molestie viverra, mauris.
jqxgulmnicb1fcejysp2w9eabb348eh
Usuario:Pere Orga/Taller/Dufí
0
95462
738142
724988
2026-04-16T10:29:45Z
~2026-23062-17
73557
738142
wikitext
text/x-wiki
{{d}}
== {{-ca-}} ==
Edit
=== Nom ===
{{ca-nom|m}}
# [[dofí comú]]
=== Referències ===
* {{ca-dicc|termcat}}
huriwp99zfsh3nm6rbljg5cnz0mq23g
Dušníky nad Vltavou
0
108748
738105
737528
2026-04-16T07:30:34Z
Dwalden test acctcreation28
73580
738105
wikitext
text/x-wiki
Kronika obce Hostín zmiňuje ještě povodeň v roce 1772, po které následovala nouze a hlad, a povodeň roku 1900 (kolem 9. 4.). K větší povodni, kterou dušnický kronikář v přehledu opominul, patří i povodeň roku 1940, která v Dušníkách zaplavila cesty a pole i chmelnice, do vsi se však nedostala. Podle dostupných údajů se vůbec nejničivějšími povodněmi (pro Prahu a pravděpodobně i pro celé dolní Povltaví) jeví povodně z roku 1432 a 1784<ref>{{Citace monografie
| příjmení = Státníková
| jméno = Pavla
| příjmení2 =
| jméno2 =
| titul = Zmizelá Praha. Povodně a záplavy.
| vydání =
| vydavatel = Paseka
| místo =
| rok = 2012
| počet stran =
| strany =
| isbn =
}}</ref>. Test
this is an edit
this is another edit
=== Ochrana proti povodním ===
Jakkoli to není při zběžném pohledu patrné, Dušníky leží na nevysokém ostrohu. Při menších povodních voda sice překonala hráze a dostala se do polí, chmelnic a zahrad, ale neohrozila obydlí. V případě stoletých vod obyvatelé zpravidla nacházeli útočiště ve statku čp. 10, nejvýše položeném. Jako částečná ochrana před povodněmi sloužily snad i tarasy (v katastru je doloženo pomístní jméno Na tarase, Na Taradii), nasucho stavěné zídky střídané hliněnými náspy. Protipovodňová opatření zesilovala od 18. století. Od poloviny 18. století se věnovali stavbě dřevěných vzpěr břehů také majitelé panství Chotkové, vzpěry byly nazývány jako sruby (dokladem je snad pomístní jméno Na zdrubech) a kobyly.
V roce 1785 tak vznikl také plán regulace Vltavy na zdejším panství od Františka Hegra, který zahrnuje regulaci toku, prorážení poloostrovů i budování hrází. Po roce 1816 docházelo k dalším průkopům a regulacím na dolní Vltavě, v dolním Povltaví pod taktovkou navigačního ředitele Gerstnera. Další úpravy a opravu dušnické hráze si vyžádala povodeň z roku 1862 (práce proběhly o sedm let později) a pak také následující z roku 1890 (opravy v roce 1896).
Po vybudování tzv. Vltavské kaskády (1930–1959) se dařilo po desetiletí tok Vltavy regulovat, a tak další „stoletá“ povodeň následovala po události 1890 se zpožděním, až v roce 2002. Přinesla však po moravských povodních z devadesátých let 20. století další „civilizační šok“. V roce 2013, patrně i díky lepší připravenosti vodohospodářů, neměla povodeň tak ničivé následky a byla hodnocena jako voda dvacetiletá nebo padesátiletá. Přesto povodeň téměř zničila nový dálniční most vystavený nad silnicí spojující Všestudy s Dušníky, který byl postaven po povodni z roku 2002. Popovodňová opatření zahrnovala především opravy protržených hrází a čištění (prohlubování koryta), ke znovuzakládání mokřadů v dolním Povltaví nedochází, nedaří se ani stavba vyšší hráze u obce Zálezlice (patřila roku 2002 a 2013 k nejvíce postiženým), zejména kvůli vlastnickým sporům.
=== Proměny řečiště Vltavy v dolním Povltaví ===
Vltava měnila koryto i v posledních staletích. V letech 1667–1683 například změnila řeka řečiště od Zelčína k Doničkám, pobořila kostel v Doničkách a roku 1798 byly z něho již jen zříceniny, přitom ves zanikla zcela. Při povodních roku 1700 de facto zanikla část blízké vsi Ouholice. Nejvýznamnější změnou pro Dušníky byl zánik „pravého“ vltavského ramene, které fungovalo v té době jako rameno hlavní, odbočovalo ve Veltrusích a vracelo se zčásti v oblasti Kubantova, vytvářeje ostrov. Rameno zčásti pokračovalo přes Vojkovice, patrně vedeno ostrohem zvaným Hebron. V 80. letech 18. století už muselo být původně hlavní rameno ve veltruském parku uměle napájeno vodou z řeky, aby si park uchoval kouzlo vodou protkané krajiny. Při pozdějších povodních se řeka vracela do starých řečišť a poškozovala stavby a hráze po svém původním toku, tedy i na dušnickém "Kubantově". Tůně v okolí vsi měnily tvar, vznikaly a zanikaly, některé z nich byly později zasypány. Například na louce při Vltavě od dušnického přívoznického domku výš proti proudu se nacházely až do roku 1904 tůňky. Při provádění kanalizace Vltavy byly zasypány výkopem materiálu z jezu u Vraňan.
=== Záznamy o povodních v Dušníkách ===
==== 18. století ====
Ve starších historických záznamech se dochovaly zápisy o povodních v Dušníkách jen sporadicky.
V roce 1752 hlásí vrchnosti velkou škodu na polích dušnický rychtář Matěj Pokorný s tím, že mu zbylo k užitku po velké povodni jen 45 ze 100 strychů polí.
Farní pamětní kniha v Lužci nad Vltavou připomíná velkou povodeň roku 1768, kdy se rozlila voda, když se nastavil led v Lužci, po celém okolí: Bukol, Hostín, Vojkovice, Křivousy, Jedibaby, Dušníky nad Vltavou byly zatopeny.
[[Soubor:Ledové kry ve Veltrusích.jpg|náhled|Ledové kry ve Veltrusích ]]
Podobné události se připomínají roku 1769, 1771 a 1773, kdy opět celá krajina až do Ouholic a Hostíně byla zatopena.
Dobře jsou dochovány zprávy o výjimečných povodních let 1784 a 1785. Lidový kronikář Pražák z Ouholic si mj. zapsal: „Roku 1783 začala počátkem prosince krutá zima a trvala nepřetržitě až do 15. února 1784. Led na Vltavě dosáhl 3/4 lokte síly. Velmi často hřmělo a blýskalo se, načež 27. února hnul se náhle led, způsobil povodeň, takže mnohé pobřežní dědiny pohubil. … V roce 1785 byla velkou vodou protržena dušnická hráz u Všestud.“ Při těchto povodních (zejména té první, ledové a smíšené) zaniklo v dolním Povltaví hned několik vesnic a řeka částečně změnila průběh toku (koryta). Ostatně povodeň roku 1784 pustošila přírodu a sídla v mnoha povodích v celé Evropě (Dunaj, Odra, Labe, Rýn, Seina, Máza, Loira) a měla na svědomí stržení několika pilířů pražského Karlova mostu.
„Roku 1799“ píše ouholický kronikář Hodek, „byla veliká povodeň s ledem, který zase náramně silný led s hřmotem dne 23. fevruára se strhnul a voda okolo pivováru šla a všady ledy s sebou nesla. U nás u sloupu zvonečkového, který stál proti č. 13 na 1/4 lokte vystoupila a opět v zahradách a gruntech dosti škody způsobila, protože když opadla a zem roztála, zase silné mrazy přešly, zkrze které mnoho stromů, zvlášť švestkových v kořenech nejlepším užitkem zmrzlo a poschlo. Příčinou tak časté velké vody jest, že je od ostrovské (veltruské) strany všechna na naši stranu sehnána a sevřena ledy.“
==== 19. století ====
Kronikáři také popisují ledové zácpy, které se opakovaly ve 40. letech 19. století od ledna do března a nesly velké nebezpečí: obrovské kusy ledu strhávaly břehy, pak znovu zamrzaly, tály a takto ztuhlé obrovské kry ohrožovaly celé okolí řek. Lidé opětovně zamrzlý povrch místy prosekávali a uvolňovali, aby se náplava ledu (obvykle se hromadící v ohybech řeky, tedy právě u Dušník) mohla volně odtéct.
[[Soubor:05-04-29-kronika-povoden1890.jpg|vlevo|náhled|Po povodni roku 1890 u dušnických tůní|alt=foto vlastní rodinný archiv]]
Ke smíšené a ledové povodni roku 1845 uvádí hostínský kronikář Vorlíček: „A co vesnic je vod Prahy až pod Mělník, a kde ještě za Mělníkem až k Litoměřicům, pořád ta voda je zatopila. Tak se rozmohla až do Chvatěrub, šla starými řečišťaty až do Veltrus a tam stavení pobořila a odtud se rozlila až pod Zlosyň, až do Vojkovic, takže Vojkovice ze všech stran vodou byly obklíčeny. U Dušník byly všechny hráze ztrhaný a pole strhaný a některý pískem aneb šutrem zanešený, takže sedláci mnoho set za to platit musili, aby jim to lidé vyvezli.“ Ouholický sedlák Pražák k této velké vodě kromě jiného uvedl: „Kubantov se podobal pískové poušti, neboť protržením dušnické hráze tam dostala voda hrozně prudký běh a tím tolik písku tam nanesla, že nebylo ani pomyšlení poznati meze neb hranice svého pozemku, tím méně snad chmelné řádky neb nějaké zaseté obilí… Po této povodni byly konané v celém mocnářství sbírky pro obce povodní poškozené. Do naší obce se však málo dostalo, bezpochyby proto, že jsme nevěděli u koho se včas hlásit. V Dušníkách byli již v této věci šťastnější, neboť vlastník č. 10 v Dušníkách dostal almužnu 1800 zlatých.“
Při únorové povodni roku 1862, která byla způsobena silnými dešti a rychlou oblevou, u Kralup vzala voda 30 [[sáh]]ů kolejí a trať musela být přeložena. Taktéž o deset let později, roku 1872, povodeň ničila a opětovně vytvořila jezero od Dušník k soutoku Vltavy s Labem. Zimní povodně napáchaly škody v polích zejména tím, že přinášely množství materiálu, písku a štěrku, a tím pole znehodnocovaly, naplavené dříví také ničilo konstrukce chmelnic a stromy.
Povodeň z roku 1872 měla charakter povodně bleskové, která přichází bez předchozího očekávání (ne po dlouhé a na sníh bohaté zimě), zpravidla vinou přívalových srážek.Nejvíce zasáhla povodí Berounky, kde způsobila stovky (!) ztrát na životech. Její síla v dolním Povltaví nebyla tak ničivá.
Větší následky pro obyvatele měly obecně právě povodně jarní a letní, při kterých byla znehodnocena veškerá úroda, jako v roce 1890, kdy jezero počínající u Dušník dosahovalo až k Neratovicím. Podle pamětnice Rosalie Králové za povodně roku 1890 voda stoupla tak, že zatopila i stavení přívozníka č. 6, takže musel odvést kozy do statku č. 10 na dvůr, kde byl už shromážděn dobytek z celé vsi. Pro chléb se tehdy jezdilo do Vojkovic na loďce. Povodeň se stala velmi "mediálně známou" hlavně vinou fotografie znovu poničeného pražského Karlova mostu, kterou otiskly nejen české noviny.
Povodeň se pak opakovala v menším rozsahu roku 1897 a 1899, kdy 15. září nešťastným pádem při vytahování stavidel za povodně zemřel tehdy šedesátiletý Antonín Bittner, poklasný z Dušník.
==== 20. století ====
Jediné dvě velké dušnické povodně 20. století líčí kronika obce Dušníky:
„10. března 1940 se Vltava též rozvodnila. Toho dne se hnuly ledy, které byly na Vltavě, zejména u Štěchovic, nakupeny. V Dušníkách chybělo jen několik decimetrů, aby se voda přelila přes ochrannou hráz. Zpětná voda Vltavy zaplavila chmelnice mezi řekou a vsí, cesta od přívozu do vsi byla u Herbstovy zahrady pod vodou. Zpětná voda zatopila i obecní silnici Dušníky-Všestudy „v topolách“ a tekla přes chmelnice u silnice, nynější višňovku Fruty k Vojkovicím. Během povodně dušničtí občané byli v pohotovosti a hráz u „společné tůně“ zesilovali pytli písku. Po povodni byla cesta k přívozu opravena, zvýšena, zejména u Herbstovy zahrady a uválcována okresním parním válcem.
V roce 1954 po suchém červnu začalo prvý týden v červenci vydatně pršet. Ve Vltavě začala voda stoupat, vylila se z břehů a dne 10. července dostoupila největší výše, a to asi 1,30 m pod korunu hráze u přívozníkova domku. Opadávání velké vody trvalo týden.“ Povodeň roku 1954 naštěstí zmírnilo rozestavěné vodní dílo Slapy, které značně snížilo kulminaci. Mýtus o kaskádě, která zabrání všem povodním, byl na světě.
==== 21. století ====
[[Povodeň v Česku (2002)|Povodně roku 2002]], které ostatně postihly i další středoevropské země, byly způsobeny dvěma vlnami tlakové níže s nimi spojenými frontálními systémy. Vydatné deště trvaly ve dnech 6.–7. srpna, a to převážně v jižních Čechách, a poté 11.–13. srpna, kdy zasáhly celé povodí Vltavy včetně přítoků Sázavy a Berounky. Obě tlakové níže zasáhly naše území nejdeštivějším sektorem a nadto postupovaly pomalu. Nádrže vltavské kaskády nasycené první vlnou vody z Vltavy a přítoků, kterou dokázaly zvládnout, neodolaly druhé vlně. Vltavská kaskáda přestala vodu zadržovat. Vltava v Praze kulminovala 13. srpna s 782 cm při průtoku 5160 m<sup>3</sup>/s, což odpovídá opakování jednou za 500 let. Poté stupňovitě klesala v souvislosti se snižováním odtoku vltavské [http://www.pvl.cz/files/download/hydrologicke-informace/zpravy-o-povodni/2002-08-zprava-o-povodni.pdf kaskády] {{Webarchive|url=https://web.archive.org/web/20140626101836/http://www.pvl.cz/files/download/hydrologicke-informace/zpravy-o-povodni/2002-08-zprava-o-povodni.pdf |date=2014-06-26 }}.
[[Soubor:Protržená povodňová hráz u Dušník - srpen 2002.jpg|vlevo|náhled|Protržená povodňová hráz u Dušník - srpen 2002]]
Teprve po pražské kulminaci dopadly na naši obec nejtěžší následky povodně. Pražské Staré Město zachránily protipovodňové stěny, dále na sever se Vltava rozlévala do šíře, kdekoli to terén umožnil, tedy i v oblasti severně od Kralup po Mělník.
[[Soubor:Povodeň 2013 Dušníky nad Vltavou.jpg|náhled|Letecký snímek Dušník nad Vltavou při povodni 2013]]
V Dušníkách a okolí byla hráz na několika místech protržena. U Všestud se voda prodrala proudem pod dálničním mostem (dálnice D8), který poškodila tak, že byl na čas zastaven, posléze omezen provoz dálnice. Most byl provizorně opraven a poté zásadním způsobem zpevněn. Veškerá pole a chmelnice byly pod vodou. V Dušníkách byly zaplaveny všechny domy s výjimkou čp. 10 a čp. 11. Veškeré obyvatelstvo bylo evakuováno a týden mohlo sledovat svou ves z protějšího vyvýšeného břehu u Mlčechvost. Při odklízení povodňových škod pomáhali kromě armádních a bezpečnostních složek také dobrovolníci.
Tlaková níže a vydatné srážky včetně přívalových dešťů byly na vině další z povodní, která proběhla povodím Vltavy počátkem června '''2013'''. Květen byl srážkově nadprůměrný a zmíněná vlna dešťů zvýšila už tak zvýšené hladiny toků. Srážky byly v jižních a středních Čechách vydatné a prudké, ke zhoršení situace přispěly drobnější taky a neměřitelné mezipovodí (stráně svažující se přímo do toků), které hrají obvykle při povodních okrajovou roli.
Voda byla v prvních dnech června zadržována vltavskou kaskádou, aby bylo možné připravit hlavní město na největší nápor. 2. června došlo k evakuaci Dušník (odmítli pouze obyvatelé domu čp. [https://www.flickr.com/photos/yancad/sets/72157634030488853/ 12]) a 3. června kulminovala voda v Praze a poté i ve Vraňanech na 785 cm / 3140 m<sup>3</sup> (11:40)[http://www.vsestudy.cz/2013/06/vody-vic-nez-v-roce-2006-a-stale-pribyva/].
Starostové Kralup a Mělníka žádali opakovaně po zajištění Prahy snížení průtoků vltavské kaskády, aby silně zvýšený odtok nepostihl jejich města tak drtivě. Na žádost starosty Kralup nad Vltavou Petra Holečka nakonec ministr zemědělství Petr Bendl rozhodl o snížení odtoku o 400 m<sup>3</sup>/s, čímž mj. pravděpodobně zabránil i zaplavení přízemí více domů v Dušníkách a dalším škodám. V Dušníkách bylo částečně zatopeno přízemí dvou domů, zahrady a [https://www.facebook.com/Dusniky/ sklepy]. Voda se opět dostala do vesnice od Dědibab a dále průrvou pod dálničním mostem u Všestud. Most, zpevněný v roce 2002, musel být včetně náspů opět opraven v následujících měsících. Voda totiž protrhla násep u Všestud, který nebyl zpevněn, a následný silný proud vymlel násep pod vozovkou u jedné části mostu tak, že odkryl až základy. Nakonec byly základy zpevněny a doplněny a násep u Všestud vybudován zcela nový s využitím železných [https://www.flickr.com/photos/yancad/sets/72157642751617153 prvků]. [https://www.rsd.cz/wps/portal/web/rsd/vyhledavani/!ut/p/a1/jZHNToNAFIWfxQXb3kvLn-5g0Bap2KhNYTYNhUlLAkyFAezj-DC-l9PGaNSUMrtJvi_nzBmgEAIt4zbbxiLjZZwf79RY-57vqQ5BfzpBFW3DvZ3o8xdEdSyBSAJ45tj420f0TLQ1x9UXUzJBC7_8H8AhRAJ3OmqLAB9tbVg-mdozzZzLRM0ao-c6M9e8fpBpxjC_p-AFfwX0hPQ1OAF9E_0Z6d8G90C3Od-c_iPaCbG_UVDBrutG2aYYJbxQMOeiqRWsWVwlO6A5T-KcQcjKwcprw6oDhO3HO6tFkx4gktOYZ98-M2DVZqyD52P_mjdVwggvBStFwFO2fmJlyqqL60o14Xvmpd8CRA7QKuI02MK-WC6XIWaLYmUJ6y3QW-_qE5MtXsU!/?urile=wcm:path%3A%2FPortal%2BSite%2FZ6_000000000000000000000000A0%2FZ6_CGAH47L0004820IDBHD79M00I6%2FZ6_KIKI1BC0K0BCC0A4F504PN0OA4%2FZ6_KIKI1BC0K83A70A47PA5RD1086%2F89e3e893-b723-4736-acc8-70c8464159ae] {{Webarchive|url=https://web.archive.org/web/20200607035715/https://www.rsd.cz/wps/portal/web/rsd/vyhledavani/%21ut/p/a1/jZHNToNAFIWfxQXb3kvLn-5g0Bap2KhNYTYNhUlLAkyFAezj-DC-l9PGaNSUMrtJvi_nzBmgEAIt4zbbxiLjZZwf79RY-57vqQ5BfzpBFW3DvZ3o8xdEdSyBSAJ45tj420f0TLQ1x9UXUzJBC7_8H8AhRAJ3OmqLAB9tbVg-mdozzZzLRM0ao-c6M9e8fpBpxjC_p-AFfwX0hPQ1OAF9E_0Z6d8G90C3Od-c_iPaCbG_UVDBrutG2aYYJbxQMOeiqRWsWVwlO6A5T-KcQcjKwcprw6oDhO3HO6tFkx4gktOYZ98-M2DVZqyD52P_mjdVwggvBStFwFO2fmJlyqqL60o14Xvmpd8CRA7QKuI02MK-WC6XIWaLYmUJ6y3QW-_qE5MtXsU%21/?urile=wcm%3Apath%3A%2FPortal%2BSite%2FZ6_000000000000000000000000A0%2FZ6_CGAH47L0004820IDBHD79M00I6%2FZ6_KIKI1BC0K0BCC0A4F504PN0OA4%2FZ6_KIKI1BC0K83A70A47PA5RD1086%2F89e3e893-b723-4736-acc8-70c8464159ae |date=2020-06-07 }}
== Doprava ==
Obcí prochází silnice III/10151.
Dušníky leží asi 7 km od exitu „Uhy“ – dálnice [http://www.ceskedalnice.cz/dalnice/d8/ D8] – jsou tedy dopravně dobře dostupné. Hromadnou dopravu obstarávají autobusy PID č. 473 – v pracovní dny celkem 7 spojů směrem na Kralupy nad Vltavou, o víkendu bez autobusového spojení. Poříční přívoz do vsi Vepřek zanikl po roce 1957.
== Turistika ==
Ves se nachází na rušné mezinárodní cyklostezce Praha – Drážďany.
Dušníky jsou přístupné pro pěší nebo cyklisty z veltruského parku (asi 3 km od Červeného mlýna) přes tzv. Kubantov, kdysi překrásnou oblast mezi Mlýnským potokem a Vltavou. Na Kubantově najdeme zbytky lužních porostů, luk, ovocných sadů a vltavských tůní (tzv. rybníčků, které sahají po Dušníky). Pěší turista musí ovšem překonat (podejít) hlučný dálniční most, který oblast znehodnocuje.
Na druhou stranu od Dušníky podél Vltavy lze dojít po hrázi nebo pobřežní polní cestě až k vraňansko-dědibabskému jezu a vodní elektrárně. V ohybu hráze najde pozorný chodec trosky nejzazšího pavilonu veltruského [http://www.vsestudy.cz/2006/02/tajemna-stavba-chotkovsky-altanek/ parku].
Okolí rybníčků a vltavské pobřeží je oblíbeným místem rybářů, konají se zde příležitostně i rybářské závody.
Ve statku čp. 10 byla zřízena v roce 2017 restaurace a penzion.
== Zajímavosti ==
Ve vesnici mají stálou adresu dva spolky, [http://www.dolnipovltavi.cz/ Dolní Povltaví] a [http://havranek.dolnipovltavi.cz/ Rodinné centrum Havránek], na hranici katastru Dušník a Všestud sídlí motorkářské sdružení [https://www.feistypistons.cz Feisty Pistons].
Od roku 2009 Dušníky každoročně na přelomu července a srpna probíhá běžci oblíbený [http://www.mkmirejovice.cz Miřejovický půlmaraton].
V roce 2001 byl zaznamenán případ tornáda na území [http://www.tornada-cz.cz/pripady/vsestudy-okr-melnik:a170.htm Dušník] (poblíž sušárny chmele u dálnice D8). Podle pamětníků se v těchto místech podobné úkazy dějí opakovaně.
[[Soubor:Znak-dušník.png|náhled|165x165pixelů|Neoficiální znak Dušník nad Vltavou od Václava Landsingera]]
V roce 2016 vytvořil Václav Landsinger z Dušník neoficiální znak osady Dušníky, který odkazuje na obecní znak všestudský a připomíná přítomnost Vltavy, chmelařskou tradici i pamětihodnost - zvoničku na návsi.
Ve školní kronice ve Zlosyni je zanesena pověst o vyhubení Vršovců. Podle vypravování Fr. Homolky, rolníka ve Zlosyni, se tradovalo, že u dušnických lesíků byl zavražděn poslední Vršovec. Zejména dušnické děti věřily, že hlubokou Černou tůň za Dušníky hlídá černý pes.
V 19. století a ještě na počátku 20. století byl poštovní úřad pro Dušníky umístěn v Jenišovicích. Transport pošty ale znemožňovala často rozvodněná Vltava a zdržoval přívoz, Dušničtí proto opakovaně žádali o změnu poštovního úřadu, ke které však došlo až po vzniku Československé republiky (dodnes je poštovním úřadem pošta Vojkovice, ačkoliv pro nadřazenou obec Všestudy platí poštovní úřad Veltrusy).
== Odkazy ==
=== Reference ===
<references />
=== Externí odkazy ===
* {{commonscat}}
* [http://dusniky.vsestudy.cz webové stránky]
* [https://www.facebook.com/Dusniky stránka na Facebooku]
{{Pahýl}}
{{Části české obce}}
{{Autoritní data}}
{{Portály|Česko|Geografie}}
[[Kategorie:Všestudy (okres Mělník)]]
[[Kategorie:Vesnice v okrese Mělník]]
[[Kategorie:Svazek obcí Dolní Povltaví]]
[[Kategorie:Sídla ve Středolabské tabuli]]
[[Kategorie:Sídla na Vltavě]]
a86954pvuw30mg9ev5pkl45kqt5qzs3
Erna Solberg
0
114118
738121
688966
2026-04-16T08:14:38Z
Dwalden test acctcreation28
73580
738121
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738122
738121
2026-04-16T08:15:05Z
Dwalden test acctcreation28
73580
738122
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738123
738122
2026-04-16T08:16:01Z
Dwalden test acctcreation28
73580
738123
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738124
738123
2026-04-16T08:16:48Z
Dwalden test acctcreation28
73580
738124
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738125
738124
2026-04-16T08:26:16Z
Dwalden test acctcreation28
73580
738125
wikitext
text/x-wiki
[https://duckduckgo.com]{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
7c6hdtnarblwe7sqmxejq7aqfdjx00h
738126
738125
2026-04-16T08:27:49Z
Dwalden test acctcreation28
73580
showcaptcha
738126
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738127
738126
2026-04-16T08:30:38Z
Dwalden test acctcreation28
73580
738127
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738128
738127
2026-04-16T08:59:18Z
Dwalden test acctcreation28
73580
738128
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738129
738128
2026-04-16T09:00:28Z
Dwalden test acctcreation28
73580
showcaptcha
738129
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738130
738129
2026-04-16T09:03:40Z
Dwalden test acctcreation28
73580
showcaptcha
738130
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738131
738130
2026-04-16T09:08:16Z
Dwalden test acctcreation28
73580
showcaptcha
738131
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738133
738131
2026-04-16T09:34:15Z
Dwalden test acctcreation28
73580
showcaptcha
738133
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738134
738133
2026-04-16T09:38:49Z
Dwalden test acctcreation28
73580
showcaptcha
738134
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738140
738134
2026-04-16T09:58:04Z
Dwalden test acctcreation28
73580
showcaptcha
738140
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
738141
738140
2026-04-16T10:24:27Z
Dwalden test acctcreation28
73580
showcaptcha
738141
wikitext
text/x-wiki
this is an edit{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
4uxdx21qdukwngg0gjca42n8x2lew4q
738143
738141
2026-04-16T10:31:35Z
Dwalden test acctcreation28
73580
showcaptcha
738143
wikitext
text/x-wiki
{{Infobox officeholder
| name = Erna Solberg
| honorific-suffix = [[Member of Parliament|MP]]
| image = File:Erna Solberg Norges Bank Årstale (191341).jpg
| caption = Erna Solberg in February 2016
| office1 = [[Conservative Party (Norway)|Leader of the Conservative Party]]
| term_start1 = 9 May 2004
| term_end1 =
| deputy1 = [[Erling Lae]]<br/> [[Per-Kristian Foss]] <br> [[Bent Høie]] <br> [[Jan Tore Sanner]]
| predecessor1 = [[Jan Petersen]]
| successor1 =
| office = [[Prime Minister of Norway ]]
| monarch = [[Harald V of Norway|Harald V]]
| term_start = 16 October 2013
| term_end =
| predecessor = [[Jens Stoltenberg ]]
| successor =
| office3 = [[Leader of the Opposition]]
| monarch3 = [[Harald V of Norway|Harald V]]
| primeminister3 = Jens Stoltenberg
| term_start3 = 17 October 2005
| term_end3 = 16 October 2013
| predecessor3 = Jens Stoltenberg
| successor3 = Jens Stoltenberg
| office4 = [[Minister of Local Government and Modernisation (Norway)|Minister of Local Government]]
| primeminister4 = [[Kjell Magne Bondevik]]
| term_start4 = 19 October 2001
| term_end4 = 17 October 2005
| predecessor4 = [[Sylvia Brustad]]
| successor4 = [[Åslaug Haga]]
| office5 = [[Leader|Leader of the]] [[Conservative Party (Norway)|Conservative]] Women's Association
| term_start5 = 7 March 1993
| term_end5 = 29 March 1998
| predecessor5 = [[Siri Frost Sterri]]
| successor5 = Sonja Sjøli
| office6 = [[Storting|Member of the Norwegian Parliament]]
| term_start6 = 2 October 1989
| term_end6 =
| constituency6 = [[Hordaland]]
| birth_date = {{birth date and age|1961|02|24|df=y}}
| birth_place = [[Bergen]], [[Norway]]
| nationality = [[Norwegians|Norwegian]]
| death_date =
| death_place =
| party = [[Conservative Party (Norway)|Conservative]]
| spouse = Sindre Finnes
| children = 2
| residence = [[Inkognitogata 18]]
| alma_mater = [[University of Bergen]]
| website = https://erna.no/
}}
'''Erna Solberg''' ({{IPA-no|ˈæ̀ːʁnɑ ˈsûːlbæʁɡ|lang}}; born 24 February 1961) is a Norwegian politician serving as [[Prime Minister of Norway]] since 2013 and Leader of the [[Conservative Party of Norway|Conservative Party]] since May 2004.<ref>{{Cite web|url=https://www.globalpartnership.org/blog/15-women-leading-way-girls-education|title=15 women leading the way for girls’ education|website=globalpartnership.org|language=en|access-date=2019-03-22}}</ref> Inspired by [[Margaret Thatcher]]'s "Iron Lady" nickname, Solberg has been given the nickname "Iron Erna".<ref>Brunsdale, Mitzi M. (2016). ''Encyclopedia of Nordic Crime Fiction: Works and Authors of Denmark, Finland, Iceland, Norway and Sweden Since 1967''. McFarland. Page 274. {{ISBN|9780786475360}}.</ref><ref>Thompson, Wayne C. (2015). ''Nordic, Central, and Southeastern Europe 2015-2016''. Rowman & Littlefield. Page 54. {{ISBN|9781475818833}}.</ref><ref>Leiren, Terje and Jan Sjåvik (2019). ''Historical Dictionary of Norway''. Rowman & Littlefield. Page 258. {{ISBN|9781538123126}}.</ref><ref>Rohde, Achim and Christina von Braun (2017). ''National Politics and Sexuality in Transregional Perspective: The Homophobic Argument''. Routledge. Page 44. {{ISBN|9781317090007}}.</ref>
Solberg was first elected to be a member of the [[Storting]] in 1989 and served as [[Minister of Local Government and Regional Development]] in [[Bondevik's Second Cabinet]] from 2001 to 2005. During her tenure, she oversaw the tightening of [[immigration policy]] and the preparation of a proposed reform of the [[administrative divisions of Norway]].<ref>{{cite encyclopedia|title=Erna Solberg |url=http://nbl.snl.no/Erna_Solberg|last1=Hellberg|first1=Lars|encyclopedia=Norsk biografisk leksikon|accessdate=May 23, 2014|language=no}}</ref> After the [[2005 Norwegian parliamentary election|2005 election]], she chaired the Conservative Party parliamentary group until 2013. Solberg has emphasized the social and ideological basis of the Conservative policies, although the party also has become visibly more pragmatic.<ref>{{cite news|last=Alstadheim|first=Kjetil B.|date=December 22, 2012|title=Solberg-og-dal-banen|newspaper=Dagens Næringsliv|location=Oslo |page=2|language=no}}</ref>
After winning the [[2013 Norwegian parliamentary election|September 2013 election]], she became the [[List of heads of government of Norway|28th]] [[Prime Minister of Norway]] and the second female to hold the position after [[Gro Harlem Brundtland]].<ref>PM 1981, 1986–1989, 1990–1996.</ref> [[Solberg's Cabinet]], often referred to informally as the "Blue-Blue Cabinet", was a two-party minority government consisting of the Conservative Party and [[Progress Party (Norway)|Progress Party]]. The cabinet established a formalized co-operation with the [[Liberal Party (Norway)|Liberal Party]] and [[Christian Democratic Party (Norway)|Christian Democratic Party]] in the Storting.<ref>{{cite web|accessdate=May 23, 2014 |language=no |url=https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |title=Avtale mellom Venstre, Kristelig Folkeparti, Fremskrittspartiet og Høyre |publisher=Høyre |url-status=dead |archiveurl=https://web.archive.org/web/20140528191008/https://www.hoyre.no/filestore/Filer/Politikkdokumenter/Samarbeidsavtale.pdf |archivedate=May 28, 2014 }}</ref> The Government was re-elected in the [[2017 Norwegian parliamentary election|2017 election]], and was extended to include the Liberal Party in January 2018.<ref>{{Cite news|url=https://www.reuters.com/article/us-norway-government/norways-liberals-to-join-conservative-led-government-idUSKBN1F30Q4|title=Norway's Liberals to join Conservative-led government|last=Dagenborg|first=Joachim|work=U.S.|access-date=2018-04-26|language=en-US}}</ref> This extended minority coalition is informally referred to as the "Blue-Green cabinet." In May 2018, Solberg surpassed [[Kåre Willoch]] and became the longest serving Prime Minister of Norway to represent the Conservative party.<ref>{{Cite news|url=https://www.nrk.no/hordaland/passerer-willoch-_-solberg-blir-hoyres-lengstsittende-statsminister-1.14045170|title=Passerer Willoch – Solberg blir Høyres lengstsittende statsminister|last=Løland|first=Leif Rune|work=NRK|access-date=2018-06-01|language=nb-NO}}</ref> In January 2019, the government was extended to also include the [[Christian Democratic Party (Norway)|Christian Democrats]] and thereby secured a majority in Parliament.
==Early life and education==
Solberg was born in [[Bergen]] in western Norway and [[Erna_Solberg#Other_news_stories|grew up]] in the affluent [[Kalfaret]] neighbourhood. Her father, [[Asbjørn Solberg]] (1925–1989), worked as a consultant in the [[Bergen Sporvei]], and her mother, Inger Wenche Torgersen (1926–2016), was an office worker. Solberg has two sisters, one older than her and one younger.<ref>{{cite news|last=Johansen |first=Per Kristian |url=http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |date=February 9, 2009 |publisher=Norwegian Broadcasting Corporation |language=no |accessdate=May 23, 2014 |archiveurl=https://web.archive.org/web/20131015153010/http://www.nrk.no/programmer/radio/stjerneklart/1.6473179 |title=Erna Solberg varsler tøffere integrering |archivedate=October 15, 2013 |url-status=dead }}</ref>
Solberg had some struggles at school and at the age of 16 was diagnosed as suffering from [[dyslexia]]. She was, nevertheless, an active and talkative contributor in the classroom.<ref>{{cite news|author=Eivind Fondenes and Aslak Eriksrud |url=http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |title=Partifellene, syntes ikke Erna Solberg var blå nok |trans-title=Comrades did not Erna Solberg was blue enough |publisher=[[TV 2 (Norway)|TV2]] |accessdate=April 2, 2013 |language=no |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.tv2.no/nyheter/vartlilleland/partifellene-syntes-ikke-erna-solberg-var-blaa-nok-3980798.html |url-status=dead }}</ref> In her final year as a high-school student in 1979, she was elected to the board of the [[School Student Union of Norway]], and in the same year led the national charity event [[Operasjon Dagsverk]], in which students collected money for [[Jamaica]].
In 1986, she graduated with her [[cand.mag.]] degree in [[sociology]], [[political science]], [[statistics]] and [[economics]] from the [[University of Bergen]]. In her final year, she also led the [[Students' League of the Conservative Party (Norway)|Students' League of the Conservative Party]] in Bergen.
Since 1996 she has been married to Sindre Finnes, a businessman and former Conservative Party politician, with whom she has two children.<ref>{{cite news|url=http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |title=After softening, 'Iron Erna' Solberg set to become Norway's PM |agency=[[Reuters]] |date=September 10, 2013 |work=[[Daily News and Analysis]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.dnaindia.com/world/1886754/report-after-softening-iron-erna-solberg-set-to-become-norway-s-pm |archivedate=June 30, 2009 |url-status=dead }}</ref><ref>{{Cite web|url=https://www.forbes.com/profile/erna-solberg/|title=Erna Solberg|website=Forbes|language=en|access-date=2019-03-22}}</ref> The family has lived in both Bergen and [[Oslo]].
==Political career==
[[Image:Erna Solberg at party congress 2009.JPG|thumb|left|Erna Solberg during a party congress in 2009.]]
===Local government===
Solberg was a deputy member of [[Bergen]] city council in the periods 1979–1983 and 1987–1989, the last period on the executive committee. She chaired local and municipal chapters of the [[Norwegian Young Conservatives|Young Conservatives]] and the Conservative Party.
===Parliamentarian===
She was first elected to the [[Storting|Storting (Norwegian Parliament)]] from [[Hordaland]] in 1989 and has been re-elected five times. She was also the leader of the national Conservative Women's Association, from 1994 to 1998.
===Minister of Local Government and Regional Development===
From 2001 to 2005 Solberg served as the [[Minister of Local Government and Regional Development (Norway)|Minister of Local Government and Regional Development]] under Prime Minister [[Kjell Magne Bondevik]]. Her alleged tough policies in this department, including a firm stance on asylum policy, earned her the nickname "Jern-Erna" (Norwegian for "Iron Erna") in the [[News media|media]].<ref>{{cite news |last=Morken |first=Johannes |date=8 May 2009 |url=http://www.vl.no/samfunn/article9794.zrm |language=no |trans-title=Erna Solberg suggests tougher integration |newspaper=[[Vårt Land (Norwegian newspaper)|Vårt Land]] |accessdate=July 11, 2010 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |title=Erna Solberg varsler tøffere integrering |archivedate=June 30, 2009 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.vl.no/samfunn/article9794.zrm |date=June 30, 2009 }}</ref> [[File:Flickr - europeanpeoplesparty - EPP Congress Warsaw (91).jpg|thumb|Solberg, [[José Manuel Barroso]] and [[Mariano Rajoy]] at [[European People's Party]] Congress in Warsaw in 2009]]
In fact, numbers show that the Bondevik government, of 2001–2005, actually let in thousands more [[asylum seeker]]s than the subsequent centre-left Red-Green government, of 2005–2009.<ref>{{cite news |accessdate=August 29, 2010 |language=no |url=http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i,-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |work=[[Bergens Tidende]] |date=September 13, 2009 |title=Det (var) altså flere asylsøkere som kom til Norge under den forrige Bondevik-regjeringen som Erna var med i, enn det har kommet nå under den rød-grønne regjeringen |trans-title=It (was) thus more asylum seekers coming to Norway during the previous Bondevik government that Erna was in, than it has now come under the red-green government |first=Helge O. |last=Svela |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/innenriks/faktasjekk/%26laquo%3BDet-%5Bvar%5D-altsaa-flere-asylsoekere-som-kom-til-Norge-under-den-forrige-Bondevik-regjeringen-som-Erna-var-med-i%2C-enn-det-har-kommet-naa-under-den-roed-groenne-regjeringen.%26raquo%3B-928308.html |date=June 30, 2009 }}</ref>
As Minister, Solberg instructed the Norwegian Directorate of Immigration to expel [[Mulla Krekar]], being a danger to national security. Later, terrorism charges were filed against Krekar for a death threat he uttered in 2010 against Erna Solberg.
===Party Leader===
She served as deputy leader of the Conservative Party from 2002 to 2004 and, in 2004, she became the party leader.
===Prime Minister===
{{Updatesection|date=April 2018}}
[[File:Secretary Kerry Delivers Remarks at a Working Luncheon He Hosted in Honor of Nordic Leaders (26926265901).jpg|thumb|Solberg and other Nordic leaders in Washington, D.C., 13 May 2016]]
[[File:President Donald Trump and Prime Minister Erna Solberg; January 2018.jpg|thumb|right|Solberg and U.S. President [[Donald Trump]] in 2018]]
[[File:The Prime Minister, Shri Narendra Modi meeting the Prime Minister of Norway, Ms. Erna Solberg, on the sidelines of India-Nordic Summit, in Stockholm, Sweden on April 17, 2018 (1).JPG|thumb|right|[[Prime Minister of Norway|Norwegian Prime Minister]] [[Erna Solberg]] (left) met with [[Prime Minister of India|Indian Prime Minister]] [[Narendra Modi]] (right), on the sidelines of India-Nordic Summit, in Stockholm in April 2018]]
Solberg became the [[head of government]] after winning the general election on 9 September 2013 and was appointed Prime Minister on 16 October 2013. Solberg is Norway's second female Prime Minister after [[Gro Harlem Brundtland]].<ref>{{cite news|date=October 16, 2013 |url=http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |title=Dette er utfordringene som møter de nye statsrådene |trans-title=These are the challenges facing the new ministers |newspaper=[[Aftenposten]] |accessdate=October 16, 2013 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.aftenposten.no/nyheter/iriks/politikk/Dette-er-utfordringene-som-moter-de-nye-statsradene-7341175.html |url-status=dead }}</ref>
The Government was re-elected in [[2017 Norwegian parliamentary election|2017]], making Solberg the country's first conservative leader to win re-election since the 1980s.<ref name="veconomist" >{{cite news|author=|title=Norway’s centre-right coalition is re-elected|url=https://www.economist.com/news/europe/21728996-letting-populists-join-governments-can-be-good-way-defang-them-norways-centre-right-coalition|work=[[The Economist]]|date=14 September 2017}}</ref> The [[Centre-right politics|centre-right]] parties were also able to maintain the majority in the [[Storting]].
Erna Solberg has combined numerous national positions as Minister, Parliamentarian and regional politician with a strong commitment to global solutions for development, growth and conflict resolution. <ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
She also negotiated with the [[Liberal Party (Norway)|Liberals]] to join the government in 2018. <ref> https://www.nrk.no/norge/venstre-sier-ja-til-a-ga-i-regjering-1.13865847</ref> The Liberals officially joined the Solberg Cabinet on 17 January 2018. After the [[Christian Democratic Party (Norway)|Christian Democrats]] alliance conflict that lasted from September to November 2018, they eventually negotiated to join the Solberg Cabinet on the grounds of a minor change in the abortion law, something that caused harsh backlash from the public and critics alike. The Christian Democrats officially joined the Cabinet on 22 January 2019.<ref> https://www.nrk.no/nyheter/krfs-veivalg-1.12282551</ref>
===International engagements===
As Prime Minister, and former Chair of the Norwegian delegation to the NATO Parliamentary Assembly, she has championed transatlantic values and security.
In 2018 she assembled a global High Level Panel on sustainable ocean economy and introduced the topic at the G7 Summit. Her Government supports the World Bank's PROBLUE <ref>https://www.worldbank.org/en/programs/problue</ref> initiative to prevent marine damage.
From 2016 the Prime Minister has co-chaired the UN Secretary General's Advocacy group for the Sustainable Development goals. Among the goals, she takes a particular interest in access to quality education for all, in particular girls and children in conflict areas. This was also central in her work as MDG Advocate from 2013 to 2016.
In one her many keynote speeches she stated that there is still a need for traditional aid and humanitarian assistance in marginalised and conflict-ridden areas of the world. The SDGs, however, take a holistic view of global development, and integrate economic, social and environmental factors.<ref>https://www.regjeringen.no/no/aktuelt/keynote-speech---european-development-days/id2556225/</ref>
Solberg has shown particular interest in gender issues, such as girl's rights and education. Together with [[Graça_Machel|Graca Machel]] she has expressed the hope that in 2030 no factors such as poverty, gender and cultural beliefs will prevent any of today's ambitious young girls from standing confidently on the world stage <ref>http://news.trust.org/item/20160308130714-qh1w6/</ref>
In 2016 she held a lecture at the International Institute for Strategic Studies The Global Goals in Singapore, addressing a road map to a Sustainable, Fair and More Peaceful Futurethe International Institute for Strategic Studies<ref>https://www.regjeringen.no/no/aktuelt/the-global-goals---a-roadmap-to-a-sustainable-fair-and-more-peaceful-future/id2483522/</ref>
Solberg has secured significant financial support for the Global Partnership for Education and hosted the Global Finance Facility for women's and children's health pledging Conference in Oslo in November 2018. Her firm belief is that investment in education will accelerate progress on all other SDG goals.
In April 2017 she held a speech on globalization and development at Peking University in Beijing <ref>https://www.regjeringen.no/no/aktuelt/sustainable-development---making-globalisation-work-for-people-and-the-planet/id2549233/</ref>
She was awarded the inaugural Global Citizen World Leader Award in 2018 for her international engagement.<ref>https://www.regjeringen.no/en/dep/smk/organization-map/prime-minister-erna-solberg/id742859/</ref>
In Solberg's speech to the UN General Assembly in 2019 she advocated for Norway's candidacy for a non-permanent seat on the Security Council for 2021–2022. She upheld that UN needs to be strengthened and that the world needs strong multilateral cooperation and institutions to tackle global challenges such as climate change, cyber security and terrorism <ref>https://www.regjeringen.no/en/aktuelt/norways-statement-at-the-united-nations-general-assembly/id2670474/</ref>
===Other news stories===
In 2014 she participated at the [[Ministry of Agriculture and Food (Norway)|Agriculture and Food]] meeting which was held by [[Sylvi Listhaug]] where Minister of Transportation [[Ketil Solvik-Olsen]] and Minister of Climate and Environment [[Tine Sundtoft]] also were present. Later on, the four took a picture which appeared on the [[Government.no]] website on 14 March the same year.<ref>{{cite news|date=March 14, 2014 |accessdate=April 12, 2014 |url=http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |title=Skogens rolle i klimasammenheng |trans-title=The forest's role in climate change |publisher=[[Government.no]] |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.regjeringen.no/nb/dep/lmd/aktuelt/nyheter/2014/mars-14/Klima-for-.html?id=753032 |url-status=dead }}</ref> In April of the same year she criticized [[European Court of Justice|European Court]] over [[data retention]] which [[Telenor|Telenor Group]] argued can be used without court proceedings.<ref>{{cite news |url=http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |title=Erna Solbergs datalagring kan bli torpedert |trans-title=Erna Solberg: Data storage can be torpedoed |newspaper=[[Bergens Tidende]] |accessdate=April 12, 2014 |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090630013226/http://www.bt.no/nyheter/lokalt/Erna-Solbergs-datalagring-kan-bli-torpedert-3098723.html#.U0mWCCwo5lb |date=June 30, 2009 }}</ref>
In 2017, the [[Embassy of Russia in Oslo|Russian Embassy]] in Oslo had accused Norwegian officials and intelligence of using “false and disconnected [[Anti-Russian sentiment|anti-Russian rhetoric]]” and “scaring Norway’s population” about a "mythical Russian threat". In response, Prime Minister Solberg said: “This is an example of Russian propaganda that often comes when there’s a focus on security policy. There is nothing in this that’s new to us.”<ref>http://www.newsinenglish.no/2017/02/17/russian-embassy-in-oslo-calls-its-relations-with-norway-unsatisfactory/</ref>
Solberg has tried to maintain and improve the [[China–Norway relations]], which have been damaged since Norway decided to give the [[Nobel Peace Prize]] to [[Chinese dissident]] [[Liu Xiaobo]] in 2010. In response to his death, caused by organ failure while in government custody on 13 July 2017, Solberg said that "It is with deep grief that I received the news of Liu Xiaobo's passing. Liu Xiaobo was for decades a central voice for human rights and China's further development."<ref>"[https://www.reuters.com/article/us-china-rights-reaction-idUSKBN19Y2DC West mourns Chinese dissident Liu Xiaobo, criticizes Beijing]". Reuters. 13 July 2017.</ref>
In April 2008, it was revealed that Solberg, as Minister of Local Government and Regional Development in 2004, had rejected a request for [[right of asylum|asylum]] in Norway by the [[Nuclear weapons and Israel|Israeli nuclear]] [[whistleblower]] [[Mordechai Vanunu]].<ref>{{cite news |date=September 4, 2008 |url=http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398
|author=Dennis Ravndal |title=Erna Solberg hindret Vanunu i å få asyl |trans-title=Erna Solberg prevented Vanunu in getting asylum |newspaper=[[Verdens Gang|VG]] |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/innenriks/norsk-politikk/artikkel.php?artid=531398 |archivedate=June 30, 2009 |accessdate=April 10, 2008
|url-status=dead }}</ref> While the [[Norwegian Directorate of Immigration]] had been prepared to grant Vanunu asylum, it was then decided that the application could not be accepted because Vanunu's application had been made outside the borders of Norway.<ref>{{cite news|date=September 4, 2008 |accessdate=April 10, 2008 |url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |title=Vanunu: - Håper Norge angrer asyl-avslaget |trans-title=Vanunu: - Hope Norway regrets asylum refusal |author=Stian Eisenträger |newspaper=VG |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531446 |archivedate=June 30, 2009 |url-status=dead }}</ref> An unclassified document revealed that Solberg and the government considered that [[extradition|extraditing]] Vanunu from [[Israel]] could be seen as an action against Israel and thus unfitting to the Norwegian government's traditional position as a [[Israel–Norway relations|friend of Israel]] and as a political player in the Middle East. Solberg rejected this criticism and defended her decision.<ref>{{cite news|url=http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |author=Stian Eisenträger |title=Vanunu-venner i harnisk |trans-title=Vanunu friends outraged |date=September 4, 2008 |accessdate=April 10, 2008 |newspaper=VG |archivedate=June 30, 2009 |archiveurl=https://web.archive.org/web/20090630013226/http://www.vg.no/nyheter/utenriks/midtosten/artikkel.php?artid=531497 |url-status=dead }}</ref>
==Honours==
===National honours===
*{{flag|Norway}} Commander of the [[Order of St. Olav]] (2005)
*{{flag|Norway}} [[King Harald V's Jubilee Medal 1991-2016 ]] (2016)
==References==
{{Reflist|30em}}
*{{Stortingetbio|ES}}
==External links==
{{commons category|Erna Solberg}}
{{s-start}}
{{s-off}}
{{s-bef|before=[[Sylvia Brustad]]}}
{{s-ttl|title=[[Minister of Local Government and Modernisation (Norway)|Minister of Local Government and Regional Development]]|years=2001–2005}}
{{s-aft|after=[[Åslaug Haga]]}}
|-
{{s-bef|before=[[Jens Stoltenberg]]}}
{{s-ttl|title=[[Prime Minister of Norway]]|years=2013–present}}
{{s-inc}}
|-
{{s-ppo}}
{{s-bef|before=[[Jan Petersen]]}}
{{s-ttl|title=Leader of the [[Conservative Party (Norway)|Conservative Party]]|years=2004–present}}
{{s-inc}}
{{s-end}}
{{Current NATO leaders}}
{{NorwegianPrimeMinisters}}
{{Minister of Local Government and Modernisation (Norway)}}
{{Stortinget 2001-2005}}
{{Stortinget 2005-2009}}
{{Stortinget 2009-2013}}
{{Stortinget 2013-2017}}
{{Stortinget 2017–2021}}
{{BondevikII}}
{{Conservative Party (Norway)}}
{{Authority control}}
{{DEFAULTSORT:Solberg, Erna}}
[[Category:1961 births]]
[[Category:20th-century Norwegian politicians]]
[[Category:20th-century Norwegian women politicians]]
[[Category:21st-century Norwegian politicians]]
[[Category:21st-century Norwegian women politicians]]
[[Category:Female heads of government]]
[[Category:Leaders of the Conservative Party (Norway)]]
[[Category:Living people]]
[[Category:Members of the Storting]]
[[Category:Ministers of Local Government and Modernisation of Norway]]
[[Category:Norwegian Lutherans]]
[[Category:Women members of the Storting]]
[[Category:People from Bergen]]
[[Category:Prime Ministers of Norway]]
[[Category:People educated at Langhaugen Upper Secondary School]]
[[Category:University of Bergen alumni]]
[[Category:Women prime ministers]]
[[Category:Women government ministers of Norway]]
<noinclude>
<small>This page was moved from [[:en:Erna Solberg]]. Its edit history can be viewed at [[Erna Solberg/edithistory]]</small></noinclude>
dtsfhaeovu3j2sf8hfs5b3g7kh9e2qj
Red Giant Entertainment/edithistory
0
114143
738106
690137
2026-04-16T07:41:42Z
Dwalden test acctcreation28
73580
738106
wikitext
text/x-wiki
this is an edit
{| class="wikitable"
! oldid || date/time || username || edit summary
|----
| 960098019 || 2020-06-01T03:32:23Z || Nihiltres || <nowiki>/* top */Replacing Other_uses2 template per [[Wikipedia:Templates_for_discussion/Log/2020_May_15|TfD]]</nowiki>
|----
| 960001974 || 2020-05-31T17:04:12Z || Cewbot || <nowiki>[[User:Cewbot/log/20150916/configuration|Normalize {{Multiple issues}}]]: Merge 1 template(s) into {{Multiple issues}}: Out of date</nowiki>
|----
| 951574349 || 2020-04-17T20:26:41Z || DemonDays64 Bot || <nowiki>HTTPS security. [[User talk:DemonDays64|Tell me]] if there's an issue with my edit. (via [[WP:JWB]])</nowiki>
|----
| 938801172 || 2020-02-02T12:18:17Z || AnomieBOT || <nowiki>Dating maintenance tags: {{Out of date}}</nowiki>
|----
| 938790193 || 2020-02-02T10:18:14Z || Joelfurr || <nowiki>out of date</nowiki>
|----
| 919267248 || 2019-10-02T19:18:53Z || Monkbot || <nowiki>[[User:Monkbot/task 16: remove replace deprecated dead-url params|Task 16]]: replaced (4×) / removed (0×) deprecated |dead-url= and |deadurl= with |url-status=;</nowiki>
|----
| 886116991 || 2019-03-04T11:07:54Z || Onel5969 || <nowiki>Adding [[Wikipedia:Short description|short description]]: "American comic book publisher" ([[User:Galobtter/Shortdesc helper|Shortdesc helper]])</nowiki>
|----
| 886116912 || 2019-03-04T11:07:15Z || Onel5969 || <nowiki>Disambiguated: [[Last Blood]] → [[Last Blood (Blatant Comics)]]</nowiki>
|----
| 875972457 || 2018-12-30T08:17:59Z || Miracle Pen || <nowiki></nowiki>
|----
| 870486402 || 2018-11-25T03:51:25Z || 2A02:2168:A401:4400:9095:3F31:BF34:316B || <nowiki>/* Film */</nowiki>
|----
| 850300699 || 2018-07-15T01:01:48Z || Hmains || <nowiki>standard quote handling in WP;standard Apostrophe/quotation marks in WP; MOS general fixes using [[Project:AWB|AWB]]</nowiki>
|----
| 848259232 || 2018-06-30T20:15:34Z || KolbertBot || <nowiki>Bot: [[User:KolbertBot|HTTP→HTTPS]] (v485)</nowiki>
|----
| 836144815 || 2018-04-12T22:49:15Z || InternetArchiveBot || <nowiki>Rescuing 3 sources and tagging 0 as dead. #IABot (v1.6.5)</nowiki>
|----
| 814524055 || 2017-12-09T09:12:05Z || KolbertBot || <nowiki>Bot: [[User:KolbertBot|HTTP→HTTPS]] (v478)</nowiki>
|----
| 800106058 || 2017-09-11T15:03:52Z || Shortride || <nowiki>infobox</nowiki>
|----
| 799902318 || 2017-09-10T14:14:56Z || KolbertBot || <nowiki>Bot: [[User:KolbertBot|HTTP→HTTPS]]</nowiki>
|----
| 798429242 || 2017-09-01T21:51:24Z || Gladamas || <nowiki>Reverted edits by [[Special:Contributions/85.76.46.51|85.76.46.51]] ([[User talk:85.76.46.51|talk]]): Unexplained removal of content ([[WP:HG|HG]]) (3.2.0)</nowiki>
|----
| 798429197 || 2017-09-01T21:51:05Z || 85.76.46.51 || <nowiki></nowiki>
|----
| 798428809 || 2017-09-01T21:47:56Z || 85.76.46.51 || <nowiki></nowiki>
|----
| 785551413 || 2017-06-14T04:51:25Z || 2604:2000:8191:6900:503A:993C:EE3D:3A8B || <nowiki>/* Film */ removed false films, left only red giant films, as that is all that should be here.</nowiki>
|----
| 785550756 || 2017-06-14T04:45:33Z || El cid, el campeador || <nowiki>Reverted 1 edit by [[Special:Contributions/2604:2000:8191:6900:503A:993C:EE3D:3A8B|2604:2000:8191:6900:503A:993C:EE3D:3A8B]] ([[User talk:2604:2000:8191:6900:503A:993C:EE3D:3A8B|talk]]) to last revision by Bender the Bot. ([[WP:TW|TW]])</nowiki>
|----
| 785550710 || 2017-06-14T04:45:14Z || 2604:2000:8191:6900:503A:993C:EE3D:3A8B || <nowiki>/* Film */</nowiki>
|----
| 770261878 || 2017-03-14T11:22:16Z || Bender the Bot || <nowiki>HTTP→HTTPS, per [[Wikipedia:Bots/Requests for approval/Bender the Bot 8|BRFA 8]] using [[Project:AWB|AWB]]</nowiki>
|----
| 762811415 || 2017-01-30T21:42:01Z || Tenebrae || <nowiki>You can't make a 2017 claim with a 2015 citation. Also can't add personal observation</nowiki>
|----
| 762683697 || 2017-01-30T04:30:11Z || 125.209.188.179 || <nowiki>/* Controversy */</nowiki>
|----
| 762302366 || 2017-01-27T23:12:26Z || Nerd1a4i || <nowiki>added hat link</nowiki>
|----
| 757547658 || 2016-12-31T08:22:00Z || 125.209.177.250 || <nowiki>/* Controversy */</nowiki>
|----
| 752461026 || 2016-12-01T10:54:56Z || Bender the Bot || <nowiki>/* Video Games and Apps */clean up; http→https for [[YouTube]] using [[Project:AWB|AWB]]</nowiki>
|----
| 749949302 || 2016-11-17T01:11:18Z || General Wesc || <nowiki>Missing space</nowiki>
|----
| 739689374 || 2016-09-16T10:02:29Z || 85.118.169.121 || <nowiki></nowiki>
|----
| 739470859 || 2016-09-14T21:57:19Z || GreenC bot || <nowiki>[[User:Green Cardamom/WaybackMedic 2|WaybackMedic 2]]</nowiki>
|----
| 732601869 || 2016-08-02T01:19:57Z || Tenebrae || <nowiki>/* Partners */ tagged for almost a year</nowiki>
|----
| 732601785 || 2016-08-02T01:19:12Z || Tenebrae || <nowiki>we don't link to crowdfunding sites.</nowiki>
|----
| 732534589 || 2016-08-01T16:22:00Z || 114.30.106.86 || <nowiki>/* Controversy */</nowiki>
|----
| 732533249 || 2016-08-01T16:12:01Z || 114.30.106.86 || <nowiki>/* Controversy */</nowiki>
|----
| 724801667 || 2016-06-11T16:02:02Z || Tassedethe || <nowiki>Disambiguated: [[Chris Crosby]] → [[Chris Crosby (comics)]] (2)</nowiki>
|----
| 709430614 || 2016-03-10T21:53:52Z || Tenebrae || <nowiki>one more</nowiki>
|----
| 709429661 || 2016-03-10T21:46:19Z || Tenebrae || <nowiki>multiple issues tag</nowiki>
|----
| 709429170 || 2016-03-10T21:42:06Z || Tenebrae || <nowiki>redlinks unlikely to convert</nowiki>
|----
| 709412424 || 2016-03-10T19:43:07Z || 2601:201:4300:3D00:195F:1FC8:9FC3:E9A || <nowiki>/* Film */</nowiki>
|----
| 707035757 || 2016-02-26T16:52:47Z || Cyberbot II || <nowiki>Rescuing 1 sources. #IABot</nowiki>
|----
| 693582375 || 2015-12-03T14:28:35Z || 106.69.142.68 || <nowiki>/* Controversy */ Minor edit: Changed "were went" to "had gone" to improve grammar.</nowiki>
|----
| 683304857 || 2015-09-29T13:14:15Z || Mikeblas || <nowiki>tag press releases as primary source; remove "recent marketing" section</nowiki>
|----
| 683304465 || 2015-09-29T13:10:06Z || Mikeblas || <nowiki>cleanup cite web tag</nowiki>
|----
| 683304348 || 2015-09-29T13:08:36Z || Mikeblas || <nowiki>/* Film */ add author info</nowiki>
|----
| 683304201 || 2015-09-29T13:06:57Z || Mikeblas || <nowiki>fix broken cite web tag; mark as primary source</nowiki>
|----
| 683303987 || 2015-09-29T13:04:49Z || Mikeblas || <nowiki>primary source</nowiki>
|----
| 683303871 || 2015-09-29T13:03:36Z || Mikeblas || <nowiki>/* Partners */ dead link; mark others as cite needed</nowiki>
|----
| 683303718 || 2015-09-29T13:02:06Z || Mikeblas || <nowiki>/* Webcomics and digital comics */ remove outdated, uncited, unencyclopedic list; WP is not a catalog</nowiki>
|----
| 683303555 || 2015-09-29T13:00:30Z || Mikeblas || <nowiki>fix broken cite web tag; mark as primary source</nowiki>
|----
| 682887044 || 2015-09-26T19:24:21Z || 50.139.165.166 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 682308384 || 2015-09-22T21:51:18Z || Tenebrae || <nowiki>/* Controversy */ redlink unlikely to convert</nowiki>
|----
| 682308355 || 2015-09-22T21:51:03Z || Tenebrae || <nowiki>/* Controversy */ This section was a complete mess. Did my best to clean it up to MOS, [[WP:TONE]], and [[WP:DATED]] standards</nowiki>
|----
| 682307538 || 2015-09-22T21:45:17Z || Tenebrae || <nowiki>/* Television */ c/e</nowiki>
|----
| 682307501 || 2015-09-22T21:44:58Z || Tenebrae || <nowiki>/* Film */ c/e</nowiki>
|----
| 682307319 || 2015-09-22T21:43:34Z || Tenebrae || <nowiki>del pure-hype violations of [[WP:TONE]]</nowiki>
|----
| 682307172 || 2015-09-22T21:42:26Z || Tenebrae || <nowiki>Anyone can plan anything they want to. When it happens, then it become something in real life. [[WP:CRYSTAL]]</nowiki>
|----
| 682307036 || 2015-09-22T21:41:24Z || Tenebrae || <nowiki>[[WP:PEACOCK]]</nowiki>
|----
| 682306918 || 2015-09-22T21:40:33Z || Tenebrae || <nowiki>according to GCD, he only wrote one Marvel comic -- and only the story basis at that, with Warren Ellis writing the script</nowiki>
|----
| 682306823 || 2015-09-22T21:39:47Z || Tenebrae || <nowiki>COI tag</nowiki>
|----
| 679740716 || 2015-09-06T13:52:34Z || 24.101.239.94 || <nowiki>/* Controversy */</nowiki>
|----
| 673945944 || 2015-07-31T15:13:40Z || 58.7.55.233 || <nowiki>Correcting information on printed book delivery status for Kickstarter backers of Japan Needs Heroes project. As of 30th July 2015, they still have not been posted.</nowiki>
|----
| 671254349 || 2015-07-13T13:49:44Z || Craigc73 || <nowiki>/* Marketing */</nowiki>
|----
| 671254019 || 2015-07-13T13:46:52Z || Craigc73 || <nowiki>/* Controversy */</nowiki>
|----
| 668790338 || 2015-06-26T17:53:02Z || Craigc73 || <nowiki>/* Marketing */</nowiki>
|----
| 668761565 || 2015-06-26T13:47:25Z || Craigc73 || <nowiki>/* Marketing */</nowiki>
|----
| 668760671 || 2015-06-26T13:38:32Z || Craigc73 || <nowiki>/* Controversy */</nowiki>
|----
| 668759943 || 2015-06-26T13:31:22Z || Craigc73 || <nowiki>/* Giant-Size Comics */</nowiki>
|----
| 651681800 || 2015-03-16T20:26:48Z || Encolpe || <nowiki>/* External links */</nowiki>
|----
| 651681720 || 2015-03-16T20:26:14Z || Encolpe || <nowiki>/* External links */</nowiki>
|----
| 640613294 || 2015-01-02T05:49:20Z || Craigc73 || <nowiki>/* Controversy */ updated latest controversy</nowiki>
|----
| 639382866 || 2014-12-23T20:28:27Z || Craigc73 || <nowiki>/* Film */ added link to Journey To Magika movie</nowiki>
|----
| 639369075 || 2014-12-23T18:27:53Z || Craigc73 || <nowiki>/* Graphic Novels */ added REF tags</nowiki>
|----
| 639369031 || 2014-12-23T18:27:25Z || Craigc73 || <nowiki>/* Graphic Novels */</nowiki>
|----
| 639368905 || 2014-12-23T18:26:23Z || Craigc73 || <nowiki>/* Graphic Novels */</nowiki>
|----
| 639368828 || 2014-12-23T18:25:45Z || Craigc73 || <nowiki>/* Graphic Novels */ added additional titles from web search and isbn search</nowiki>
|----
| 639367700 || 2014-12-23T18:16:20Z || Craigc73 || <nowiki>/* Graphic Novels */</nowiki>
|----
| 639367241 || 2014-12-23T18:12:47Z || Craigc73 || <nowiki>added links to Graphic Novels pages</nowiki>
|----
| 639363286 || 2014-12-23T17:41:39Z || Craigc73 || <nowiki>/* Works */</nowiki>
|----
| 638679675 || 2014-12-18T19:43:10Z || Craigc73 || <nowiki>/* Print collections and graphic novels */</nowiki>
|----
| 638601643 || 2014-12-18T05:06:39Z || Craigc73 || <nowiki>/* Webcomics and digital comics */ removed buzzboy and roboy red comics</nowiki>
|----
| 638131111 || 2014-12-15T02:04:59Z || BD2412 || <nowiki>minor fixes, mostly [[Wikipedia:Disambiguation pages with links|disambig links]] using [[Project:AWB|AWB]]</nowiki>
|----
| 637170726 || 2014-12-08T14:35:09Z || Craigc73 || <nowiki>/* Marketing */ added Hulu release</nowiki>
|----
| 637170458 || 2014-12-08T14:32:36Z || Craigc73 || <nowiki>/* Film */ added Journey to Magika release date w/ Hulu</nowiki>
|----
| 635148223 || 2014-11-23T21:10:12Z || Mogism || <nowiki>/* Marketing */Cleanup/[[WP:AWB/T|Typo fixing]], [[WP:AWB/T|typo(s) fixed]]: Youtube → YouTube using [[Project:AWB|AWB]]</nowiki>
|----
| 634936217 || 2014-11-22T07:55:09Z || 66.74.176.59 || <nowiki>sp</nowiki>
|----
| 633544889 || 2014-11-12T16:55:13Z || Craigc73 || <nowiki></nowiki>
|----
| 633544809 || 2014-11-12T16:54:32Z || Craigc73 || <nowiki></nowiki>
|----
| 633544521 || 2014-11-12T16:51:44Z || Craigc73 || <nowiki></nowiki>
|----
| 633544324 || 2014-11-12T16:50:08Z || Craigc73 || <nowiki>Added Markiplier to the board and key people</nowiki>
|----
| 631901389 || 2014-10-31T16:07:57Z || 99.109.0.18 || <nowiki>Undid revision 628135118 by [[Special:Contributions/98.196.10.21|98.196.10.21]] ([[User talk:98.196.10.21|talk]]) - Article date is January 2, 2014. Must cite reputable source if changing the content.</nowiki>
|----
| 628135118 || 2014-10-03T21:40:21Z || 98.196.10.21 || <nowiki>/* Controversy */</nowiki>
|----
| 625673515 || 2014-09-15T15:16:08Z || Craigc73 || <nowiki>/* Print collections and graphic novels */</nowiki>
|----
| 625671658 || 2014-09-15T15:00:26Z || Craigc73 || <nowiki>/* Print collections and graphic novels */</nowiki>
|----
| 624901813 || 2014-09-10T05:25:52Z || Craigc73 || <nowiki>/* Giant-Size Comics */</nowiki>
|----
| 624901387 || 2014-09-10T05:18:34Z || Craigc73 || <nowiki>update print collections and clarity under Film section</nowiki>
|----
| 624627223 || 2014-09-08T04:42:37Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 624626775 || 2014-09-08T04:36:34Z || Craigc73 || <nowiki>/* Marketing */</nowiki>
|----
| 624625590 || 2014-09-08T04:18:59Z || Craigc73 || <nowiki></nowiki>
|----
| 624624468 || 2014-09-08T04:04:31Z || Craigc73 || <nowiki></nowiki>
|----
| 624622146 || 2014-09-08T03:32:57Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 624622095 || 2014-09-08T03:32:01Z || Craigc73 || <nowiki>/* Film projects */ added links to each of the movies that had a wiki page</nowiki>
|----
| 624620213 || 2014-09-08T03:05:54Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 624620108 || 2014-09-08T03:04:36Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 624619507 || 2014-09-08T02:56:54Z || Craigc73 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 623971211 || 2014-09-03T07:03:51Z || Yobot || <nowiki>[[WP:CHECKWIKI]] error fixes using [[Project:AWB|AWB]] (10454)</nowiki>
|----
| 623802613 || 2014-09-02T03:01:50Z || Craigc73 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 623800961 || 2014-09-02T02:44:12Z || Craigc73 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 623799530 || 2014-09-02T02:29:19Z || Craigc73 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 623796997 || 2014-09-02T02:01:42Z || Craigc73 || <nowiki>/* Giant-Size Comics */</nowiki>
|----
| 623796802 || 2014-09-02T01:59:54Z || Craigc73 || <nowiki>/* Giant-Size Comics */</nowiki>
|----
| 623191571 || 2014-08-28T15:58:30Z || 66.162.9.208 || <nowiki>/* Film projects */ released on Shockwave, Darkside</nowiki>
|----
| 623177203 || 2014-08-28T13:48:25Z || 66.162.9.208 || <nowiki>/* Properties */ - removed section</nowiki>
|----
| 622992079 || 2014-08-27T07:39:18Z || Craigc73 || <nowiki>/* Giant-Size Comics */ Monster Isle and Larry Hama add</nowiki>
|----
| 622991686 || 2014-08-27T07:33:24Z || Craigc73 || <nowiki>/* Webcomics and digital comics */</nowiki>
|----
| 619777244 || 2014-08-04T06:40:09Z || Freedmanman || <nowiki></nowiki>
|----
| 619459294 || 2014-08-01T19:13:56Z || Craigc73 || <nowiki>/* Video Games and Apps */</nowiki>
|----
| 619377850 || 2014-08-01T03:28:59Z || BattyBot || <nowiki>fixed [[Help:CS1 errors#bad_date|CS1 errors: dates]] to meet [[MOS:DATEFORMAT]] (also [[WP:AWB/GF|General fixes]]) using [[Project:AWB|AWB]] (10331)</nowiki>
|----
| 618765025 || 2014-07-28T04:17:05Z || Craigc73 || <nowiki>/* Works */ Added Video Games and Apps</nowiki>
|----
| 618764125 || 2014-07-28T04:07:50Z || Craigc73 || <nowiki>added other directors of the company</nowiki>
|----
| 618763380 || 2014-07-28T03:59:53Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 618763009 || 2014-07-28T03:55:49Z || Craigc73 || <nowiki>/* Film projects */ added Media to end of Red Giant</nowiki>
|----
| 618762183 || 2014-07-28T03:46:18Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 618762133 || 2014-07-28T03:45:37Z || Craigc73 || <nowiki>/* Film projects */ alphabetize and add films</nowiki>
|----
| 618760544 || 2014-07-28T03:25:02Z || 99.109.0.18 || <nowiki>/* Film projects */ - updated correct link to http://en.wikipedia.org/wiki/Shockwave,_Darkside</nowiki>
|----
| 618456937 || 2014-07-25T19:47:43Z || Craigc73 || <nowiki>/* Partners */</nowiki>
|----
| 618451593 || 2014-07-25T18:54:27Z || Craigc73 || <nowiki>/* Partners */</nowiki>
|----
| 617837809 || 2014-07-21T12:23:38Z || 92.41.108.122 || <nowiki>/* Controversy */ copyedit</nowiki>
|----
| 617837678 || 2014-07-21T12:22:33Z || Craigc73 || <nowiki>/* Film projects */ removed duplicate film entry</nowiki>
|----
| 617837537 || 2014-07-21T12:21:27Z || Craigc73 || <nowiki>/* History */</nowiki>
|----
| 617816281 || 2014-07-21T08:42:37Z || Yobot || <nowiki>[[WP:CHECKWIKI]] error fixes, added Empty section (1) tag using [[Project:AWB|AWB]] (10319)</nowiki>
|----
| 617748831 || 2014-07-20T20:13:37Z || Craigc73 || <nowiki>/* Marketing */</nowiki>
|----
| 617748321 || 2014-07-20T20:08:56Z || Craigc73 || <nowiki></nowiki>
|----
| 617747741 || 2014-07-20T20:03:14Z || Craigc73 || <nowiki></nowiki>
|----
| 617747039 || 2014-07-20T19:57:21Z || Craigc73 || <nowiki></nowiki>
|----
| 617744385 || 2014-07-20T19:33:24Z || Craigc73 || <nowiki>/* History */</nowiki>
|----
| 617744299 || 2014-07-20T19:32:38Z || Craigc73 || <nowiki></nowiki>
|----
| 617741597 || 2014-07-20T19:07:01Z || Craigc73 || <nowiki>/* Television */</nowiki>
|----
| 617741129 || 2014-07-20T19:02:36Z || Craigc73 || <nowiki></nowiki>
|----
| 617741037 || 2014-07-20T19:01:29Z || Craigc73 || <nowiki></nowiki>
|----
| 617738671 || 2014-07-20T18:38:45Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 617738160 || 2014-07-20T18:33:59Z || Craigc73 || <nowiki></nowiki>
|----
| 617737775 || 2014-07-20T18:30:40Z || Craigc73 || <nowiki>/* Film projects */</nowiki>
|----
| 617736922 || 2014-07-20T18:22:21Z || Craigc73 || <nowiki></nowiki>
|----
| 617736699 || 2014-07-20T18:20:11Z || NatGertler || <nowiki>proper header capitalization</nowiki>
|----
| 617724371 || 2014-07-20T16:35:18Z || Craigc73 || <nowiki>/* Film Projects */</nowiki>
|----
| 617723809 || 2014-07-20T16:29:42Z || Craigc73 || <nowiki>/* Film Projects */</nowiki>
|----
| 617723734 || 2014-07-20T16:28:46Z || Craigc73 || <nowiki>/* Film Projects */</nowiki>
|----
| 593647271 || 2014-02-02T22:31:50Z || Deville || <nowiki>Remove link to dab page [[Katrina]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]</nowiki>
|----
| 592282062 || 2014-01-25T05:16:03Z || Nikkimaria || <nowiki>rm per [[WP:ELNO]]</nowiki>
|----
| 591440129 || 2014-01-19T17:57:38Z || ImageRemovalBot || <nowiki>Removing links to deleted file [[:File:Giantsizecomicspreviewsad-jan2014.jpg]]</nowiki>
|----
| 589047311 || 2014-01-03T22:38:02Z || Sawyerkaden || <nowiki>/* Film Projects */</nowiki>
|----
| 588881500 || 2014-01-02T22:54:26Z || Vegaswikian || <nowiki>/* References */ Format</nowiki>
|----
| 588847527 || 2014-01-02T18:24:45Z || Sawyerkaden || <nowiki>/* Controversy */</nowiki>
|----
| 588784597 || 2014-01-02T08:14:57Z || RussBot || <nowiki>Robot: Change redirected category [[:Category:Comic book publishers|Comic book publishers]] to [[:Category:Comics publishing companies|Comics publishing companies]]</nowiki>
|----
| 588775176 || 2014-01-02T06:27:07Z || BG19bot || <nowiki>[[WP:CHECKWIKI]] error fix. Section heading problem. Violates [[WP:MOSHEAD]].</nowiki>
|----
| 588709304 || 2014-01-01T20:44:31Z || Sawyerkaden || <nowiki></nowiki>
|----
| 588709156 || 2014-01-01T20:43:41Z || Sawyerkaden || <nowiki></nowiki>
|----
| 588708123 || 2014-01-01T20:37:53Z || Sawyerkaden || <nowiki>/* Giant-Size Comics */</nowiki>
|----
| 588704081 || 2014-01-01T20:07:00Z || Sawyerkaden || <nowiki></nowiki>
|----
| 588703479 || 2014-01-01T20:02:36Z || Sawyerkaden || <nowiki></nowiki>
|----
| 588702458 || 2014-01-01T19:55:12Z || Sawyerkaden || <nowiki>Added Giant-Size Comics image</nowiki>
|----
| 588700631 || 2014-01-01T19:40:20Z || Sawyerkaden || <nowiki></nowiki>
|----
| 588698996 || 2014-01-01T19:27:09Z || Sawyerkaden || <nowiki>Added company logo</nowiki>
|----
| 588696099 || 2014-01-01T19:05:12Z || Sawyerkaden || <nowiki>page creation</nowiki>
|}
n98157c5bgyzuzzj0cewb5wyy994gy2
List of Fables characters (Thirteenth Floor Fables)
0
114158
738025
689587
2026-04-15T13:37:55Z
~2026-23142-66
73571
738025
wikitext
text/x-wiki
{{See also|List of Fables characters}}
{{multiple issues|
{{In-universe|date=October 2009}}
{{Update|date=September 2010|[[Cinderella: From Fabletown With Love]]}}
}}
This article is a list of [[fictional character]]s in the [[Vertigo (comics)|Vertigo]] [[comic book]] series ''[[Fables (comics)|Fables]]'', ''[[Jack of Fables]]'', ''[[Cinderella: From Fabletown with Love]]'', ''Cinderella: Fables Are Forever'' and ''[[Fairest (comics)|Fairest]]'', published by [[DC Comics]].
These are the spellcaster Fables who live on the 13th floor of [[The Woodlands, Texas|the Woodlands]] building.
this is an edit
==Frau Totenkinder==
{{Main|Frau Totenkinder}}
==Ozma==
[[Princess Ozma|Ozma]], Princess of Oz, a little girl witch who is often seen with Frau Totenkinder. While the Fairy Witch's great task was the creation of Fabletown and the Woodland building and Totenkinder's great task was the battle against the Empire, Ozma sees her task as the battle against Mr. Dark for the sake of Fabletown. Was voted the new leader of the 13th floor after Frau Totenkinder went to find Dunster Happ. She participated in a power struggle in The Farm along with Stinky and Gepetto, and in the end was named one of Rose Red's three advisers. With Totenkinder gone to live with her new husband, Ozma is the leader of the thirteenth floor witches.
==Great Fairy Witch==
The former leader of the 13th floor who has gone a little crazy. She is the witch from ''[[Thumbelina]]'', as seen in flashbacks in the Fables story ''The Barleycorn Brides''. It is hinted that Frau Totenkinder was the one who caused her to go mad after she took over the Fairy Witch's role - besides, when Totenkinder gave her knitting needles and her bag of wool balls their true forms and names back and stopped knitting, the Great Fairy Witch continued to knit with no needles and no wool. She has gone by other names including Ardelia, Cherish, Birdie and Bulah.
==Maddy==
A black [[cat]] with a demon-like, pointed tail who has appeared in a few panels with Frau Totenkinder and other witches. She is the acknowledged specialist in matters of locating and hiding and is a shape-shifter. Her powers at hiding are so great that Mr. Dark couldn't detect her, when she went to spy on him, although she had trouble keeping away from his detection, calling Mr Dark the most paranoid and dangerous of any subject she had spied on before. Ozma used her to find Frau Totenkinder before she went into seclusion. The character has been revealed to represent both [[Sycorax]] from ''[[The Tempest]]'' and [[Medea]] from [[Greek mythology]]. Maddy seems to be second in command to Ozma, the same way that Ozma herself was always second to Totenkinder.
==Mr. Grandours==
Mr. Grandours (in French, literally "large [[bear]]") is the title character from the [[France|French]] fairy tale ''[[The Wizard King]]'' (from [[Andrew Lang]]'s ''[[The Yellow Fairy Book]]''), about a [[shapeshift]]ing wizard king; this is revealed in the ''Fables Encyclopedia''.<ref name="Fables Encyclopedia">Nevins, Jess, [[Bill Willingham|Willingham, Bill]], [[Mark Buckingham (comic book artist)|Buckingham, Mark]] (2013). ''Fables Encyclopedia''. New York. [[DC Comics]]. {{ISBN|978-1-4012-4395-1}}</ref>
The local Imperial Governor instructed Mr. Grandours to guard a tower, filled with various treasures, including the magic barleycorns. He helped [[John Barleycorn]] and Arrow retrieve the jar and joined [[Fabletown]]. He eventually returned to his human form and lives on the 13th floor. He's the man in a big fur coat and hat with bear eyes. He was the one that originally revived [[Boy Blue (Fables)|Boy Blue]] and [[List of Fables characters#Bigby Wolf|Bigby Wolf]] when they were almost killed by a magic arrow.
==Mrs. Someone==
A witch who stayed with [[List of Fables characters#Briar Rose|Briar Rose]] and Hakim during the takeover of Calabri Anagni, the Imperial capital of the Fable [[Homelands (Fables)|Homelands]]. Her true name is still unknown as she keeps it a secret "tucked away where no fell power can discern it." Mrs. Someone and Hakim chose to stay with Briar Rose until the end and fell under Briar's sleeping spell. Recently, the sleeping Briar Rose was carried out of the city by [[goblins]], before the city was burned to the ground, and it is assumed that Mrs. Someone and Hakim were killed in the fire.
==Prospero==
Protagonist of [[William Shakespeare]]'s play ''[[The Tempest]]''. [[Prospero]] is seen agreeing with Mr. Grandours that haste is a "mundy quality". It is unknown how the relation between Prospero and Maddy is, as he used her son as a slave for years. However, this would fall under the Fabletown Compact which pardons any crimes done before signing.
==Mr. Kadabra==
Depicted as a stereotypical Stage Magician wearing a top hat. In earlier appearances, he is seen wearing a button with "yog" on it which refers to meditation and concentration. His name derives from the magic phrase [[abracadabra]]. He has recently been killed by an unknown assailant in the first issue of ''Cinderella: Fables Are Forever''. In the ''Fables'' story ''In Those Days'', it is revealed that Mr. Kadabra's real name is Karrant. Karrant was a powerful sorcerer who cast a spell on [[Geppetto (Fables)|the Adversary]] that made the Adversary ignore any land in which Kadabra dwelled. The cost of Karrant's actions was that he forgot all about the spell and his enemy. He wandered from world to world until he ended up in [[Fabletown]] in the mundane world, where he made a living as a stage magician, adopting the [[stage name]] "Master Kadabra". He was eventually killed by [[List of Fables characters#Dorothy Gale|Dorothy Gale]], who needed a dead body to send a message to her archenemy, [[List of Fables characters#Cinderella|Cinderella]].
==Geppetto==
{{Main|Geppetto (Fables)}}
==Morgan le Fay==
[[Morgan le Fay]], from the saga of [[King Arthur]], is the witch with the long dark hair and beauty mark on her face. Her identity was first revealed in the ''[[Fairest (comics)|Fairest]]'' graphic novel ''Fairest In All the Land'', and later in ''Fables'' 136 (''Camelot'', Part 6). She is also known as Mrs. Green, as revealed in ''Fables'' 128 (''Snow White'', Chapter Four). Morgan was part of [[List of Fables characters#Ozma|Ozma]]'s super team put together to defeat [[List of Fables characters#Mister Dark|Mister Dark]], where she was given the "superhero name" The Green Witch. She has the power to fly.
In the ''Snow White'' story arc, after [[Snow White (Fables)|the story arc's title character]] is taken captive by [[List of Fables characters#Prince Brandish/Werian Holt|Prince Brandish]], Morgan is seen discussing rescue plans with her fellow witches, Ozma and [[List of Fables characters#Maddy|Maddy]]. Because Brandish has bewitched himself so that any injury inflicted on him will also hurt Snow, the task is easier said than done, especially since the spell is difficult to break. Morgan comes up with a plan. They will not try to break the spell, but add to it instead: She will build in a delay between cause and effect, which will give them some wiggle room between the moment Brandish is killed, and when the same thing happens to Snow. Morgan begins the hard work of casting the spell, while [[List of Fables characters#Rose Red|Rose Red]] rescues Snow while Brandish is distracted. Snow then engages Brandish in a sword fight, while Morgan slowly, but carefully manages to unwind the spell that Brandish has cast on himself, allowing Snow stab him straight through his heart.
Unfortunately, the spell is not broken, merely delayed. In the ''Camelot'' story arc, Morgan breaks the bad news to Snow: Sooner or later, the fatal wound is bound to be inflicted on Snow herself, which will kill her. In ''Fairest In All the Land'', Morgan, along with [[Bean Nighe]], a Fabled oracle, were victims at the hands of [[List of Fables characters#Goldilocks|Goldilocks]]. While [[List of Fables characters#Cinderella|Cinderella]] is able to bring back the victims of Goldilocks; she's only allowed to choose half. She ultimately chooses Morgan Le Fay, seeing her more valuable an ally for [[Fabletown]]. Snow White's fate is resolved in the same story; she is stabbed through the heart (as Morgan predicted) by Goldilocks. Fortunately, Cinderella is able to bring her back to life.
==References==
{{reflist|colwidth=30em}}
{{Fables}}
{{DISPLAYTITLE:List of ''Fables'' characters (Thirteenth Floor Fables)}}
{{DEFAULTSORT:Fables characters}}
[[Category:Lists of DC Comics characters|Fables characters, List of]]
[[Category:Fables (comics)|Characters]]
[[Category:Characters created by Bill Willingham]]
<noinclude>
<small>This page was moved from [[:en:List of Fables characters (Thirteenth Floor Fables)]]. Its edit history can be viewed at [[List of Fables characters (Thirteenth Floor Fables)/edithistory]]</small></noinclude>
jy94zy4y39awjhlxylv71cx8dtu4axy
738026
738025
2026-04-15T13:38:08Z
~2026-23142-66
73571
738026
wikitext
text/x-wiki
{{See also|List of Fables characters}}
{{multiple issues|
{{In-universe|date=October 2009}}
{{Update|date=September 2010|[[Cinderella: From Fabletown With Love]]}}
}}
This article is a list of [[fictional character]]s in the [[Vertigo (comics)|Vertigo]] [[comic book]] series ''[[Fables (comics)|Fables]]'', ''[[Jack of Fables]]'', ''[[Cinderella: From Fabletown with Love]]'', ''Cinderella: Fables Are Forever'' and ''[[Fairest (comics)|Fairest]]'', published by [[DC Comics]].
These are the spellcaster Fables who live on the 13th floor of [[The Woodlands, Texas|the Woodlands]] building.
==Frau Totenkinder==
{{Main|Frau Totenkinder}}
==Ozma==
[[Princess Ozma|Ozma]], Princess of Oz, a little girl witch who is often seen with Frau Totenkinder. While the Fairy Witch's great task was the creation of Fabletown and the Woodland building and Totenkinder's great task was the battle against the Empire, Ozma sees her task as the battle against Mr. Dark for the sake of Fabletown. Was voted the new leader of the 13th floor after Frau Totenkinder went to find Dunster Happ. She participated in a power struggle in The Farm along with Stinky and Gepetto, and in the end was named one of Rose Red's three advisers. With Totenkinder gone to live with her new husband, Ozma is the leader of the thirteenth floor witches.
==Great Fairy Witch==
The former leader of the 13th floor who has gone a little crazy. She is the witch from ''[[Thumbelina]]'', as seen in flashbacks in the Fables story ''The Barleycorn Brides''. It is hinted that Frau Totenkinder was the one who caused her to go mad after she took over the Fairy Witch's role - besides, when Totenkinder gave her knitting needles and her bag of wool balls their true forms and names back and stopped knitting, the Great Fairy Witch continued to knit with no needles and no wool. She has gone by other names including Ardelia, Cherish, Birdie and Bulah.
==Maddy==
A black [[cat]] with a demon-like, pointed tail who has appeared in a few panels with Frau Totenkinder and other witches. She is the acknowledged specialist in matters of locating and hiding and is a shape-shifter. Her powers at hiding are so great that Mr. Dark couldn't detect her, when she went to spy on him, although she had trouble keeping away from his detection, calling Mr Dark the most paranoid and dangerous of any subject she had spied on before. Ozma used her to find Frau Totenkinder before she went into seclusion. The character has been revealed to represent both [[Sycorax]] from ''[[The Tempest]]'' and [[Medea]] from [[Greek mythology]]. Maddy seems to be second in command to Ozma, the same way that Ozma herself was always second to Totenkinder.
==Mr. Grandours==
Mr. Grandours (in French, literally "large [[bear]]") is the title character from the [[France|French]] fairy tale ''[[The Wizard King]]'' (from [[Andrew Lang]]'s ''[[The Yellow Fairy Book]]''), about a [[shapeshift]]ing wizard king; this is revealed in the ''Fables Encyclopedia''.<ref name="Fables Encyclopedia">Nevins, Jess, [[Bill Willingham|Willingham, Bill]], [[Mark Buckingham (comic book artist)|Buckingham, Mark]] (2013). ''Fables Encyclopedia''. New York. [[DC Comics]]. {{ISBN|978-1-4012-4395-1}}</ref>
The local Imperial Governor instructed Mr. Grandours to guard a tower, filled with various treasures, including the magic barleycorns. He helped [[John Barleycorn]] and Arrow retrieve the jar and joined [[Fabletown]]. He eventually returned to his human form and lives on the 13th floor. He's the man in a big fur coat and hat with bear eyes. He was the one that originally revived [[Boy Blue (Fables)|Boy Blue]] and [[List of Fables characters#Bigby Wolf|Bigby Wolf]] when they were almost killed by a magic arrow.
==Mrs. Someone==
A witch who stayed with [[List of Fables characters#Briar Rose|Briar Rose]] and Hakim during the takeover of Calabri Anagni, the Imperial capital of the Fable [[Homelands (Fables)|Homelands]]. Her true name is still unknown as she keeps it a secret "tucked away where no fell power can discern it." Mrs. Someone and Hakim chose to stay with Briar Rose until the end and fell under Briar's sleeping spell. Recently, the sleeping Briar Rose was carried out of the city by [[goblins]], before the city was burned to the ground, and it is assumed that Mrs. Someone and Hakim were killed in the fire.
==Prospero==
Protagonist of [[William Shakespeare]]'s play ''[[The Tempest]]''. [[Prospero]] is seen agreeing with Mr. Grandours that haste is a "mundy quality". It is unknown how the relation between Prospero and Maddy is, as he used her son as a slave for years. However, this would fall under the Fabletown Compact which pardons any crimes done before signing.
==Mr. Kadabra==
Depicted as a stereotypical Stage Magician wearing a top hat. In earlier appearances, he is seen wearing a button with "yog" on it which refers to meditation and concentration. His name derives from the magic phrase [[abracadabra]]. He has recently been killed by an unknown assailant in the first issue of ''Cinderella: Fables Are Forever''. In the ''Fables'' story ''In Those Days'', it is revealed that Mr. Kadabra's real name is Karrant. Karrant was a powerful sorcerer who cast a spell on [[Geppetto (Fables)|the Adversary]] that made the Adversary ignore any land in which Kadabra dwelled. The cost of Karrant's actions was that he forgot all about the spell and his enemy. He wandered from world to world until he ended up in [[Fabletown]] in the mundane world, where he made a living as a stage magician, adopting the [[stage name]] "Master Kadabra". He was eventually killed by [[List of Fables characters#Dorothy Gale|Dorothy Gale]], who needed a dead body to send a message to her archenemy, [[List of Fables characters#Cinderella|Cinderella]].
==Geppetto==
{{Main|Geppetto (Fables)}}
==Morgan le Fay==
[[Morgan le Fay]], from the saga of [[King Arthur]], is the witch with the long dark hair and beauty mark on her face. Her identity was first revealed in the ''[[Fairest (comics)|Fairest]]'' graphic novel ''Fairest In All the Land'', and later in ''Fables'' 136 (''Camelot'', Part 6). She is also known as Mrs. Green, as revealed in ''Fables'' 128 (''Snow White'', Chapter Four). Morgan was part of [[List of Fables characters#Ozma|Ozma]]'s super team put together to defeat [[List of Fables characters#Mister Dark|Mister Dark]], where she was given the "superhero name" The Green Witch. She has the power to fly.
In the ''Snow White'' story arc, after [[Snow White (Fables)|the story arc's title character]] is taken captive by [[List of Fables characters#Prince Brandish/Werian Holt|Prince Brandish]], Morgan is seen discussing rescue plans with her fellow witches, Ozma and [[List of Fables characters#Maddy|Maddy]]. Because Brandish has bewitched himself so that any injury inflicted on him will also hurt Snow, the task is easier said than done, especially since the spell is difficult to break. Morgan comes up with a plan. They will not try to break the spell, but add to it instead: She will build in a delay between cause and effect, which will give them some wiggle room between the moment Brandish is killed, and when the same thing happens to Snow. Morgan begins the hard work of casting the spell, while [[List of Fables characters#Rose Red|Rose Red]] rescues Snow while Brandish is distracted. Snow then engages Brandish in a sword fight, while Morgan slowly, but carefully manages to unwind the spell that Brandish has cast on himself, allowing Snow stab him straight through his heart.
Unfortunately, the spell is not broken, merely delayed. In the ''Camelot'' story arc, Morgan breaks the bad news to Snow: Sooner or later, the fatal wound is bound to be inflicted on Snow herself, which will kill her. In ''Fairest In All the Land'', Morgan, along with [[Bean Nighe]], a Fabled oracle, were victims at the hands of [[List of Fables characters#Goldilocks|Goldilocks]]. While [[List of Fables characters#Cinderella|Cinderella]] is able to bring back the victims of Goldilocks; she's only allowed to choose half. She ultimately chooses Morgan Le Fay, seeing her more valuable an ally for [[Fabletown]]. Snow White's fate is resolved in the same story; she is stabbed through the heart (as Morgan predicted) by Goldilocks. Fortunately, Cinderella is able to bring her back to life.
==References==
{{reflist|colwidth=30em}}
{{Fables}}
{{DISPLAYTITLE:List of ''Fables'' characters (Thirteenth Floor Fables)}}
{{DEFAULTSORT:Fables characters}}
[[Category:Lists of DC Comics characters|Fables characters, List of]]
[[Category:Fables (comics)|Characters]]
[[Category:Characters created by Bill Willingham]]
<noinclude>
<small>This page was moved from [[:en:List of Fables characters (Thirteenth Floor Fables)]]. Its edit history can be viewed at [[List of Fables characters (Thirteenth Floor Fables)/edithistory]]</small></noinclude>
fh34rfirxr9i9h2ytwubxa4hqt14u59
738027
738026
2026-04-15T13:38:25Z
~2026-23142-66
73571
738027
wikitext
text/x-wiki
{{See also|List of Fables characters}}
{{multiple issues|
{{In-universe|date=October 2009}}
{{Update|date=September 2010|[[Cinderella: From Fabletown With Love]]}}
}}
This article is a list of [[fictional character]]s in the [[Vertigo (comics)|Vertigo]] [[comic book]] series ''[[Fables (comics)|Fables]]'', ''[[Jack of Fables]]'', ''[[Cinderella: From Fabletown with Love]]'', ''Cinderella: Fables Are Forever'' and ''[[Fairest (comics)|Fairest]]'', published by [[DC Comics]].
These are the spellcaster Fables who live on the 13th floor of [[The Woodlands, Texas|the Woodlands]] building.
this is an edit
==Frau Totenkinder==
{{Main|Frau Totenkinder}}
==Ozma==
[[Princess Ozma|Ozma]], Princess of Oz, a little girl witch who is often seen with Frau Totenkinder. While the Fairy Witch's great task was the creation of Fabletown and the Woodland building and Totenkinder's great task was the battle against the Empire, Ozma sees her task as the battle against Mr. Dark for the sake of Fabletown. Was voted the new leader of the 13th floor after Frau Totenkinder went to find Dunster Happ. She participated in a power struggle in The Farm along with Stinky and Gepetto, and in the end was named one of Rose Red's three advisers. With Totenkinder gone to live with her new husband, Ozma is the leader of the thirteenth floor witches.
==Great Fairy Witch==
The former leader of the 13th floor who has gone a little crazy. She is the witch from ''[[Thumbelina]]'', as seen in flashbacks in the Fables story ''The Barleycorn Brides''. It is hinted that Frau Totenkinder was the one who caused her to go mad after she took over the Fairy Witch's role - besides, when Totenkinder gave her knitting needles and her bag of wool balls their true forms and names back and stopped knitting, the Great Fairy Witch continued to knit with no needles and no wool. She has gone by other names including Ardelia, Cherish, Birdie and Bulah.
==Maddy==
A black [[cat]] with a demon-like, pointed tail who has appeared in a few panels with Frau Totenkinder and other witches. She is the acknowledged specialist in matters of locating and hiding and is a shape-shifter. Her powers at hiding are so great that Mr. Dark couldn't detect her, when she went to spy on him, although she had trouble keeping away from his detection, calling Mr Dark the most paranoid and dangerous of any subject she had spied on before. Ozma used her to find Frau Totenkinder before she went into seclusion. The character has been revealed to represent both [[Sycorax]] from ''[[The Tempest]]'' and [[Medea]] from [[Greek mythology]]. Maddy seems to be second in command to Ozma, the same way that Ozma herself was always second to Totenkinder.
==Mr. Grandours==
Mr. Grandours (in French, literally "large [[bear]]") is the title character from the [[France|French]] fairy tale ''[[The Wizard King]]'' (from [[Andrew Lang]]'s ''[[The Yellow Fairy Book]]''), about a [[shapeshift]]ing wizard king; this is revealed in the ''Fables Encyclopedia''.<ref name="Fables Encyclopedia">Nevins, Jess, [[Bill Willingham|Willingham, Bill]], [[Mark Buckingham (comic book artist)|Buckingham, Mark]] (2013). ''Fables Encyclopedia''. New York. [[DC Comics]]. {{ISBN|978-1-4012-4395-1}}</ref>
The local Imperial Governor instructed Mr. Grandours to guard a tower, filled with various treasures, including the magic barleycorns. He helped [[John Barleycorn]] and Arrow retrieve the jar and joined [[Fabletown]]. He eventually returned to his human form and lives on the 13th floor. He's the man in a big fur coat and hat with bear eyes. He was the one that originally revived [[Boy Blue (Fables)|Boy Blue]] and [[List of Fables characters#Bigby Wolf|Bigby Wolf]] when they were almost killed by a magic arrow.
==Mrs. Someone==
A witch who stayed with [[List of Fables characters#Briar Rose|Briar Rose]] and Hakim during the takeover of Calabri Anagni, the Imperial capital of the Fable [[Homelands (Fables)|Homelands]]. Her true name is still unknown as she keeps it a secret "tucked away where no fell power can discern it." Mrs. Someone and Hakim chose to stay with Briar Rose until the end and fell under Briar's sleeping spell. Recently, the sleeping Briar Rose was carried out of the city by [[goblins]], before the city was burned to the ground, and it is assumed that Mrs. Someone and Hakim were killed in the fire.
==Prospero==
Protagonist of [[William Shakespeare]]'s play ''[[The Tempest]]''. [[Prospero]] is seen agreeing with Mr. Grandours that haste is a "mundy quality". It is unknown how the relation between Prospero and Maddy is, as he used her son as a slave for years. However, this would fall under the Fabletown Compact which pardons any crimes done before signing.
==Mr. Kadabra==
Depicted as a stereotypical Stage Magician wearing a top hat. In earlier appearances, he is seen wearing a button with "yog" on it which refers to meditation and concentration. His name derives from the magic phrase [[abracadabra]]. He has recently been killed by an unknown assailant in the first issue of ''Cinderella: Fables Are Forever''. In the ''Fables'' story ''In Those Days'', it is revealed that Mr. Kadabra's real name is Karrant. Karrant was a powerful sorcerer who cast a spell on [[Geppetto (Fables)|the Adversary]] that made the Adversary ignore any land in which Kadabra dwelled. The cost of Karrant's actions was that he forgot all about the spell and his enemy. He wandered from world to world until he ended up in [[Fabletown]] in the mundane world, where he made a living as a stage magician, adopting the [[stage name]] "Master Kadabra". He was eventually killed by [[List of Fables characters#Dorothy Gale|Dorothy Gale]], who needed a dead body to send a message to her archenemy, [[List of Fables characters#Cinderella|Cinderella]].
==Geppetto==
{{Main|Geppetto (Fables)}}
==Morgan le Fay==
[[Morgan le Fay]], from the saga of [[King Arthur]], is the witch with the long dark hair and beauty mark on her face. Her identity was first revealed in the ''[[Fairest (comics)|Fairest]]'' graphic novel ''Fairest In All the Land'', and later in ''Fables'' 136 (''Camelot'', Part 6). She is also known as Mrs. Green, as revealed in ''Fables'' 128 (''Snow White'', Chapter Four). Morgan was part of [[List of Fables characters#Ozma|Ozma]]'s super team put together to defeat [[List of Fables characters#Mister Dark|Mister Dark]], where she was given the "superhero name" The Green Witch. She has the power to fly.
In the ''Snow White'' story arc, after [[Snow White (Fables)|the story arc's title character]] is taken captive by [[List of Fables characters#Prince Brandish/Werian Holt|Prince Brandish]], Morgan is seen discussing rescue plans with her fellow witches, Ozma and [[List of Fables characters#Maddy|Maddy]]. Because Brandish has bewitched himself so that any injury inflicted on him will also hurt Snow, the task is easier said than done, especially since the spell is difficult to break. Morgan comes up with a plan. They will not try to break the spell, but add to it instead: She will build in a delay between cause and effect, which will give them some wiggle room between the moment Brandish is killed, and when the same thing happens to Snow. Morgan begins the hard work of casting the spell, while [[List of Fables characters#Rose Red|Rose Red]] rescues Snow while Brandish is distracted. Snow then engages Brandish in a sword fight, while Morgan slowly, but carefully manages to unwind the spell that Brandish has cast on himself, allowing Snow stab him straight through his heart.
Unfortunately, the spell is not broken, merely delayed. In the ''Camelot'' story arc, Morgan breaks the bad news to Snow: Sooner or later, the fatal wound is bound to be inflicted on Snow herself, which will kill her. In ''Fairest In All the Land'', Morgan, along with [[Bean Nighe]], a Fabled oracle, were victims at the hands of [[List of Fables characters#Goldilocks|Goldilocks]]. While [[List of Fables characters#Cinderella|Cinderella]] is able to bring back the victims of Goldilocks; she's only allowed to choose half. She ultimately chooses Morgan Le Fay, seeing her more valuable an ally for [[Fabletown]]. Snow White's fate is resolved in the same story; she is stabbed through the heart (as Morgan predicted) by Goldilocks. Fortunately, Cinderella is able to bring her back to life.
==References==
{{reflist|colwidth=30em}}
{{Fables}}
{{DISPLAYTITLE:List of ''Fables'' characters (Thirteenth Floor Fables)}}
{{DEFAULTSORT:Fables characters}}
[[Category:Lists of DC Comics characters|Fables characters, List of]]
[[Category:Fables (comics)|Characters]]
[[Category:Characters created by Bill Willingham]]
<noinclude>
<small>This page was moved from [[:en:List of Fables characters (Thirteenth Floor Fables)]]. Its edit history can be viewed at [[List of Fables characters (Thirteenth Floor Fables)/edithistory]]</small></noinclude>
jy94zy4y39awjhlxylv71cx8dtu4axy
Help:My sandbox
12
118518
738045
649895
2026-04-15T14:23:51Z
~2026-23347-42
73585
738045
wikitext
text/x-wiki
Idk figure it out yourself
this is an edit
cuoy5ahybcuab6gu2axcz00dgp6s8ar
User:Nardog/sandbox2.js
2
118608
738089
735820
2026-04-16T06:12:17Z
Nardog
40946
738089
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let hasSection;
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
hasSection = hasSection || !!match;
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (section < 1 && lowest === 7 && !hasSection) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.*?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
// if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $preview = $('.mw-summary-preview > .comment > span[dir="auto"]');
if (!$preview.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $preview.children('.autocomment:first-child');
if ($ac.length && !$ac[0].previousSibling) {
if (hasHead) {
$ac.children('a').attr('href', url).text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).text(text),
mw.messages.get('colon-separator', ': ')
).prependTo($preview);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
d0xpc4re9ds0denkxuwelx6pa4uub2w
738090
738089
2026-04-16T06:14:41Z
Nardog
40946
738090
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.*?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
// if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $preview = $('.mw-summary-preview > .comment > span[dir="auto"]');
if (!$preview.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $preview.children('.autocomment:first-child');
if ($ac.length && !$ac[0].previousSibling) {
if (hasHead) {
$ac.children('a').attr('href', url).text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).text(text),
mw.messages.get('colon-separator', ': ')
).prependTo($preview);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
kkhmb2d44a5sqf41yxgm1qnty1yp4bq
738091
738090
2026-04-16T06:18:14Z
Nardog
40946
738091
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.*?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $preview = $('.mw-summary-preview > .comment');
if (!$preview.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $preview.children('.autocomment:first-child');
if ($ac.length && !$ac[0].previousSibling) {
if (hasHead) {
$ac.children('a').attr('href', url).text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).text(text),
mw.messages.get('colon-separator', ': ')
).prependTo($preview);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
l4qe9ih47s8s8spzz66tqey3clegd0z
738092
738091
2026-04-16T06:26:54Z
Nardog
40946
738092
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.*?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $preview = $('.mw-summary-preview > .comment');
if (!$preview.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $preview.children('.autocomment:first-child');
if ($ac.length) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
// $('<span>').addClass('autocomment').append(
// $('<a>').attr({
// href: url,
// title: mw.config.get('wgPageName').replace(/_/g, ' ')
// }).append($('<bdi>').text(text)),
// mw.messages.get('colon-separator', ': ')
// ).prependTo($preview);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
5d4yg9z2qj9hzatc4q8qo2gxhpqa0op
738093
738092
2026-04-16T06:46:01Z
Nardog
40946
738093
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.*?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment[0].normalize();
let paren = [...mw.messages.get('parentheses', '($1)')][0];
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).append($('<bdi>').text(text)),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
k25fomam07itg4v0085nj847g9dbtyq
738094
738093
2026-04-16T06:50:34Z
Nardog
40946
738094
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (document.dir === 'rtl' ? '←\u200F' : '→\u200E') + (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment[0].normalize();
let paren = [...mw.messages.get('parentheses', '($1)')][0];
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).append($('<bdi>').text(text)),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
pyrqx4vxhr64s9ginskzmz0lws53z64
738095
738094
2026-04-16T06:55:48Z
Nardog
40946
738095
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replace(/ /g, '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment[0].normalize();
let paren = [...mw.messages.get('parentheses', '($1)')][0];
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replace(/_/g, ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
7zisxdpcnnooy680im2vea7sn9xo7m7
738096
738095
2026-04-16T06:59:48Z
Nardog
40946
738096
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
let paren = [...mw.messages.get('parentheses', '($1)')][0];
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
adv9dwk5aoywf50eyn3fhtta74x36ph
738097
738096
2026-04-16T07:03:42Z
Nardog
40946
738097
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
q41uv5vhvhf3achyn5fbml802wz4ily
738098
738097
2026-04-16T07:04:15Z
Nardog
40946
738098
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join('');
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
7d8tpcakazg7usc3hjpv7twumz0yzpn
738099
738098
2026-04-16T07:10:18Z
Nardog
40946
738099
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
let tc = this.textContent;
if (tc.startsWith(paren)) {
this.textContent = tc.slice(paren.length);
}
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
qf8l39agoftnafy2er6pb5lst6fjef5
738100
738099
2026-04-16T07:12:36Z
Nardog
40946
738100
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().text((_, tc) => (
tc.startsWith(paren) ? tc.slice(paren.length) : tc
));
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
mrgfh6yf27rhjdms84poopdhexaczop
738101
738100
2026-04-16T07:13:20Z
Nardog
40946
738101
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
let tc = this.textContent;
if (tc.startsWith(paren)) {
this.textContent = tc.slice(paren.length);
}
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
qf8l39agoftnafy2er6pb5lst6fjef5
738102
738101
2026-04-16T07:14:13Z
Nardog
40946
738102
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
document.createTextNode(paren),
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
7y71b9xv3ased17wav2blb6aorhz4du
738103
738102
2026-04-16T07:17:24Z
Nardog
40946
738103
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $ac = $comment.children('.autocomment:first-child');
if ($ac.length && $ac[0].previousSibling?.textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$comment.contents().first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
81mulin76ttmz00ncq5zgx2h8x9620x
738104
738103
2026-04-16T07:29:23Z
Nardog
40946
738104
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $contents = $comment.contents();
let $ac = $contents.eq(1).filter('.autocomment:first-child');
if ($ac.length && $contents[0].textContent === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $ac[0].nextSibling;
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$contents.first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
sqdxa49rlbwnoc7xfuz79bngmwcyvhr
738107
738104
2026-04-16T07:42:46Z
Nardog
40946
738107
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $contents = $tools.parent().contents();
$contents.slice(
$contents.index($tools) + 1,
$contents.index($contents.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $contents = $comment.contents();
let $ac = $contents.eq(1).filter('.autocomment:first-child');
if ($ac.length && $contents.eq(0).text() === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
let node = $contents[2];
if (node?.nodeType === 3) {
node.textContent = node.textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$contents.first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
9c81rhepkf0w7p0or3larjmk6syj0em
738108
738107
2026-04-16T07:45:42Z
Nardog
40946
738108
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $nodes = $tools.parent().contents();
$nodes.slice(
$nodes.index($tools) + 1,
$nodes.index($nodes.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $nodes = $comment.contents();
let $ac = $nodes.eq(1);
if ($ac.is('.autocomment:first-child') && $nodes.eq(0).text() === paren) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
if ($nodes[2]?.nodeType === 3) {
$nodes[2].textContent = $nodes[2].textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$nodes.first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
54admfp3quwq1ui05byw9sjti217lti
738109
738108
2026-04-16T07:46:10Z
Nardog
40946
738109
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $nodes = $tools.parent().contents();
$nodes.slice(
$nodes.index($tools) + 1,
$nodes.index($nodes.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum;
if (isOld) {
let i, lastLineNum;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
lines.length = lastLineNum || 0;
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
for (let i = 1, x = lines.length, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lines.pop();
}
}
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/, lowest = 7;
lines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
lines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (lines.length && section < 1 && lowest === 7) {
head = '';
}
let match = input.getValue().match(/^(?:\/\*\s*(.+?)\s*\*\/)?\s*(.*?)$/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((typeof head === 'string' ? '/* ' + head + ' */ ' : '') + match[2]);
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let rtl = document.body.classList.contains('sitedir-rtl');
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let hasHead = typeof head === 'string';
let url = hasHead && mw.util.getUrl() + '#' + head.replaceAll(' ', '_');
let text = hasHead && (head || mw.messages.get('autocomment-top', '(top)'));
let $nodes = $comment.contents();
let $ac = $nodes.eq(1);
if ($nodes.eq(0).text() === paren && $ac.is('.autocomment:first-child')) {
if (hasHead) {
$ac.children('a').attr('href', url).children('bdi').text(text);
} else {
if ($nodes[2]?.nodeType === 3) {
$nodes[2].textContent = $nodes[2].textContent.trimStart();
}
$ac.remove();
}
} else if (hasHead) {
$nodes.first().each(function () {
this.textContent = this.textContent.split(paren).slice(1).join(paren);
});
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(text)
),
mw.messages.get('colon-separator', ': ')
)
);
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.switch').add((on, $codeMirror) => {
if (on && $codeMirror[0].CodeMirror) {
$codeMirror[0].CodeMirror.on('change', update);
}
});
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
$.when($.ready, mw.loader.using([
'user.options', 'mediawiki.util', 'mediawiki.api'
])).then(function rcMuter() {
let os = mw.user.options.get('userjs-rcmuter');
let set = new Set(os && os.split('|'));
let save = () => {
let ns = [...set].join('|');
if (ns === mw.user.options.get('userjs-rcmuter')) return;
new mw.Api().saveOption('userjs-rcmuter', ns);
mw.user.options.set('userjs-rcmuter', ns);
$edit.attr('data-rcmuter', set.size);
};
mw.loader.addStyleTag('body:not(.rcmuter-disabled) .rcmuter-muted{display:none !important} .rcmuter-edit::after, .rcmuter-togglemuted::after{content:": " attr(data-rcmuter)}');
let $edit = $('<a>').attr({
class: 'rcmuter-edit',
href: '#',
'data-rcmuter': set.size
}).text('Edit muted').on('click', e => {
e.preventDefault();
mw.loader.using([
'oojs-ui-windows', 'mediawiki.widgets.UsersMultiselectWidget'
]).then(() => OO.ui.getWindowManager().getWindow('message')).then(dialog => {
let multiselect = new mw.widgets.UsersMultiselectWidget({
$overlay: dialog.$overlay,
ipAllowed: true,
selected: [...set]
}).connect(dialog, { change: 'updateSize', reorder: 'updateSize' });
let instance = dialog.open({
message: $([
document.createTextNode('Muted users:'),
multiselect.$element[0]
]),
size: 'medium'
});
instance.opened.then(() => {
setTimeout(() => {
multiselect.focus().menu.toggle(false);
});
});
instance.closed.then(result => {
if (!result || result.action !== 'accept') return;
set = new Set(multiselect.getSelectedUsernames());
save();
mw.notify('Changes will take effect in next load.', {
tag: 'rcmuter'
});
});
});
});
let buttonsShown;
let $toggleButtons = $('<a>').attr('href', '#').text('Show toggle buttons').on('click', function (e) {
e.preventDefault();
if (buttonsShown) {
mw.hook('wikipage.content').remove(addButtons);
$('.rcmuter-toggle').remove();
this.textContent = 'Show toggle buttons';
} else {
mw.hook('wikipage.content').add(addButtons);
this.textContent = 'Hide toggle buttons';
}
buttonsShown = !buttonsShown;
});
let $toggle = $('<a>').attr({
class: 'rcmuter-togglemuted',
href: '#'
}).text('Show muted').on('click', function (e) {
e.preventDefault();
this.textContent = document.body.classList.toggle('rcmuter-disabled')
? 'Hide muted'
: 'Show muted';
});
let $toggleSpan = $('<span>').hide().append($toggle);
mw.util.addSubtitle(
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append($edit),
$('<span>').append($toggleButtons),
$toggleSpan
)[0]
);
let toggle = function (e) {
e.preventDefault();
let user = $(this)
.closest('.mw-userlink ~ .mw-usertoollinks, .mw-changeslist-line-inner-userLink ~ .mw-changeslist-line-inner-userTalkLink')
.prevAll('.mw-userlink, .mw-changeslist-line-inner-userLink')
.last().text().trim();
if (!user) {
mw.notify(`Can't retrieve the username.`, {
tag: 'rcmuter',
type: 'error'
});
return;
}
let muting = this.parentElement.classList.toggle('rcmuter-unmute');
set[muting ? 'add' : 'delete'](user);
save();
this.textContent = muting ? 'unmute' : 'mute';
mw.notify(`${muting ? 'Muting' : 'Unmuting'} ${user} from next load.`, {
tag: 'rcmuter'
});
};
let addButtons = $content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) {
$content = $('#mw-content-text');
if ($content.has('.rcmuter-toggle').length) return;
}
let $tools = $content.find('.mw-usertoollinks.mw-changeslist-links');
let $muted = $tools.filter('.rcmuter-muted *');
$tools.not($muted).append(
$('<span>').addClass('rcmuter-toggle').append(
$('<a>').attr('href', '#').text('mute').on('click', toggle)
)
);
if (!$muted.length) return;
$muted.append(
$('<span>').addClass('rcmuter-toggle rcmuter-unmute').append(
$('<a>').attr('href', '#').text('unmute').on('click', toggle)
)
);
};
let mutedCount;
let filter = function () {
let muted = set.has(this.textContent);
if (muted) mutedCount++;
return muted;
};
mw.hook('wikipage.content').add($content => {
if (!$content.is('#mw-content-text, .mw-changeslist')) return;
if (!set.size) {
$toggleSpan.hide();
return;
}
mutedCount = 0;
$content.find('.changedby > .mw-userlink:only-child')
.filter(filter).closest('table').addClass('rcmuter-muted');
$content.find('.mw-userlink:not(.changedby > *, .comment *, .rcmuter-muted *)')
.filter(filter).closest('.mw-changeslist-line, table').addClass('rcmuter-muted')
.closest('table.mw-enhanced-rc').find('.changedby > .mw-userlink').filter(filter).addClass('rcmuter-muted');
$toggleSpan.toggle(!!mutedCount);
$toggle.attr('data-rcmuter', mutedCount);
});
});
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView();
} else {
yToSpan[Math.min(...lower)].scrollIntoView();
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView();
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
g7tre1usfstd3p6wgdnlw9xb8at9yd9
Tellurium
0
119445
738137
570291
2026-04-16T09:47:05Z
Dwalden test acctcreation28
73580
738137
wikitext
text/x-wiki
''Tellurium'' is a chemical element with symbol Te and atomic number Te in period 5. It appears as a None. It is a Solid
this is an edit
==Chemical properties==
Tellurium has an atomic mass of 127.603, a boiling point of 1261, a density of 6.24, a melting point of 722.66, a molar heat of 25.73, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 5s2 5p4, and an electron affinity of 190.161.
[[Category:Chemical elements]]
i0wa6jam56kt6ndpoq8n3ixnn676ecj
738138
738137
2026-04-16T09:47:26Z
Dwalden test acctcreation28
73580
738138
wikitext
text/x-wiki
''Tellurium'' is a chemical element with symbol Te and atomic number Te in period 5. It appears as a None. It is a Solid
==Chemical properties==
Tellurium has an atomic mass of 127.603, a boiling point of 1261, a density of 6.24, a melting point of 722.66, a molar heat of 25.73, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 5s2 5p4, and an electron affinity of 190.161.
[[Category:Chemical elements]]
6d50btq2wng298a1aorv2pvm3o6v5vi
Americium
0
119486
738041
470453
2026-04-15T14:06:07Z
~2026-23292-93
73584
showcaptcha
738041
wikitext
text/x-wiki
''Americium'' is a chemical element with symbol Am and atomic number Am in period 7. It appears as a silvery white. It is a Solid
edit
==Chemical properties==
Americium has an atomic mass of 243, a boiling point of 2880, a density of 12, a melting point of 1449, a molar heat of 62.7, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f14 5s2 5p6 5d10 5f7 6s2 6p6 7s2, and an electron affinity of 9.93.
[[Category:Chemical elements]]
kdzu36hfoside4ihbmbw9b7qiomy2mk
VA (Public & Science)
0
121929
738086
704694
2026-04-16T00:03:41Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
738086
wikitext
text/x-wiki
[[File:Vetenskap och Allmänhet årskonferens 2015.JPG|thumb|testing]]
'''VA (Public & Science)''' ([[Swedish language|Swedish]]: "'''[[:sv:Vetenskap & Allmänhet|Vetenskap & Allmänhet]]"''') is a Vetenskap & Allmänhet, VA, is a non-profit association that wants to promote dialogue and openness between researchers and the general public. VA operates primarily Sweden, but with an eye on European and International science communication including several new EU projects.<ref>{{Cite web|title=Projekt|url=https://v-a.se/projekt-portal/|access-date=2021-09-07|website=Vetenskap & Allmänhet|language=sv-SE|archive-date=2021-09-08|archive-url=https://web.archive.org/web/20210908054627/https://v-a.se/projekt-portal/}}</ref> The organization was founded in 2002 and is currently based from Grev Turegatan 14 in [[Stockholm]], [[Sweden]].<ref>{{Cite web|title=Om VA|url=https://v-a.se/om-va-portal/|access-date=2021-09-07|website=Vetenskap & Allmänhet|language=sv-SE|archive-date=2021-09-16|archive-url=https://web.archive.org/web/20210916154132/https://v-a.se/om-va-portal/}}</ref>
VA's members<ref>{{Cite news|url=https://v-a.se/member-organisations/|title=Member organisations - Vetenskap & Allmänhet|work=Vetenskap & Allmänhet|access-date=2018-03-08|language=sv-SE}}</ref> consist of some 80 organizations, authorities, universities, companies and associations. In addition, it has a number of individual members. The organization is funded through membership fees, project grants and a grant from the Swedish Ministry of Education and Research.
'''VA's Purpose and Primary Objectives'''
# Increase collaboration between researchers and the public.
# Increase knowledge and dialogue about:
#* Public perceptions of research and research needs
#* Prerequisites for research, research methods and results
#* Methods for science communication
# Be a leading knowledge hub for public engagement and science communication
# Strengthen all aspects of engagement with society; democratic, cultural as well as for the benefit and education of society
Part of what the VA does is carry out surveys<ref>{{Cite news|url=https://v-a.se/english-portal/projects/studies/the-public/|title=The public - Vetenskap & Allmänhet|work=Vetenskap & Allmänhet|access-date=2018-03-08|language=sv-SE}}</ref> and studies with the aim of increasing knowledge about the relationships between science and society at large. This includes an annual barometer into the Swedish public's general attitudes towards science and researchers, as well as more specific studies on how opinion-forming groups in society view and use research, how researchers interact with [[society]], and [[Methodology|methods]] for dialogue on [[science]] and [[research]].
VA also arranges many events and activities aimed at stimulating dialogue between researchers and the public in new ways and in novel arenas. VA is the Swedish national co-ordinator for the [[science festival]] [http://ec.europa.eu/research/researchersnight/index_en.htm European Researchers’ Night]. The first Researchers' Night was run in [[Sweden]] in September 2005<ref>{{cite web | url = http://ec.europa.eu/research/researchersineurope/news/article_3191_en.htm | title = Stockholm ‘erupts’ with activity for European Researchers’ Night | accessdate = 2009-03-20}}</ref> and in 2017 events were run in 29 cities in Sweden.<ref>{{cite news|url=https://forskarfredag.se/europeanresearchersnight2017/|title=A night of wonder, curiosity and fun: European Researchers’ Night 2017|accessdate=2018-03-08}}</ref>
VA also organises the [http://www.forskargrandprix.se/ Researchers' Grand Prix] {{Webarchive|url=https://web.archive.org/web/20210907153451/https://forskargrandprix.se/ |date=2021-09-07 }} , a science communication competition for researchers and [http://v-a.se/in-english/projects/activity-projects/researchers%E2%80%99-night/mass-experiments/ mass experiments in schools], a [[citizen science]] initiative that engages pupils in real research. Other activities include [[Science Cafe|Science Cafés]]. VA also participates in societal debate via traditional as well as social media.
VA is currently a partner in three EU-funded [[Framework Programmes for Research and Technological Development|Horizon 2020]] projects SciShops: ''[http://www.orion-openscience.eu/ ORION], Open Responsible research and Innovation to further Outstanding Knowledge'', is aimed at fostering [[Responsible Research and Innovation|RRI]] and [[open science]] in research performing and research funding organisations. ''[http://www.scishops.eu SciShops]'' will expand the ecosystem of [[Science shop|Science Shops]] in Europe and ''BLOOM'' is aimed at raising public awareness and interest in the [[Biobased economy|bioeconomy]] through dialogue and co-creation activities.
VA is a member of EUSEA [http://www.eusea.info/ (European Science Events Association)], ECSA ([https://ecsa.citizen-science.net/ European Citizen Science Association]) and the [http://www.livingknowledge.org/ Living Knowledge] {{Webarchive|url=https://web.archive.org/web/20210919022930/https://www.livingknowledge.org/ |date=2021-09-19 }} network.
== Impact & Development Work <ref>{{Cite web|title=VA-projekt|url=https://v-a.se/va-projekt-listade/|access-date=2021-09-07|website=Vetenskap & Allmänhet|language=sv-SE|archive-date=2021-09-07|archive-url=https://web.archive.org/web/20210907141435/https://v-a.se/va-projekt-listade/}}</ref> ==
* Letters and consultation responses
* Debate articles
* ARCS - a Swedish collaborative project to create a national portal for citizen research.
* Bloom - EU project to create regional networks to increase public interest and knowledge of bioeconomy
* EU-Citizen.Science - EU project for citizen research in Europe
* Falling Walls Engage Hub Sweden - the northern European node in an international network for research communication and research collaboration.
* March for Science / How do you know that? - Sweden
* News evaluator - Researchers, teachers and students together develop a tool for teaching source criticism
* ORION Open Science - EU project that will make research in life science and biomedicine more inclusive for society
* RETHINK - EU project for research communication in the future
* RRI Tools - EU project that has created a toolbox for responsible research and innovation
* Cooperation
* SciShops - EU project where a European network for Science shops is created
* Smedpack - a package solution for the safety of pharmaceutical consumers
* Super MoRRI - will develop and improve methods for measuring activities within RRI (responsible research and innovation).
* Swafs - Swedish platform to influence Horizon 2020
* Youcount - EU project to increase young people's participation in society.
* Open science workshops
== Studies ==
* Public attitudes
** The VA Barometer - recurring measurement of the Swedish public's view of science, research and researchers.
** Science in society (ViS) - recurring measurement and in-depth studies of the Swedish public's view of science, research and researchers in collaboration with the SOM Institute.
** International comparisons - analyzes of foreign attitude surveys
Other surveys:
* Communication about the corona - A study of the public's understanding, perception and how they are reached by information about the Covid-19 pandemic in Sweden.
* Animal experiments - the public's attitudes towards animal experiments
* Tiresome acquired and quickly ruined? In- depth study on researcher confidence
* Linnaeus Jubilee - The public about Carl von Linné 2007
* Science & Values - How does the outlook on life, values, cultural and social background affect the view of knowledge, research and researchers?
* Attitudes of children and young people
** Generation Equation - A report on children and young people's attitudes to school, school subjects and learning in Sweden
** Knowledge owns - young people's attitudes to knowledge, research and researchers
** Myself as a researcher - analysis of children's drawings of themselves as researchers.
* Researcher
** Researchers' views on communication and open science
** Research on research communication
* Journalists / media & communicators
* Authorities - Research-based knowledge in Swedish administration
* Business - the view of research and science in business
* Politicians - politicians' relationship to science
* Collaboration indicators
* School meets science - the school world's view of research and contacts with researchers
== Dialogue Activities ==
* Allthings.BioPRO - an online game about bioeconomy
* Almedalen
* Researcher dialogue
** ComScience - EU project to try new forms of conversation about research.
** Science Café - coffee with a researcher
* Researcher Friday - part of European Researchers' Night. EU project that shows how fun, exciting and everyday research can be.
** Mass experiment
* Researcher Grand Prix - research communication competition
* Forum for research communication - conference for actors in research communication
* Find Vasa's missing cannons
* Media seminars for researchers
* New images of research
* Post-antibiotic futures
* Collaborative seminar
** Research for the future - conference on future research policy
** ODE - external dialogue and commitment
** Science & Society - seminar on the collaboration of science with the outside world
** Stockholm Meeting with tour - seminar to promote mobility and collaboration between sectors, universities and countries
* Youth Parliament - introduces students to parliamentary procedures on science and research
* VA-day - Science & Publicity's annual half-day conference when this year's VA project is presented and discussed.
* What is science? - A film project with the aim of increasing children and young people's scientific understanding and ability.
* 2WAYS - EU project to test new ways of disseminating knowledge about research
'''The Board of Representatives'''
VA's board consists of representatives from the association's members. There are currently thirteen members and each member representative serves for a term of two years. The following are the representatives as of 2021.<ref>{{Cite web|title=Styrelse|url=https://v-a.se/om-va/organisation/styrelse/|access-date=2021-09-07|website=Vetenskap & Allmänhet|language=sv-SE}}</ref>
* Board President - Ann Fust
* Young Researchers Representative - Anna Hedlund
* Foundation for Strategic Research Representative - Lars Hultman
* IKEM Innovations and the chemical industry Representative - Magnus Huss
* KTH - Kungl. Institute of Technology Representative - Sigbritt Karlsson
* Engineers of Sweden Representative - Ulrika Lindstrand
* Chairman of the RIFO Society members of parliament and researchers - Betty Malmberg
* The Swedish Museum of Natural History - Lisa Månsson
* Student Unions Representative - David Samuelsson
* The Swedish Research Council - Sven Stafström
* IVA - Kungl. Academy of Engineering Sciences - Tuula Teeri
* Consultant - Urban Wass
* Freelance Journalist - Jack Werner
{| class="wikitable"
|+VA Member Organizations (2020) <ref>{{Cite web|title=Medlemsorganisationer|url=https://v-a.se/medlemsorganisationer/|access-date=2021-09-07|website=Vetenskap & Allmänhet|language=sv-SE}}</ref>
!ABF
AFA
Astronomical Youth
Childhood Cancer
Blekinge Institute of Technology
Chalmers University of Technology
Energy Agency
Swedish Defense Research Agency
Folkbildningsrådet
People's University
Research non-animal
research network Scania
Formas
FORTE, Council for health, labor and welfare
Freedom of Thought
Defense University
of Gothenburg
Keep Sweden Tidy
Dalarna University
College of Borås
School Halmstad
University of Skövde
University West
Ibn Rushd Study Association
Ifous - innovation, research and development in school and preschool
IKEM - Innovation and chemistry industries in Sweden
Institut français de Suède
Institutet för Framtidsstudier
IVA, Kungl. Academy of Engineering Sciences
IYPT Sweden
Jönköping University
Karlstad University
!Karolinska Institutet
KK-stiftelsen
Kungliga biblioteket
Kungl. The Swedish Academy of Forestry and Agriculture
Royal Institute of Technology
Royal. Vetenskapsakademien
Kungl. Vitterhetsakademien
LIF - the researching pharmaceutical companies
Linköping University
Linnaeus University
Food
Companies LSU, Swedish Youth Organizations
Luleå University of
Lund University,
Malmö University
Mistra
Mittuniversitetet
Swedish Civil Contingencies
Mälardalen University
Nature & Culture
Natural Resource Schools Association
Museum of Natural History
Scientists
Nobel Museum
Rifo, Society of MPs and researchers
National Heritage
Bank of Sweden Tercentenary
Research Institutes of Sweden
Runö college
Rymdstyrelsen
Share Music & Performing Arts
SIQ, Institute for Quality
development School Research Institute
!SMHI, Swedish Meteorological and Hydrological Institute
National Maritime and Transport historical museums,
the National Veterinary Institute
Foundation Maritime University
Foundation for Strategic Research
, Stockholm University
study associations
Swedish Science Center
Graduate Engineers
SLU
Swedish National Union of Students
Young Academy
of Sweden university teachers and researchers
Systembolaget
Södertörn University
Engineering Industries
Technical Museum
Thorildsplans Gymnasium
Tom Tits Experiment
Umeå University
Young Researchers
Universeum
University and College Council, UHR University Chancellor's Office
Uppsala University
Science in school
Science and Folkbildning
Swedish Research Council
VINNOVA
Wikimedia Sweden
Ängelholms gymnasieskola
Örebro University
|}
==References==
{{Reflist}}
==External links==
* [http://www.v-a.se Vetenskap & Allmänhet website] {{Webarchive|url=https://web.archive.org/web/20210829092650/https://v-a.se/ |date=2021-08-29 }}
* [http://forskarfredag.se/researchers-night/ Researchers' Night in Sweden] {{Webarchive|url=https://web.archive.org/web/20210916174618/https://forskarfredag.se/researchers-night/ |date=2021-09-16 }}
* [http://forskargrandprix.se/researchers-grand-prix-2014/ Researchers' Grand Prix] {{Webarchive|url=https://web.archive.org/web/20211016113248/https://forskargrandprix.se/researchers-grand-prix-2014/ |date=2021-10-16 }}
{{Authority control}}
[[Category:Scientific organizations based in Sweden]]
[[Category:Science in society]]
l77cfuv5mfdr1ziyce55wjxxm4y1um2
User:SongVĩ.Bot II
2
124239
738064
737959
2026-04-15T17:00:15Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1570 ngày...
738064
wikitext
text/x-wiki
Cập nhật lần cuối: 16-04-2026
Đã 1570 ngày...
ryf7dv9g31ijulyf58er9h4ebk782dv
Arthur Goldschmidt (Jurist)
0
149258
738017
688451
2026-04-15T12:13:47Z
~2026-23142-66
73571
738017
wikitext
text/x-wiki
'''Arthur Felix Goldschmidt'''<ref>{{Internetquelle |autor= |url=https://www.museumsverein-reinbek.de/wp-content/uploads/2019/11/Reinbek_Wege_und_Stra%C3%9Fen_2019.pdf |titel=Wege Straßen, Brücken und Plätze in Reinbek |werk= |hrsg=Museumsverein Reinbek |datum= |abruf=2020-10-22 |sprache=}}</ref> ([[30. April]] [[1873]] in [[Berlin]] – [[9. Februar]] [[1947]] in [[Reinbek]]) war ein deutscher Jurist und Lokalpolitiker.
this is an edit
== Leben ==
=== Familie und Ausbildung ===
Arthur Felix Goldschmidt wuchs als Sohn von Alfred Oscar Goldschmidt und dessen Ehefrau Pauline Lassar in einer assimilierten jüdischen Familie auf, die 1858 aus der jüdischen Gemeinde ausgetreten und zum Protestantismus übergetreten war. Er wurde 1889 evangelisch getauft. Seine Großmutter war die Schriftstellerin und [[Philanthrop]]in [[Johanna Goldschmidt]]. Nach dem Studium der Rechtswissenschaften, der [[Promotion (Doktor)|Promotion]] 1895 und dem 2. Juristischen Staatsexamen wurde er 1902 in [[Hamburg]] zum [[Amtsrichter]] ernannt und stieg dort zum [[Hanseatisches Oberlandesgericht|Oberlandesgerichtsrat in Hamburg]] auf. Während der [[Weimarer Republik]] lehnte Goldschmidt zweimal eine Berufung an das [[Reichsgericht]] in [[Leipzig]] ab, die Familie wollte in Reinbek bleiben. Dort saß er außerdem als Vertreter der nationalliberalen [[Deutsche Volkspartei|Deutschen Volkspartei]] im Gemeinderat.
=== Zeit des Nationalsozialismus und der Judenverfolgung ===
Nach der [[Machtergreifung|Machtübergabe an die Nationalsozialisten]] 1933 wurde Goldschmidt aufgrund des [[Gesetz zur Wiederherstellung des Berufsbeamtentums|Gesetzes zur Wiederherstellung des Berufsbeamtentums]] entlassen. In den folgenden Jahren arbeitete er als Kunstmaler, bis dahin war die Malerei sein Hobby gewesen.
Die Verfolgungspolitik der [[Nationalsozialistische Deutsche Arbeiterpartei|Nationalsozialisten]] schätzte Goldschmidt bald realistisch ein. Seine beiden Söhne [[Georges-Arthur Goldschmidt|Jürgen-Arthur]] und Erich schickte er 1938 ins Ausland, die Söhne sahen ihre Eltern nie wieder. Die ältere Tochter Ilse-Maria lebte mit ihrem Ehemann, dem Philosophen [[Ludwig Landgrebe]], zunächst in [[Prag]], dann bis 1940 in [[Belgien]] und kehrte von dort nach dem Beginn der deutschen Besetzung 1940 mit ihrer Familie nach Reinbek zurück. Goldschmidt sah sich tiefverwurzelt im protestantischen Glauben.
[[File:Philipp Manes, Arthur Goldschmidt, Theresienstadt 1944.jpg|thumb|Von Goldschmidt porträtierter Mitgefangener [[Philipp Manes]], etwa zwei Monate vor dessen Ermordung (Theresienstadt 1944)]]
Der schleswig-holsteinische Landesbischof [[Adalbert Paulsen]] unterstützte zusammen mit dem Landeskirchenamtspräsidenten [[Christian Kinder]] die Verfolgung der jüdischen Minderheit; am 10. Februar 1942 wurde der Ausschluss der „nichtarischen“ Christen aus der evangelischen Kirche für die Landeskirche verfügt. Dies geschah in Kenntnis und auch als Reaktion auf die Deportationen der deutschen Juden, die im Herbst 1941 begonnen hatte und von der auch evangelische Christen jüdischer Herkunft betroffen waren.<ref>http://www.geschichte-s-h.de/christen-und-juden-1933-1945/</ref> Im Juni 1942 starb seine Frau Toni Katharina-Maria Jeanette, genannt Kitty, geborene Horschitz, (1882–1942); der damalige Reinbeker Pastor Hermann Hartung (1904–1990), der sich gerne in Marineuniform als [[Militärgeistlicher]] präsentierte, weigerte sich, Kitty
als „einer Glaubensschwester den letzten Segen zu geben.“<ref>Detlev Landgrebe: Kückallee 37. Eine Kindheit am Rande des Holocaust, hrsg. von Thomas Hübner, Rheinbach 2009, ISBN 9783870621049, S. 138/</ref>.<ref>http://media.offenes-archiv.de/Rathausausstellung_2013_Wehrmachtjustiz_23.pdf</ref>
=== Deportation ins KZ Theresienstadt ===
Einen Monat nach dem Tod seiner Frau wurde Arthur Goldschmidt in das [[KZ Theresienstadt]] deportiert. Dort gründete er im Sommer 1942 einem Andachtskreis Hamburger Deportierter, aus dem nach und nach eine evangelische Gemeinde im KZ Theresienstadt entstand.<ref>Arthur Goldschmidt: ''Geschichte der evangelischen Gemeinde Theresienstadt 1942–1945'', neu hrsg. von Thomas Hübner, enth. in: Detlev Landgrebe, Kückallee 37, Rheinbach, CMZ-Verl 2009, ISBN 978-3-87062-104-9</ref> Trotz hoher Sterblichkeit und ständiger Transporte nach [[KZ Auschwitz-Birkenau|Auschwitz]] wuchs die Gemeinde auf einen Kern von etwa 800 eingeschriebenen Mitgliedern. Die Gottesdienste wurden an Feiertagen von mehreren hundert Menschen besucht.
=== Nach 1945 ===
Nach dem Krieg und der Befreiung kehrte Goldschmidt nach Reinbek zurück. Er wurde 1945 für die [[Christlich Demokratische Union Deutschlands|CDU]] Gemeindevertreter und stellvertretender Bürgermeister in Reinbek und einer der Mitbegründer der [[Volkshochschule Sachsenwald]], bei deren Eröffnungsrede er starb.<ref>[https://www.abendblatt.de/region/stormarn/article134245167/Ehre-fuer-einen-grossen-Reinbeker.html Hamburger Abendblatt: Ehre für einen großen Reinbeker]</ref><ref>https://www.bergedorfer-zeitung.de/archiv/reinbek/article112618080/65-Jahre-die-VHS-blickt-zurueck.html</ref>
== Gedenken ==
[[File:Stolperstein für Dr. Arthur Goldschmidt (Reinbek).jpg|thumb|x120px]]
<div class="tright" style="clear:none;">[[File:Stolperstein für Katharina Goldschmidt (Reinbek).jpg|thumb|ohne|x120px]]</div>
[[Gunter Demnig]] verlegte am 9. Oktober 2006 in der Kückallee 43 von Reinbek zwei [[Liste der Stolpersteine in Reinbek|Stolpersteine]] für das Ehepaar Goldschmidt.<ref>AKENS: ''[http://www.akens.org/akens/texte/stolpersteine/Stolpersteineliste.htm Liste der verlegten Stolpersteine in Schleswig-Holstein]'', abgerufen am 26. Oktober 2020</ref>
== Werke ==
* ''Geschichte der evangelischen Gemeinde Theresienstadt 1942–1945.'' (= ''[[Das christliche Deutschland 1933–1945|Das christliche Deutschland 1933 bis 1945]]''. H. 7). Furche-Verlag, Tübingen 1948.
* ''Geschichte der evangelischen Gemeinde Theresienstadt 1942–1945.'' (= ''[[Das christliche Deutschland 1933–1945|Das christliche Deutschland 1933 bis 1945]]''. H. 7). Furche-Verlag, Tübingen 1948, enthalten als Anhang in: Detlev Landgrebe: ''Kückallee 37 – Eine Kindheit am Rande des Holocaust'', hrsg. von Thomas Hübner, cmz Verlag, Rheinbach 2009, ISBN 978-3-87062-104-9, S. 375–426.
* Zahlreiche Zeichnungen erhalten, die er in Reinbek und Theresienstadt anfertigte, und die sich heute im Centre d'Histoire de la Résistance ee de la Déporation in Lyon befinden (Vermächtnis Georges-Arthur Goldschmidt).
== Literatur ==
* ''Kirche, Christen, Juden in Nordelbien 1933–1945.'' Die Ausstellung im Landtag 2005. Präsident des Schleswig-Holsteinischen Landtages, Kiel 2006 (''Schriftenreihe des Schleswig-Holsteinischen Landtages'' 7, {{ZDB|2151694-7}}).
* Detlev Landgrebe, Arthur Goldschmidt: ''Kückallee 37 – Eine Kindheit am Rande des Holocaust. Geschichte der evangelischen Gemeinde Theresienstadt 1942-1945.'' Herausgegeben von Thomas Hübner, cmz Verlag, Rheinbach 2009, ISBN 978-3-87062-104-9.
== Weblinks ==
* {{DNB-Portal|119315599}}
* {{Webarchiv |url=http://www.ghetto-theresienstadt.info/pages/g/goldschmidta.htm |wayback=20190214070115 |text=''Goldschmidt, Arthur''}} In: ''Theresienstadt Lexikon''
* [http://reinbeker-geschichten.de/?p=162 Museumsverein Reinbek - Reinbeker Geschichten]
== Einzelnachweise ==
<references />
{{Normdaten|TYP=p|GND=119315599|LCCN=no/2009/64306|VIAF=45108902}}
{{DEFAULTSORT:Goldschmidt, Arthur}}
[[Category:Richter (Hanseatisches Oberlandesgericht)]]
[[Category:Person (Reinbek)]]
[[Category:Häftling im Ghetto Theresienstadt]]
[[Category:Überlebender des Holocaust]]
[[Category:Person, für die in Schleswig-Holstein ein Stolperstein verlegt wurde]]
[[Category:DVP-Mitglied]]
[[Category:CDU-Mitglied]]
[[Category:Mitglied der Familie Goldschmidt|Arthur]]
[[Category:Deutscher]]
[[Category:Geboren 1873]]
[[Category:Gestorben 1947]]
[[Category:Mann]]
{{Personendaten
|NAME=Goldschmidt, Arthur
|ALTERNATIVNAMEN=Goldschmidt, Arthur Felix
|KURZBESCHREIBUNG=deutscher Richter
|GEBURTSDATUM=30. April 1873
|GEBURTSORT=[[Berlin]]
|STERBEDATUM=9. Februar 1947
|STERBEORT=[[Reinbek]]
}}
prefc9rli8mhj322e74if8k1gvo6opv
User talk:JWBTH/CD test page
3
154341
738029
737983
2026-04-15T13:40:24Z
JWBTH
52211
/* Last subsection */ fix layout in comment by [[User:Example|Example]] ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
738029
wikitext
text/x-wiki
== Section 1 ==
first section comment [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:37, 20 November 2024 (UTC)
unsigned comment
end {{unsigned|user}}
: comment to be edited [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:38, 20 November 2024 (UTC)
:: comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 02:41, 20 November 2024 (UTC)
::: child comment of comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 06:09, 27 August 2025 (UTC)
::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC)
:::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC)
: [[#c-Test_account_8-20241120023700-Section_1|Test account 8 @ 02:37, 20 November 2024 (UTC)]] [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 06:43, 28 March 2026 (UTC)
=== test2 ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:55, 14 September 2025 (UTC)
: [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 20:13, 26 March 2026 (UTC)
:: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:17, 8 April 2026 (UTC)
=== Comment with complex markup ===
* ̴͍͖̪̭̂ฑεᚹẻ̴̦̜̜͙̰̉̒͠͠иℳἒԊ৩βà̸̩̳̗m̶̧̲̲̬̌̀̈́̀ь β ì̵̛̹̌͛͝«Зᾷу៚ἐฑἒдì̵̛̹̌͛͝ю»ì̵̛̹̌͛͝ ! Ᾰ D̴̞̓̊̀ля чẻ̴̦̜̜͙̰̉̒͠͠рẻ̴̦̜̜͙̰̉̒͠͠счуr̵̢͈͕̺͎̀̅ s̸̢̈́ерьӚz̵͓̫̻͔͠Ԋыᚸ βыΔε௭иm̶̧̲̲̬̌̀̈́̀ь <s>ฑр৩c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀раԊc̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀β৩</s> ३ᾷβ৩Δь «D̴̞̓̊̀β৩йԊая z̵͓̫̻͔͠à̸̩̳̗௶พь». Ἇ m̶̧̲̲̬̌̀̈́̀৩ иz̵͓̫̻͔͠ Ԋẻ̴̦̜̜͙̰̉̒͠͠k̸̟͔̯̯̖̍̂͐̎͘৩m̶̧̲̲̬̌̀̈́̀৩ᚹыᚸ c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀ᾷ z̵͓̫̻͔͠а௶พь m̶̧̲̲̬̌̀̈́̀ᾷк ì̵̛̹̌͛͝и ௭ε३εm̶̧̲̲̬̌̀̈́̀ чεᚹعz̵͓̫̻͔͠ k̸̟͔̯̯̖̍̂͐̎͘ᚹᾷй !!! ̴͍͖̪̭̂ <span style="font-family:Calibri; font-size:175%; display: inline-block; letter-spacing: 5px; transform: rotate(10deg); padding: 20px 0px;>[[User:Example|'''<span style="color: Magenta; position: relative; top: -4px;">ঞ</span><span style="color: SpringGreen; position: relative; top: -3px;">ʆ</span><span style="color: red; position: relative; top: -2px;">ἕ</span><span style="color: LimeGreen; position: relative; top: -1px;">ฃ</span><span style="color: DeepPink; position: relative; top: 2px;">r̵̢͈͕̺͎̀̅</span><span style="color: Aqua; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: DarkOrange; position: relative; top: 4px;">D̴̞̓̊̀</span><span style="color: DarkOrchid; position: relative; top: 3px;">ἒ</span><span style="color: Chartreuse; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: Fuchsia; position: relative; top: 1px;">ໃ</span><span style="color: DarkTurquoise; position: relative; top: 0px;">à̸̩̳̗</span><span style="color: Forestgreen; position: relative; top: -2px;">ʁ</span><span style="color: deeppink; position: relative; top: 2px;">i̵͖̒͆̕͝ͅ</span><span style="color: Turquoise; position: relative; top: -1px;">ń̸̳͑̑͌</span><span style="color: LimeGreen; position: relative; top: -4px;">៩</span><span style="color: Magenta; position: relative; top: 1px;">♥</font>''']]</span> 14:08, 1 April 2026 (UTC)
*:test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:tests [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:57, 8 April 2026 (UTC)
*: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:07, 8 April 2026 (UTC)
*:s [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:39, 8 April 2026 (UTC)
*:ss [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:23, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Vote ===
Comment.
# Vote 1. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC)
# Vote 2. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC)
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
: Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 31 March 2026 (UTC)
:: Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:25, 31 March 2026 (UTC)
::: Comment beginning<br> Comment ending [[User:Example|Example]] ([[User talk:Example|talk]]) 09:34, 31 March 2026 (UTC)
== Section to add test comments ==
section [[User:Example|Example]] ([[User talk:Example|talk]]) 02:37, 1 March 2026 (UTC)
== Section with equals sign (=) for moving ==
<div class="cd-moveMark">''Moved to [[User talk:JWBTH/CD test page 2#Section with equals sign ({{=}}) for moving]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 13:40, 1 April 2026 (UTC)''</div>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
apu0zi3ym2fwpk08c6os3ip9wwej8ov
Wikimedia Commons
0
154377
738037
655542
2026-04-15T14:02:26Z
~2026-23292-93
73584
738037
wikitext
text/x-wiki
{{sust:cdb|Natural_environment}}
'''Wikimedia Commons''' (anaa simply '''Wiki''' '''Commons''') be wiki-based media repository of free-to-use images, sounds, videos den oda media. Ebe project of de Wikimedia Foundation.
Dem fi use files from Wikimedia Commons across all of de Wikimedia projects<ref name="Embedding">{{cite web|url=http://commons.wikimedia.org/wiki/Commons:First_steps/Reuse#Embedding_Commons'_media_in_Wikimedia_projects|title=Embedding Commons' media in Wikimedia projects|publisher=Wikimedia Commons|access-date=August 7, 2007}}</ref> for all languages insyd, wey dey include [[Wikipedia]], Wikivoyage, Wikisource, Wikiquote, Wiktionary, Wikinews, Wikibooks, den Wikispecies, anaa dem download for offsyt use. As of January 2024, de repository dey contain ova 102 million free-to-use media files, dem manage den editable by registered volunteers.<ref name="CommonsStats">[[c:Special:Statistics|Statistics page]] on Wikimedia Commons</ref>
edit
== References ==
3e43mcf5yhsyozqxu6a86w5znqp3itz
User:At1as
2
155647
738067
669336
2026-04-15T18:44:25Z
HakanIST
30391
HakanIST moved page [[User:TurkMapper]] to [[User:At1as]]: Automatically moved page while renaming the user "[[Special:CentralAuth/TurkMapper|TurkMapper]]" to "[[Special:CentralAuth/At1as|At1as]]"
669336
wikitext
text/x-wiki
Turk_Mapper'ın En aktif olduğu hesaba hoşgeldiniz, For English please look to DIĞER DİLLER Part.
==== Hakkımda ====
====== • Vikipedi ======
Ben Turk Mapper, 2020'de Vikipturkish hesabını açarak Vikipediye girdim, o hesapta çok şeyler yapmadım, şuana kadar ana hesabla 8 Tane Değişiklik Yaptım, 2023'e çok nadir değişiklikler yaptım, 2023'ün mayısında bu hesabı açtım ve bu hesabla Vikipediada Şuana 100den fazla değişiklik yaptım.
• Bazı Kişisel bilgiler
Ben Türkiyede yaşıyorum, istanbulda doğdum, memleketim karadeniz, B2 ingilizce seviyem var.
==== Babil Kulesi ====
==== Diğer Dillerde hesabım/Other Languages ====
t5r8grs6gm54garpda88yryqn6lc40k
Event:13May
1728
155780
738048
735059
2026-04-15T14:28:04Z
~2026-23347-42
73585
showcaptcha
738048
wikitext
text/x-wiki
hola este es un test
2xyt19253a2bou4xqs6cvdh3emvxqxb
File:22Test image.jpg
6
165007
738044
649823
2026-04-15T14:20:40Z
~2026-23347-42
73585
738044
wikitext
text/x-wiki
This is an edit
== Summary ==
test image for replacement test
8oubjb1waxq3nystdf6d1ks4fedtpw8
NonExistentPage-1746377258
0
165936
738038
656437
2026-04-15T14:02:42Z
~2026-23292-93
73584
738038
wikitext
text/x-wiki
Hi I'm a new page
this is an edit
27yn3gjeimacwlpzyjxyj15fwhvsgrk
File:Wikitech-2021-blue-large-icon(2).svg
6
166430
738084
737410
2026-04-15T23:26:01Z
Ladsgroup
2217
Ladsgroup uploaded a new version of [[File:Wikitech-2021-blue-large-icon(2).svg]]
659173
wikitext
text/x-wiki
== Licensing ==
{{self|GFDL|cc-by-sa-all|migration=redundant}}
5ln3dh380i59r738tvezfwc817fpsth
CAPTCHA test
0
167162
738111
666573
2026-04-16T07:48:41Z
Dwalden test acctcreation28
73580
738111
wikitext
text/x-wiki
Test
this is an edit
bu33mz05skvrpg1397ac3zqs2ab8ega
Picture of the Year/2024/Gallery/Mammals
0
167540
738145
670401
2026-04-16T10:52:59Z
~2026-17482-89
73184
738145
wikitext
text/x-wiki
this is an edit{{POTY gallery/2024}}
debr3rz8gui9nauos03j9aa9t626rqv
Τυρόγαλα
0
167855
738135
673177
2026-04-16T09:46:16Z
Dwalden test acctcreation28
73580
738135
wikitext
text/x-wiki
[[Αρχείο:Whey.jpg|160px|thumb|right|Τυρόγαλο]]
Το '''τυρόγαλα''' ή '''τυρόγαλο''' <ref>[http://www.greek-language.gr/greekLang/modern_greek/tools/lexica/triantafyllides/search.html?lq=τυρόγαλο&dq= Τυρόγαλο] στο Λεξικό της κοινής νεοελληνικής. Ανάκτηση 13/11/2014.</ref> ή ο '''ορός γάλακτος''' είναι το [[διάλυμα]] που προκύπτει κατά τη διάρκεια της [[τυροκόμηση]]ς και πιο συγκεκριμένα είναι το υγρό που απομένει μετά την πήξη του [[γάλα]]κτος και την απομάκρυνση του στερεού [[Τυρόπηγμα|τυροπήγματος]].<ref>{{cite web
|author=Εργαστήριο Μηχανικής Μεταποίησης Αγροτικών Προϊόντων
|title=Αξιοποίηση Τυρογάλακτος με Ζύμωση για Παραγωγή Αλκοόλης
|url=http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|publisher=fabe.gr
|date=
|accessdate=2014-11-12
|archive-date=2015-05-01
|archive-url=https://web.archive.org/web/20150501075455/http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|url-status=dead
}}</ref> Το διάλυμα είναι [[pH|όξινο]] λόγω του [[γαλακτικό οξύ|γαλακτικού οξέος]] που περιέχει. Το τυρόγαλα έχει διάφορες χρήσεις, μεταξύ άλλων στη γαλακτοβιομηχανία ως πρόσθετο στη διαδικασία [[τυποποίηση γάλακτος|τυποποίησης γάλακτος]], ως [[συμπλήρωμα διατροφής]], καθώς και ως πηγή [[πρωτεΐνη|πρωτεϊνών]]. Με την κατάλληλη επεξεργασία του τυρόγαλου προκύπτει τα νωπά τυριά τύπου [[μυζήθρα]]ς<ref>{{cite web
|url=http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|title=Παρασκευή γαλακτοκομικών προϊόντων
|publisher=[[Υπουργείο Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος (Κύπρος)|Υπουργείου Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος της Κύπρου]]
|accessdate=2019-09-16
|archive-date=2021-10-28
|archive-url=https://web.archive.org/web/20211028020509/http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|url-status=
}}</ref>. Το τυρόγαλα αποτελεί [[απόβλητο]] της [[τυροκομία]]ς, ιδιαίτερα επιβλαβές<ref>{{cite news|last=Ελαφρός|first=Γιάννης|title=Βιολειτουργικό τρόφιμο από τυρόγαλα|url=http://www.kathimerini.gr/460872/article/epikairothta/ellada/violeitoyrgiko-trofimo-apo-tyrogala|accessdate=13 Νοεμβρίου 2014|newspaper=Καθημερινή|date=23 Ιουνίου 2012}}</ref> για το περιβάλλον όταν δεν γίνεται σωστή διαχείρισή του.
this is an edit
== Συγχρώσεις ==
Το γλυκό γάλα και το οξύ test test test γάλα είναι παρόμοια στην ακαθάριστη διατροφική ανάλυση.
== Παραπομπές ==
<references />
==Εξωτερικοί σύνδεσμοι==
* {{commonscat2}}
{{Authority control}}
{{επέκταση}}
[[Κατηγορία:Γαλακτοκομικά προϊόντα]]
[[Κατηγορία:Βιομηχανία τροφίμων]]
qldx2t2xfh1acadnabgnm6yaeuhbrob
738136
738135
2026-04-16T09:46:49Z
Dwalden test acctcreation28
73580
738136
wikitext
text/x-wiki
[[Αρχείο:Whey.jpg|160px|thumb|right|Τυρόγαλο]]
Το '''τυρόγαλα''' ή '''τυρόγαλο''' <ref>[http://www.greek-language.gr/greekLang/modern_greek/tools/lexica/triantafyllides/search.html?lq=τυρόγαλο&dq= Τυρόγαλο] στο Λεξικό της κοινής νεοελληνικής. Ανάκτηση 13/11/2014.</ref> ή ο '''ορός γάλακτος''' είναι το [[διάλυμα]] που προκύπτει κατά τη διάρκεια της [[τυροκόμηση]]ς και πιο συγκεκριμένα είναι το υγρό που απομένει μετά την πήξη του [[γάλα]]κτος και την απομάκρυνση του στερεού [[Τυρόπηγμα|τυροπήγματος]].<ref>{{cite web
|author=Εργαστήριο Μηχανικής Μεταποίησης Αγροτικών Προϊόντων
|title=Αξιοποίηση Τυρογάλακτος με Ζύμωση για Παραγωγή Αλκοόλης
|url=http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|publisher=fabe.gr
|date=
|accessdate=2014-11-12
|archive-date=2015-05-01
|archive-url=https://web.archive.org/web/20150501075455/http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|url-status=dead
}}</ref> Το διάλυμα είναι [[pH|όξινο]] λόγω του [[γαλακτικό οξύ|γαλακτικού οξέος]] που περιέχει. Το τυρόγαλα έχει διάφορες χρήσεις, μεταξύ άλλων στη γαλακτοβιομηχανία ως πρόσθετο στη διαδικασία [[τυποποίηση γάλακτος|τυποποίησης γάλακτος]], ως [[συμπλήρωμα διατροφής]], καθώς και ως πηγή [[πρωτεΐνη|πρωτεϊνών]]. Με την κατάλληλη επεξεργασία του τυρόγαλου προκύπτει τα νωπά τυριά τύπου [[μυζήθρα]]ς<ref>{{cite web
|url=http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|title=Παρασκευή γαλακτοκομικών προϊόντων
|publisher=[[Υπουργείο Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος (Κύπρος)|Υπουργείου Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος της Κύπρου]]
|accessdate=2019-09-16
|archive-date=2021-10-28
|archive-url=https://web.archive.org/web/20211028020509/http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|url-status=
}}</ref>. Το τυρόγαλα αποτελεί [[απόβλητο]] της [[τυροκομία]]ς, ιδιαίτερα επιβλαβές<ref>{{cite news|last=Ελαφρός|first=Γιάννης|title=Βιολειτουργικό τρόφιμο από τυρόγαλα|url=http://www.kathimerini.gr/460872/article/epikairothta/ellada/violeitoyrgiko-trofimo-apo-tyrogala|accessdate=13 Νοεμβρίου 2014|newspaper=Καθημερινή|date=23 Ιουνίου 2012}}</ref> για το περιβάλλον όταν δεν γίνεται σωστή διαχείρισή του.
== Συγχρώσεις ==
Το γλυκό γάλα και το οξύ test test test γάλα είναι παρόμοια στην ακαθάριστη διατροφική ανάλυση.
== Παραπομπές ==
<references />
==Εξωτερικοί σύνδεσμοι==
* {{commonscat2}}
{{Authority control}}
{{επέκταση}}
[[Κατηγορία:Γαλακτοκομικά προϊόντα]]
[[Κατηγορία:Βιομηχανία τροφίμων]]
jikdmtyaxssx0875jit1tovxwjlilem
738139
738136
2026-04-16T09:50:34Z
Dwalden test acctcreation28
73580
738139
wikitext
text/x-wiki
[[Αρχείο:Whey.jpg|160px|thumb|right|Τυρόγαλο]]
Το '''τυρόγαλα''' ή '''τυρόγαλο''' <ref>[http://www.greek-language.gr/greekLang/modern_greek/tools/lexica/triantafyllides/search.html?lq=τυρόγαλο&dq= Τυρόγαλο] στο Λεξικό της κοινής νεοελληνικής. Ανάκτηση 13/11/2014.</ref> ή ο '''ορός γάλακτος''' είναι το [[διάλυμα]] που προκύπτει κατά τη διάρκεια της [[τυροκόμηση]]ς και πιο συγκεκριμένα είναι το υγρό που απομένει μετά την πήξη του [[γάλα]]κτος και την απομάκρυνση του στερεού [[Τυρόπηγμα|τυροπήγματος]].<ref>{{cite web
|author=Εργαστήριο Μηχανικής Μεταποίησης Αγροτικών Προϊόντων
|title=Αξιοποίηση Τυρογάλακτος με Ζύμωση για Παραγωγή Αλκοόλης
|url=http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|publisher=fabe.gr
|date=
|accessdate=2014-11-12
|archive-date=2015-05-01
|archive-url=https://web.archive.org/web/20150501075455/http://www.fabe.gr/images/stories/KAINOTOMIAS/rizostexn.pdf
|url-status=dead
}}</ref> Το διάλυμα είναι [[pH|όξινο]] λόγω του [[γαλακτικό οξύ|γαλακτικού οξέος]] που περιέχει. Το τυρόγαλα έχει διάφορες χρήσεις, μεταξύ άλλων στη γαλακτοβιομηχανία ως πρόσθετο στη διαδικασία [[τυποποίηση γάλακτος|τυποποίησης γάλακτος]], ως [[συμπλήρωμα διατροφής]], καθώς και ως πηγή [[πρωτεΐνη|πρωτεϊνών]]. Με την κατάλληλη επεξεργασία του τυρόγαλου προκύπτει τα νωπά τυριά τύπου [[μυζήθρα]]ς<ref>{{cite web
|url=http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|title=Παρασκευή γαλακτοκομικών προϊόντων
|publisher=[[Υπουργείο Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος (Κύπρος)|Υπουργείου Γεωργίας, Φυσικών Πόρων και Περιβάλλοντος της Κύπρου]]
|accessdate=2019-09-16
|archive-date=2021-10-28
|archive-url=https://web.archive.org/web/20211028020509/http://www.moa.gov.cy/moa/da/da.nsf/All/9A8800947665C8F6C2257DE800468038/$file/%CE%9F%CE%BA%2013_2014_Galaktokomika%20proionta.pdf
|url-status=
}}</ref>. Το τυρόγαλα αποτελεί [[απόβλητο]] της [[τυροκομία]]ς, ιδιαίτερα επιβλαβές<ref>{{cite news|last=Ελαφρός|first=Γιάννης|title=Βιολειτουργικό τρόφιμο από τυρόγαλα|url=http://www.kathimerini.gr/460872/article/epikairothta/ellada/violeitoyrgiko-trofimo-apo-tyrogala|accessdate=13 Νοεμβρίου 2014|newspaper=Καθημερινή|date=23 Ιουνίου 2012}}</ref> για το περιβάλλον όταν δεν γίνεται σωστή διαχείρισή του.
this is an edit
== Συγχρώσεις ==
Το γλυκό γάλα και το οξύ test test test γάλα είναι παρόμοια στην ακαθάριστη διατροφική ανάλυση.
== Παραπομπές ==
<references />
==Εξωτερικοί σύνδεσμοι==
* {{commonscat2}}
{{Authority control}}
{{επέκταση}}
[[Κατηγορία:Γαλακτοκομικά προϊόντα]]
[[Κατηγορία:Βιομηχανία τροφίμων]]
qldx2t2xfh1acadnabgnm6yaeuhbrob
Farzi
0
169022
738054
683958
2026-04-15T14:54:07Z
~2026-23302-47
73582
738054
wikitext
text/x-wiki
Jjjjjzsxixdiecdie
this is an edit
d0fi07zzh5y1r389u8dohbp6sih42gt
Page 81
0
169580
738028
704022
2026-04-15T13:40:22Z
DWalden (WMF)
44719
738028
wikitext
text/x-wiki
9fd87900b3872e8dbe533929003a5a00
this is an edit
tkjh9p50de5bqn3n0kps0kwj8c8872u
738030
738028
2026-04-15T13:40:32Z
DWalden (WMF)
44719
738030
wikitext
text/x-wiki
9fd87900b3872e8dbe533929003a5a00
7cehamyjwqzovt1ll8xty4gkekt4bml
Page 3b
0
169588
738112
703988
2026-04-16T07:50:48Z
Dwalden test acctcreation28
73580
738112
wikitext
text/x-wiki
c0b9002b56d57349338b6a48183bfdc9
edit
qc6qioxrtl08lqy2p3k7fr3d9k7btid
738113
738112
2026-04-16T08:01:41Z
Dwalden test acctcreation28
73580
738113
wikitext
text/x-wiki
c0b9002b56d57349338b6a48183bfdc9
6pwhcnjt88vwxehzpo3r6mvkdjgtz17
Page ec
0
169611
738039
704044
2026-04-15T14:03:02Z
~2026-23292-93
73584
738039
wikitext
text/x-wiki
cda1cf728cb6c55739d60ea1f41f331c
this is an edit
9rkb0n62pomy2opdgg4b3jbw4phmu4c
Wp/sas/Kabupatén Manggarai Timuq
0
171819
738023
725827
2026-04-15T13:36:50Z
~2026-23142-66
73571
738023
wikitext
text/x-wiki
'''Kabupatén Manggarai Timuq''' no [[Wp/sas/Kabupatén|kabupatén]] sak arak léq [[Wp/sas/Propinsi Nusa Tenggara Timuq|Propinsi Nusa Tenggara Timuq]], [[Wp/sas/Indonésia|Indonésia]]. Mangkin araq 249.218 dengan léq kabupatén niki. Ibu kotan kabupatén ni Kota Waingapu. Luas wilayahn kabupatén ni 7.000,5 km<sup>2</sup>.
[[Category:Wp/sas]]
[[Category:Wp/sas/Kabupatén léq Indonésia]]
{{INTERWIKI|Q14137}}
this is an edit
1fva040azub9i97rk5fr2u28qayjd63
738024
738023
2026-04-15T13:37:03Z
~2026-23142-66
73571
738024
wikitext
text/x-wiki
'''Kabupatén Manggarai Timuq''' no [[Wp/sas/Kabupatén|kabupatén]] sak arak léq [[Wp/sas/Propinsi Nusa Tenggara Timuq|Propinsi Nusa Tenggara Timuq]], [[Wp/sas/Indonésia|Indonésia]]. Mangkin araq 249.218 dengan léq kabupatén niki. Ibu kotan kabupatén ni Kota Waingapu. Luas wilayahn kabupatén ni 7.000,5 km<sup>2</sup>.
[[Category:Wp/sas]]
[[Category:Wp/sas/Kabupatén léq Indonésia]]
{{INTERWIKI|Q14137}}
2sn2aiaunbpt64uxd47nujtdkd92omk
Žaba žaba 1
0
174513
738065
735388
2026-04-15T17:04:14Z
KSoučková-WMF
61884
738065
wikitext
text/x-wiki
Žaba is not a žirafa, but I'm testing, so.
[[category:Žirafy]]
[[category:Žaby]]
[[category:Žeby dva patkáňe?]]
snamyfw35uj4dnbswjuu07l2qbnalu6
AutoArticle mdADdCAvAV
0
174531
738050
735431
2026-04-15T14:37:29Z
~2026-23347-42
73585
738050
wikitext
text/x-wiki
Clam demoror solus vorago textilis.
this is an edit
20u8cifs1v11ef3ydbq5us0vbpzxc54
User talk:JWBTH/CD example page
3
174624
738055
737255
2026-04-15T14:59:36Z
JWBTH
52211
/* Proposal to update the main infobox image */
738055
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|200px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
jj4t841ndh4ofhyy4s12uiqm0d7gd8u
738056
738055
2026-04-15T15:13:10Z
JWBTH
52211
/* Proposal to update the main infobox image */
738056
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|194px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
aajnkeydh83c6405n6pwvdmhuw8hg3h
738057
738056
2026-04-15T15:16:05Z
JWBTH
52211
/* Proposal to update the main infobox image */
738057
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
chhohirswhsccr1wivjrvz0l393n3n1
738058
738057
2026-04-15T15:19:52Z
JWBTH
52211
/* Proposal to update the main infobox image */
738058
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|194px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
aajnkeydh83c6405n6pwvdmhuw8hg3h
738059
738058
2026-04-15T15:20:21Z
JWBTH
52211
/* Proposal to update the main infobox image */
738059
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
chhohirswhsccr1wivjrvz0l393n3n1
738060
738059
2026-04-15T15:35:23Z
JWBTH
52211
/* Proposal to update the main infobox image */
738060
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|194px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
aajnkeydh83c6405n6pwvdmhuw8hg3h
738061
738060
2026-04-15T15:35:50Z
JWBTH
52211
/* Proposal to update the main infobox image */
738061
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:Blue Penguin|BluePenguin]] ([[User talk:Blue Penguin|talk]]) 19:10, 14 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
chhohirswhsccr1wivjrvz0l393n3n1
738062
738061
2026-04-15T15:44:43Z
JWBTH
52211
/* Proposal to update the main infobox image */
738062
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: Collapsed reply. --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
galvu1hnfvxwc353w73lrwbmhvyuygt
738063
738062
2026-04-15T15:47:04Z
JWBTH
52211
738063
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity. However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
88gkv77qodq1dw7wys37jxb1u6thdxw
738087
738063
2026-04-16T04:49:47Z
JWBTH
52211
/* Proposal to update the main infobox image */
738087
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity.
However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
dvzz5lgys735zijlrxxe5lg3jq85hca
738088
738087
2026-04-16T05:04:20Z
JWBTH
52211
/* Proposal to update the main infobox image */
738088
wikitext
text/x-wiki
== Archive 15 moved to subpage ==
(Bot message text...)
== RfC: Neutral point of view regarding the "Criticism" section ==
(Long-winded debate text...)
== Proposal to update the main infobox image ==
[[File:Macaca nigra self-portrait large.jpg|thumb|193px|right|Proposed replacement]]
Hey everyone, I've noticed that the current image in the infobox is quite outdated and doesn't really reflect the recent consensus we reached regarding the [[Wikipedia:Manual of Style/Images|MOS on images]].
I propose we replace it with this widely known alternative from Wikimedia Commons. This new image has much better lighting, is public domain, and directly illustrates the core subject looking right at the camera.
What are your thoughts? --[[User:WikiEditor99|WikiEditor99]] ([[User talk:WikiEditor99|talk]]) 14:45, 8 April 2026 (UTC)
: I generally support this change. The new image is definitely a step up in personality and clarity.
However, have we checked if the resolution is high enough to look crisp for users on high-density screens? --[[User:DesertRose87|DesertRose87]] ([[User talk:DesertRose87|talk]]) 15:42, 8 April 2026 (UTC)
:: Collapsed reply. --[[User:Feline Fanatic|FelineFanatic]] ([[User talk:Feline Fanatic|talk]]) 16:40, 8 April 2026 (UTC)
::: Collapsed reply. --[[User:User 4920|User 4920]] ([[User talk:User 4920|talk]]) 17:40, 8 April 2026 (UTC)
:: I'm going to have to play devil's advocate here and '''oppose''' replacing it entirely. While the charm of the new photo is undeniable, the historical context of the ''current'' image is highly relevant to the article's text. Perhaps we should keep the old one and just move it down into the body of the article. --[[User:HistorianX|HistorianX]] ([[User talk:HistorianX|talk]]) 19:08, 8 April 2026 (UTC)
== Missing secondary sources in the 'Legacy' section ==
(Editor request text...)
== Good Article nomination: Final checklist ==
(Criteria list...) [[User:Example|Example]] 17:48, 15 February 2026 (UTC)
== Typo ==
(Short message about a misspelling...)
noo75l1m9zqtmnckhep59n3ysdopmif
User:MrJaroslavik/GlobalCheckUserStats.js
2
174673
738066
737960
2026-04-15T17:43:12Z
MrJaroslavik
44012
e
738066
javascript
text/javascript
// GlobalCheckUserStats.js
// -------------------------------------------------------
// Features:
// - Global Stats: Scans logs across 800+ projects at once.
// - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles.
// - Deep Scan: Bypasses API limits to get full history (limit 500 entries).
// - Stable: Retries 3x on rate limits so no wiki is missed.
// - Error Logs: Lists failed projects (ex. because of 429) at the end of the report.
// - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]].
// - Full Reporting: Outputs sortable Wikitables and detailed rights change logs.
// - With help of Gemini 3
// -------------------------------------------------------
// Wrap everything so this script doesn't break other Wikipedia scripts
(function () {
'use strict';
// Stop immediately if we are NOT on the specific GlobalCheckUserStats page
if (
mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' ||
mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1
) return;
// A fixed list of all valid Wikimedia projects to scan
const rawWikis = ['abstractwiki', 'abwiki', 'acewiki', 'adywiki', 'afwiki', 'afwikibooks', 'afwikiquote', 'afwiktionary', 'alswiki', 'altwiki', 'amiwiki', 'amwiki', 'amwiktionary', 'angwiki', 'angwiktionary', 'annwiki', 'anpwiki', 'anwiki', 'anwiktionary', 'arcwiki', 'arwiki', 'arwikibooks', 'arwikimedia', 'arwikinews', 'arwikiquote', 'arwikisource', 'arwikiversity', 'arwiktionary', 'arywiki', 'arzwiki', 'astwiki', 'astwiktionary', 'aswiki', 'aswikiquote', 'aswikisource', 'atjwiki', 'avkwiki', 'avwiki', 'awawiki', 'aywiki', 'aywiktionary', 'azbwiki', 'azwiki', 'azwikibooks', 'azwikiquote', 'azwikisource', 'azwiktionary', 'banwiki', 'banwikisource', 'barwiki', 'bat_smgwiki', 'bawiki', 'bawikibooks', 'bbcwiki', 'bclwiki', 'bclwikiquote', 'bclwikisource', 'bclwiktionary', 'bdrwiki', 'bdwikimedia', 'be_x_oldwiki', 'betawikiversity', 'bewiki', 'bewikibooks', 'bewikimedia', 'bewikiquote', 'bewikisource', 'bewiktionary', 'bewwiki', 'bewwiktionary', 'bgwiki', 'bgwikibooks', 'bgwikiquote', 'bgwikisource', 'bgwiktionary', 'bhwiki', 'biwiki', 'bjnwiki', 'bjnwikiquote', 'bjnwiktionary', 'blkwiki', 'blkwiktionary', 'bmwiki', 'bnwiki', 'bnwikibooks', 'bnwikiquote', 'bnwikisource', 'bnwikivoyage', 'bnwiktionary', 'bowiki', 'bpywiki', 'brwiki', 'brwikimedia', 'brwikiquote', 'brwikisource', 'brwiktionary', 'bswiki', 'bswikibooks', 'bswikinews', 'bswikiquote', 'bswikisource', 'bswiktionary', 'btmwiki', 'btmwiktionary', 'bugwiki', 'bxrwiki', 'cawiki', 'cawikibooks', 'cawikimedia', 'cawikinews', 'cawikiquote', 'cawikisource', 'cawiktionary', 'cbk_zamwiki', 'cdowiki', 'cebwiki', 'cewiki', 'chrwiki', 'chrwiktionary', 'chwiki', 'chywiki', 'ckbwiki', 'ckbwiktionary', 'commonswiki', 'cowiki', 'cowikimedia', 'cowiktionary', 'crhwiki', 'csbwiki', 'csbwiktionary', 'cswiki', 'cswikibooks', 'cswikinews', 'cswikiquote', 'cswikisource', 'cswikiversity', 'cswikivoyage', 'cswiktionary', 'cuwiki', 'cvwiki', 'cvwikibooks', 'cywiki', 'cywikibooks', 'cywikiquote', 'cywikisource', 'cywiktionary', 'dagwiki', 'dawiki', 'dawikibooks', 'dawikiquote', 'dawikisource', 'dawiktionary', 'dewiki', 'dewikibooks', 'dewikinews', 'dewikiquote', 'dewikisource', 'dewikiversity', 'dewikivoyage', 'dewiktionary', 'dgawiki', 'dinwiki', 'diqwiki', 'diqwiktionary', 'dkwikimedia', 'dsbwiki', 'dtpwiki', 'dtywiki', 'dvwiki', 'dvwiktionary', 'dzwiki', 'eewiki', 'elwiki', 'elwikibooks', 'elwikinews', 'elwikiquote', 'elwikisource', 'elwikiversity', 'elwikivoyage', 'elwiktionary', 'emlwiki', 'enwiki', 'enwikibooks', 'enwikinews', 'enwikiquote', 'enwikisource', 'enwikiversity', 'enwikivoyage', 'enwiktionary', 'eowiki', 'eowikibooks', 'eowikinews', 'eowikiquote', 'eowikisource', 'eowikivoyage', 'eowiktionary', 'eswiki', 'eswikibooks', 'eswikinews', 'eswikiquote', 'eswikisource', 'eswikiversity', 'eswikivoyage', 'eswiktionary', 'etwiki', 'etwikibooks', 'eewikimedia', 'etwikiquote', 'etwikisource', 'etwiktionary', 'euwiki', 'euwikibooks', 'euwikiquote', 'euwikisource', 'euwiktionary', 'extwiki', 'fatwiki', 'fawiki', 'fawikibooks', 'fawikinews', 'fawikiquote', 'fawikisource', 'fawikivoyage', 'fawiktionary', 'ffwiki', 'fiu_vrowiki', 'fiwiki', 'fiwikibooks', 'fiwikimedia', 'fiwikinews', 'fiwikiquote', 'fiwikisource', 'fiwikiversity', 'fiwikivoyage', 'fiwiktionary', 'fjwiki', 'fjwiktionary', 'fonwiki', 'foundationwiki', 'fowiki', 'fowikisource', 'fowiktionary', 'frpwiki', 'frrwiki', 'frwiki', 'frwikibooks', 'frwikinews', 'frwikiquote', 'frwikisource', 'frwikiversity', 'frwikivoyage', 'frwiktionary', 'furwiki', 'fywiki', 'fywikibooks', 'fywiktionary', 'gagwiki', 'ganwiki', 'gawiki', 'gawiktionary', 'gcrwiki', 'gdwiki', 'gdwiktionary', 'glkwiki', 'glwiki', 'glwikibooks', 'glwikiquote', 'glwikisource', 'glwiktionary', 'gnwiki', 'gnwiktionary', 'gomwiki', 'gomwiktionary', 'gorwiki', 'gorwikiquote', 'gorwiktionary', 'gotwiki', 'gpewiki', 'gucwiki', 'gurwiki', 'guwiki', 'guwikiquote', 'guwikisource', 'guwiktionary', 'guwwiki', 'guwwikinews', 'guwwikiquote', 'guwwiktionary', 'gvwiki', 'gvwiktionary', 'hakwiki', 'hawiki', 'hawiktionary', 'hawwiki', 'hewiki', 'hewikibooks', 'hewikinews', 'hewikiquote', 'hewikisource', 'hewikivoyage', 'hewiktionary', 'hifwiki', 'hifwiktionary', 'hiwiki', 'hiwikibooks', 'hiwikiquote', 'hiwikisource', 'hiwikiversity', 'hiwikivoyage', 'hiwiktionary', 'hrwiki', 'hrwikibooks', 'hrwikiquote', 'hrwikisource', 'hrwiktionary', 'hsbwiki', 'hsbwiktionary', 'htwiki', 'huwiki', 'huwikibooks', 'huwikisource', 'huwiktionary', 'hywiki', 'hywikibooks', 'hywikiquote', 'hywikisource', 'hywiktionary', 'hywwiki', 'iawiki', 'iawikibooks', 'iawiktionary', 'ibawiki', 'idwiki', 'idwikibooks', 'idwikiquote', 'idwikisource', 'idwikivoyage', 'idwiktionary', 'iewiki', 'iewiktionary', 'iglwiki', 'igwiki', 'igwikiquote', 'igwiktionary', 'ikwiki', 'ilowiki', 'incubatorwiki', 'inhwiki', 'iowiki', 'iowiktionary', 'iswiki', 'iswikibooks', 'iswikiquote', 'iswikisource', 'iswiktionary', 'itwiki', 'itwikibooks', 'itwikinews', 'itwikiquote', 'itwikisource', 'itwikiversity', 'itwikivoyage', 'itwiktionary', 'iuwiki', 'iuwiktionary', 'jamwiki', 'jawiki', 'jawikibooks', 'jawikinews', 'jawikisource', 'jawikiversity', 'jawikivoyage', 'jawiktionary', 'jbowiki', 'jbowiktionary', 'jvwiki', 'jvwikisource', 'jvwiktionary', 'kaawiki', 'kaawiktionary', 'kabwiki', 'kaiwiki', 'kajwiki', 'kawiki', 'kawikibooks', 'kawikiquote', 'kawikisource', 'kawiktionary', 'kbdwiki', 'kbdwiktionary', 'kbpwiki', 'kcgwiki', 'kcgwiktionary', 'kgewiki', 'kgwiki', 'kiwiki', 'kkwiki', 'kkwikibooks', 'kkwiktionary', 'klwiktionary', 'kmwiki', 'kmwikibooks', 'kmwiktionary', 'kncwiki', 'knwiki', 'knwikiquote', 'knwikisource', 'knwiktionary', 'koiwiki', 'kowiki', 'kowikibooks', 'kowikinews', 'kowikiquote', 'kowikisource', 'kowikiversity', 'kowiktionary', 'krcwiki', 'kshwiki', 'kswiki', 'kswiktionary', 'kuswiki', 'kuwiki', 'kuwikibooks', 'kuwikiquote', 'kuwiktionary', 'kvwiki', 'kwwiki', 'kwwiktionary', 'kywiki', 'kywikiquote', 'kywiktionary', 'labswiki', 'ladwiki', 'lawiki', 'lawikibooks', 'lawikiquote', 'lawikisource', 'lawiktionary', 'lbewiki', 'lbwiki', 'lbwiktionary', 'lezwiki', 'lfnwiki', 'lgwiki', 'lijwiki', 'lijwikisource', 'liwiki', 'liwikibooks', 'liwikinews', 'liwikiquote', 'liwikisource', 'liwiktionary', 'lldwiki', 'lmowiki', 'lmowiktionary', 'lnwiki', 'lnwiktionary', 'lowiki', 'lowiktionary', 'ltgwiki', 'ltwiki', 'ltwikibooks', 'ltwikiquote', 'ltwikisource', 'ltwiktionary', 'lvwiki', 'lvwiktionary', 'madwiki', 'madwikisource', 'madwiktionary', 'maiwiki', 'map_bmswiki', 'mdfwiki', 'mediawikiwiki', 'metawiki', 'mgwiki', 'mgwikibooks', 'mgwiktionary', 'mhrwiki', 'minwiki', 'minwikibooks', 'minwikisource', 'minwiktionary', 'miwiki', 'miwiktionary', 'mkwiki', 'mkwikibooks', 'mkwikimedia', 'mkwikisource', 'mkwiktionary', 'mlwiki', 'mlwikibooks', 'mlwikiquote', 'mlwikisource', 'mlwiktionary', 'mniwiki', 'mniwiktionary', 'mnwiki', 'mnwiktionary', 'mnwwiki', 'mnwwiktionary', 'moswiki', 'mrjwiki', 'mrwiki', 'mrwikibooks', 'mrwikiquote', 'mrwikisource', 'mrwiktionary', 'mswiki', 'mswikibooks', 'mswikiquote', 'mswikisource', 'mswiktionary', 'mtwiki', 'mtwiktionary', 'mwlwiki', 'mxwikimedia', 'myvwiki', 'mywiki', 'mywikisource', 'mywiktionary', 'mznwiki', 'nahwiki', 'nahwiktionary', 'napwiki', 'napwikisource', 'nawiktionary', 'nds_nlwiki', 'ndswiki', 'ndswiktionary', 'newiki', 'newikibooks', 'newiktionary', 'newwiki', 'niawiki', 'niawiktionary', 'nlwiki', 'nlwikibooks', 'nlwikimedia', 'nlwikinews', 'nlwikiquote', 'nlwikisource', 'nlwikivoyage', 'nlwiktionary', 'nnwiki', 'nnwikiquote', 'nnwiktionary', 'novwiki', 'nowiki', 'nowikibooks', 'nowikimedia', 'nowikinews', 'nowikiquote', 'nowikisource', 'nowiktionary', 'nqowiki', 'nrmwiki', 'nrwiki', 'nsowiki', 'nupwiki', 'nvwiki', 'nycwikimedia', 'nywiki', 'ocwiki', 'ocwikibooks', 'ocwiktionary', 'olowiki', 'omwiki', 'omwiktionary', 'orwiki', 'orwikisource', 'orwiktionary', 'oswiki', 'outreachwiki', 'pagwiki', 'pamwiki', 'papwiki', 'pawiki', 'pawikibooks', 'pawikisource', 'pawiktionary', 'pcdwiki', 'pcmwiki', 'pcmwikiquote', 'pdcwiki', 'pflwiki', 'piwiki', 'plwiki', 'plwikibooks', 'plwikimedia', 'plwikinews', 'plwikiquote', 'plwikisource', 'plwikivoyage', 'plwiktionary', 'pmswiki', 'pmswikisource', 'pnbwiki', 'pnbwiktionary', 'pntwiki', 'pplwiki', 'pswiki', 'pswikivoyage', 'pswiktionary', 'ptwiki', 'ptwikibooks', 'ptwikimedia', 'ptwikinews', 'ptwikiquote', 'ptwikisource', 'ptwikiversity', 'ptwikivoyage', 'ptwiktionary', 'pwnwiki', 'quwiktionary', 'rkiwiki', 'rmwiki', 'rmywiki', 'rnwiki', 'roa_rupwiki', 'roa_rupwiktionary', 'roa_tarawiki', 'rowiki', 'rowikibooks', 'rowikinews', 'rowikiquote', 'rowiktionary', 'rskwiki', 'ruewiki', 'ruwiki', 'ruwikibooks', 'ruwikinews', 'ruwikiquote', 'ruwikisource', 'ruwikiversity', 'ruwikivoyage', 'ruwiktionary', 'rwwiki', 'rwwiktionary', 'sahwiki', 'sahwikiquote', 'sahwikisource', 'satwiki', 'satwiktionary', 'sawiki', 'sawikibooks', 'sawikiquote', 'sawikisource', 'sawiktionary', 'scnwiki', 'scnwiktionary', 'scowiki', 'scwiki', 'sdwiki', 'sdwiktionary', 'sewiki', 'sewikimedia', 'sgwiki', 'sgwiktionary', 'shiwiki', 'shnwiki', 'shnwikibooks', 'shnwikinews', 'shnwikivoyage', 'shnwiktionary', 'shwiki', 'shwiktionary', 'shywiktionary', 'simplewiki', 'simplewiktionary', 'siwiki', 'siwikibooks', 'siwiktionary', 'skwiki', 'skrwiki', 'skrwiktionary', 'slwiki', 'slwikibooks', 'slwikiquote', 'slwikisource', 'slwikiversity', 'slwiktionary', 'smnwiki', 'smwiki', 'smwiktionary', 'snwiki', 'sourceswiki', 'sowiki', 'sowiktionary', 'specieswiki', 'sqwiki', 'sqwikibooks', 'sqwikinews', 'sqwikiquote', 'sqwiktionary', 'srnwiki', 'srwiki', 'srwikibooks', 'srwikinews', 'srwikiquote', 'srwikisource', 'srwiktionary', 'sswiki', 'sswiktionary', 'stqwiki', 'stwiki', 'stwiktionary', 'suwiki', 'suwikiquote', 'suwikisource', 'suwiktionary', 'svwiki', 'svwikibooks', 'svwikinews', 'svwikiquote', 'svwikisource', 'svwikiversity', 'svwikivoyage', 'svwiktionary', 'swwiki', 'swwiktionary', 'sylwiki', 'szlwiki', 'szywiki', 'tawiki', 'tawikibooks', 'tawikinews', 'tawikiquote', 'tawikisource', 'tawiktionary', 'taywiki', 'tcywiki', 'tcywikisource', 'tcywiktionary', 'tddwiki', 'tewiki', 'tewikibooks', 'tewikiquote', 'tewikisource', 'tewiktionary', 'tgwiki', 'tgwikibooks', 'tgwiktionary', 'thwiki', 'thwikibooks', 'thwikimedia', 'thwikiquote', 'thwikisource', 'thwiktionary', 'tigwiki', 'tiwiki', 'tiwiktionary', 'tkwiki', 'tkwiktionary', 'tlwiki', 'tlwikibooks', 'tlwikiquote', 'tlwikisource', 'tlwiktionary', 'tlywiki', 'tnwiki', 'tnwiktionary', 'tokwiki', 'towiki', 'tpiwiki', 'tpiwiktionary', 'trvwiki', 'trwiki', 'trwikibooks', 'trwikimedia', 'trwikinews', 'trwikiquote', 'trwikisource', 'trwikivoyage', 'trwiktionary', 'tswiki', 'tswiktionary', 'ttwiki', 'ttwikibooks', 'ttwikiquote', 'ttwiktionary', 'tumwiki', 'twwiki', 'twwiktionary', 'tyvwiki', 'tywiki', 'uawikimedia', 'udmwiki', 'ugwiki', 'ugwiktionary', 'ukwiki', 'ukwikibooks', 'ukwikinews', 'ukwikiquote', 'ukwikisource', 'ukwikivoyage', 'ukwiktionary', 'urwiki', 'urwikibooks', 'urwikiquote', 'urwikisource', 'urwiktionary', 'uzwiki', 'uzwikiquote', 'uzwiktionary', 'vecwiki', 'vecwikisource', 'vecwiktionary', 'vepwiki', 'vewiki', 'viwiki', 'viwikibooks', 'viwikiquote', 'viwikisource', 'viwikivoyage', 'viwiktionary', 'vlswiki', 'vowiki', 'vowiktionary', 'warwiki', 'wawiki', 'wawikisource', 'wawiktionary', 'wikidatawiki', 'wikimaniawiki', 'wowiki', 'wowiktionary', 'wuuwiki', 'xalwiki', 'xhwiki', 'xmfwiki', 'yiwiki', 'yiwikisource', 'yiwiktionary', 'yowiki', 'zawiki', 'zeawiki', 'zghwiki', 'zghwiktionary', 'zh_classicalwiki', 'zh_min_nanwiki', 'zh_min_nanwikisource', 'zh_min_nanwiktionary', 'zh_yuewiki', 'zhwiki', 'zhwikibooks', 'zhwikinews', 'zhwikiquote', 'zhwikisource', 'zhwikiversity', 'zhwikivoyage', 'zhwiktionary', 'zuwiki', 'zuwiktionary', 'test2wiki', 'testwiki'];
// Time delay between API requests to prevent rate limiting (429 errors)
const DELAY_MS = 400;
// Helper function to pause execution for a specified number of milliseconds
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Cache for global user group information to avoid redundant API calls
let userCache = {};
// Cache for historical global rights changes within the audit period
let historyCache = {};
// Helper function to handle API calls with up to 3 retries on 429 errors
async function robustCall(api, params) {
let retries = 0;
const maxRetries = 3;
// Continue attempting the API request as long as we haven't hit the maximum retry limit
while (retries < maxRetries) {
try {
// Attempt to execute the API GET request
return await api.get(params);
} catch (err) {
// If the error is "429 Too Many Requests" (rate limit)
if (err?.status === 429) {
retries++;
// Get wait time from 'Retry-After' header, or use a default backoff (30s, 60s, 90s)
const retryAfter = parseInt(err.xhr?.getResponseHeader('Retry-After')) || (30 * retries);
// Update UI status message with the remaining wait time
$('#status-msg').html(
`<span style="color:orange; font-weight:bold;">Rate limit hit! Resting ${retryAfter}s... (Attempt ${retries}/${maxRetries})</span>`
);
// Pause execution before the next retry
await sleep(retryAfter * 1000);
} else {
// Rethrow any error that is not a rate limit (e.g., 404, 403, 500)
throw err;
}
}
}
// Fail if max retries are exceeded
throw new Error("Max retries reached");
}
// Converts a database name (like 'enwiki') into a real web address.
function getDomain(db) {
// Static mapping for projects with non-standard naming conventions
const mapping = {
'commonswiki': 'commons.wikimedia.org',
'metawiki': 'meta.wikimedia.org',
'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org',
'sourceswiki': 'wikisource.org',
'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org',
'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org',
'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org',
'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org',
'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org',
'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
// Return early if an explicit mapping exists
if (mapping[db]) return mapping[db];
// Format language codes: replace underscores with hyphens (e.g., zh_yue -> zh-yue)
const name = db.replace(/_/g, '-');
// Handle sister project families by trimming suffixes and appending correct domains
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
// Default logic for Wikipedias: remove 'wiki' suffix and append domain
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
// Generic fallback
return name + '.wikipedia.org';
}
function getInterwikiPrefix(db) {
// Mapping for special projects that use non-standard interwiki shortcuts
const specials = {
'commonswiki': 'commons',
'metawiki': 'm',
'wikidatawiki': 'd',
'mediawikiwiki': 'mw',
'specieswiki': 'wikispecies',
'foundationwiki': 'wikimedia',
'sourceswiki': 'wikisource',
};
// Return early if an explicit special prefix is defined
if (specials[db]) return specials[db];
// Format name for interwiki consistency (underscores to hyphens)
const name = db.replace(/_/g, '-');
// Logic for sister project families: trim suffixes and append the standard shorthand
if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10);
if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11);
if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10);
if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10);
if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9);
if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9);
if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8);
if (name.endsWith('wikimedia')) return 'wikimedia:' + name.slice(0, -9);
// Standard Wikipedia logic: removes 'wiki' suffix and adds 'w:' prefix
if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4);
// Default fallback for any other database patterns
return 'w:' + name;
}
// Ensure required MediaWiki API and ForeignApi modules are loaded before execution
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function () {
// Initialize API connection to Meta-Wiki for global user information and rights logs
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
// Capture current date/time for report generation and timestamping
const now = new Date();
// State management variables:
let results = {}, // Stores aggregated CU action counts per wiki/user
emptyWikis = [], // List of projects with no recorded CU actions
failedWikis = [], // List of projects that encountered API errors
scannedWikis = [], // Tracking list of successfully processed projects
isRunning = false; // Flag to control the start/stop state of the audit
// UI state variables for filtering wikis and user roles
let currentFilterMode = 'all';
let currentUserFilter = 'all';
// Initializes and builds the User Interface (UI) elements on the page.
function setupUI() {
// Get the current year to ensure the date range is always up-to-date
const currentYear = now.getFullYear();
// Generate HTML options for the year selection (starting from 2005 to current)
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
// Prepare HTML options for month selection (1-12)
let monthOptsFrom = '';
let monthOptsTo = '';
for (let m = 1; m <= 12; m++) {
// Set default: 'From' dropdown defaults to January (1)
monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`;
// Set default: 'To' dropdown defaults to December (12)
monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`;
}
// Set the page title to identify the script
$('#firstHeading').text('GlobalCheckUserStats.js');
// Clear the main content area and append the control panel HTML
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Stats Range</h3>
From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>Wiki Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-all" checked> All wikis</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-except"> All wikis except</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-only"> Only these wikis</label>
<span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold; font-size:12px; line-height:18px;" title="Show all wiki DB names">?</span>
</div>
<div id="filter-input-container" style="display:none; margin-top:10px;">
<input id="wiki-filter" type="text" style="width:100%; max-width:600px;" placeholder="">
</div>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>User Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-all" checked> All users</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-local"> Only Local CUs</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-steward"> Only Stewards</label>
</div>
<div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace; color:#202122;">
<strong>Available Wikis (alphabetical):</strong><br>${[...rawWikis].sort().join(', ')}
</div>
<div style="margin-top:20px;">
<button id="start" class="mw-ui-button mw-ui-progressive">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
</div>
<div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div>
<div style="margin-top:5px;"><progress id="bar" value="0" max="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
// Handle clicking 'All wikis': resets filter mode and hides the input field
$('#btn-all').click(() => {
currentFilterMode = 'all';
$('#filter-input-container').hide();
});
// Handle clicking 'All wikis except': shows input field, sets mode to 'exclude', and focuses the box
$('#btn-except').click(() => {
currentFilterMode = 'exclude';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Exclude (dbname1, dbname2)...').focus();
});
// Handle clicking 'Only these wikis': shows input field, sets mode to 'include', and focuses the box
$('#btn-only').click(() => {
currentFilterMode = 'include';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Only (dbname1, dbname2)...').focus();
});
// User role filter buttons: define which user categories will appear in the final table
$('#u-all').click(() => { currentUserFilter = 'all'; });
$('#u-local').click(() => { currentUserFilter = 'local'; });
$('#u-steward').click(() => { currentUserFilter = 'steward'; });
// Toggle visibility of the 'Available Wikis' help list when the question mark icon is clicked
$('#wiki-help-trigger').click(() => $('#wiki-list-help').toggle());
// Main execution trigger: starts the audit using the selected date range from dropdowns
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
// Stop button: sets isRunning to false to halt the loop between wiki scans
$('#stop').click(() => isRunning = false);
}
// Fetches general statistics for a specific wiki, primarily to get the active user count.
async function fetchWikiMetrics(db) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
// Request site statistics via the 'siteinfo' meta module
const res = await robustCall(api, {
action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2
});
// Extract the active users count; default to 0 if the data is missing
return { active: res.query.statistics.activeusers || 0 };
} catch (e) {
// Graceful fallback: return zero if the project is unreachable or API fails
return { active: 0 };
}
}
// Checks if a user held global roles (Steward, Staff, Ombuds) during the audit period.
async function checkGlobalHistory(user, start, end) {
try {
// Fetch global rights logs from Meta-Wiki for the specific user
const res = await robustCall(metaApi, {
action: 'query', list: 'logevents', letype: 'gblrights',
letitle: 'User:' + user, lelimit: 'max', formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let state = { steward: false, staff: false, ombuds: false };
// Look at the last log entry BEFORE the audit started to see the initial status
const priorLogs = logs
.filter(l => new Date(l.timestamp) <= auditStart)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (priorLogs.length > 0) {
const p = priorLogs[0].params || {};
const groupsInLog = p.newGroups || p[1] || [];
state.steward = groupsInLog.includes('steward');
state.staff = groupsInLog.includes('staff');
state.ombuds = groupsInLog.includes('ombuds');
}
// Keep track of any global roles gained during the audit period itself
let held = { wasSteward: state.steward, wasStaff: state.staff, wasOmbuds: state.ombuds };
logs
.filter(l => {
const ts = new Date(l.timestamp);
return ts > auditStart && ts < auditEnd;
})
.forEach(e => {
const p = e.params || {};
const added = p.newGroups || p[1] || [];
if (added.includes('steward')) held.wasSteward = true;
if (added.includes('staff')) held.wasStaff = true;
if (added.includes('ombuds')) held.wasOmbuds = true;
});
return held;
} catch (e) {
// If anything fails, assume no global roles were held
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
// Gathers all necessary data for a specific user on a specific wiki.
async function fetchUserData(user, db, start, end) {
// Convert start and end strings into Date objects for easier comparison
const auditStart = new Date(start);
const auditEnd = new Date(end);
// 1. FETCH CURRENT GLOBAL GROUPS (Using cache to save time)
if (!userCache[user]) {
try {
// Ask Meta-Wiki for the user's current global groups (steward, staff, etc.)
const gres = await robustCall(metaApi, {
action: 'query', meta: 'globaluserinfo', guiprop: 'groups',
guiuser: user, formatversion: 2
});
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch (e) {
// Fallback to an empty list if the request fails
userCache[user] = [];
}
}
// Check if the user currently holds any key global roles
const gGroups = userCache[user];
const isGloballySteward = gGroups.includes('steward');
const isGloballyStaff = gGroups.includes('staff');
const isGloballyOmbuds = gGroups.includes('ombuds') || gGroups.includes('ombudsman');
// 2. FETCH GLOBAL HISTORY (Identify roles held during the period but lost since)
if (!historyCache[user]) {
historyCache[user] = await checkGlobalHistory(user, start, end);
}
const historyRes = historyCache[user];
// 3. CHECK CURRENT LOCAL CHECKUSER STATUS
let isCurrentLocal = false;
try {
// Connect to the local wiki to see if the user is currently a CheckUser
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await robustCall(localApi, {
action: 'query', list: 'users', ususers: user,
usprop: 'groups', formatversion: 2
});
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch (e) {
// Ignore errors; defaults to false
}
// Define the target string for rights log searching
const target = 'User:' + user + '@' + db;
// Initialize variables for log analysis and role calculation
let logText = '',
addTime = null,
removeTime = null,
isSelfAssign = false,
maxDurationMins = -1,
longestTimeStr = "",
assignCount = 0,
hasLocalRightsInPeriod = false;
// 4. DEEP RIGHTS LOG SCAN (Pagination to bypass 500-limit)
let events = [];
let continueToken = null;
let finished = false;
try {
while (!finished) {
let params = {
action: 'query', list: 'logevents', letype: 'rights',
letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
const res = await robustCall(metaApi, params);
const batch = res.query.logevents || [];
events = events.concat(batch);
if (res.continue) {
continueToken = res.continue;
} else {
finished = true;
}
}
let logEntries = [];
let pendingRemoved = null;
let lastPairedDate = null;
for (let i = 0; i < events.length; i++) {
const e = events[i];
const p = e.params || {};
// Clean the actor name by removing project prefixes (e.g., "metawiki>User" becomes "User")
const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user;
// Check if checkuser group actually changed state
const hadCU = (p.oldgroups || []).includes('checkuser');
const hasCU = (p.newgroups || []).includes('checkuser');
const addedInList = (p.add || []).includes('checkuser');
const removedInList = (p.remove || []).includes('checkuser');
// Detect if an expiry was added to a previously permanent group
let expiryAddedLater = false;
if (hasCU && hadCU && p.newmetadata && p.oldmetadata) {
const oldM = p.oldmetadata.find(m => m.group === 'checkuser');
const newM = p.newmetadata.find(m => m.group === 'checkuser');
if (oldM && newM && oldM.expiry === 'infinity' && newM.expiry !== 'infinity') {
expiryAddedLater = true;
}
}
// Define if rights were gained or lost in this specific log entry
let cuAdded = addedInList || (hasCU && !hadCU) || expiryAddedLater;
const cuRemoved = removedInList || (hadCU && !hasCU);
if (cuAdded || cuRemoved) {
const eventDate = new Date(e.timestamp);
const exactTime = e.timestamp.replace('T', ' ').replace('Z', '');
if (cuAdded) addTime = eventDate;
if (cuRemoved) removeTime = eventDate;
// Track if user held rights at any point during the audit period
if (addTime && addTime <= auditEnd && (!removeTime || removeTime >= auditStart)) {
hasLocalRightsInPeriod = true;
}
const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd);
// Process events within period, or paired events (Deep Scan)
if ((isInPeriod && (cuAdded || cuRemoved)) || (pendingRemoved && cuAdded)) {
let expiryDate = null;
if (p.newmetadata) {
const cuMeta = p.newmetadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') {
expiryDate = new Date(cuMeta.expiry);
}
}
// Check if the action was performed by the user themselves
const eventBySelf = (actor === user || !actor);
if (cuAdded) {
if (pendingRemoved || expiryDate) {
const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate;
const diffMs = Math.abs(checkDate - eventDate);
const totalSecs = Math.floor(diffMs / 1000);
let durStr = "";
// Format duration: show seconds if under 1 minute, otherwise use D H M
if (totalSecs < 60) {
durStr = totalSecs + "s";
} else {
const diffMins = Math.floor(totalSecs / 60);
const d = Math.floor(diffMins / 1440);
const h = Math.floor((diffMins % 1440) / 60);
const m = diffMins % 60;
durStr = `${d > 0 ? d + 'd ' : ''}${h > 0 ? h + 'h ' : ''}${m}m`;
}
if (pendingRemoved) {
// Mark as self-assign if both ADD and REMOVE were performed by the user
if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true;
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user} (Duration: ${durStr})`);
pendingRemoved = null;
} else {
if (eventBySelf) isSelfAssign = true;
const exactExpiryTime = expiryDate.toISOString().replace('T', ' ').substring(0, 19);
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | EXPIRED: ${exactExpiryTime} (Duration: ${durStr})`);
}
// Update max duration for role classification
const durationForComparison = totalSecs / 60;
if (durationForComparison > maxDurationMins) {
maxDurationMins = durationForComparison;
longestTimeStr = durStr;
}
assignCount++;
lastPairedDate = eventDate;
} else {
// Handle cases where group is still active
if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) {
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`);
lastPairedDate = eventDate;
}
}
} else if (cuRemoved) {
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf };
}
}
}
}
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
if (logEntries.length) logText = '\n' + logEntries.join('\n');
// 5. ROLE CLASSIFICATION
let roles = [];
const isTemporaryMission = (longestTimeStr && isSelfAssign && (isGloballySteward || historyRes.wasSteward));
// 1. WMF Staff status
if (isGloballyStaff || historyRes.wasStaff) {
roles.push(isGloballyStaff ? "Current Staff" : "Former Staff (in period)");
}
// 2. Steward status
if (isTemporaryMission) {
const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : "";
roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`);
} else if (isGloballySteward || historyRes.wasSteward) {
roles.push(isGloballySteward ? "Current Steward" : "Former Steward (in period)");
}
// 3. Ombudsman status
if (isGloballyOmbuds || historyRes.wasOmbuds) {
roles.push(isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)");
}
// 4. Local CheckUser status
if (isCurrentLocal) {
roles.push("Current Local CheckUser");
}
// Strict check: Only show Former Local CU if they actually HELD the group in the past
// (hasLocalRightsInPeriod = true from rights log), NOT just because they have actions.
if (!isCurrentLocal && hasLocalRightsInPeriod && !isTemporaryMission) {
roles.push("Former Local CheckUser (in period)");
}
let uniqueRoles = [...new Set(roles)];
let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role";
return { role: roleLabel, log: logText };
}
catch (err) {
return { role: "Error fetching data", log: "" };
}
}
// The main engine that iterates through wikis...
// The main engine that iterates through wikis and collects CheckUser logs.
async function runAudit(yf, mf, yt, mt) {
// Reset all data and state variables for a fresh run
isRunning = true;
userCache = {};
historyCache = {};
results = {};
emptyWikis = [];
failedWikis = [];
scannedWikis = [];
// Parse and prepare the wiki filter list from the UI input
const filterText = $('#wiki-filter').val().trim().toLowerCase();
const filterList = filterText
? filterText.split(',').map(s => s.trim()).filter(s => s !== "")
: [];
// Apply include/exclude logic to the master wiki list
let wikisToScan = rawWikis;
if (currentFilterMode === 'include') {
wikisToScan = rawWikis.filter(w => filterList.includes(w));
} else if (currentFilterMode === 'exclude') {
wikisToScan = rawWikis.filter(w => !filterList.includes(w));
}
if (wikisToScan.length === 0) {
alert("No wikis selected!");
return;
}
// Update UI buttons and progress bar to "running" state
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
$('#bar').attr('max', wikisToScan.length).val(0);
// Format the date strings into ISO format for the MediaWiki API
const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`;
const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`;
// Generate the columns for the table based on the selected month range
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
let endY = parseInt(yt), endM = parseInt(mt);
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}`
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
// Loop through each selected wiki and fetch its logs
for (let i = 0; i < wikisToScan.length; i++) {
if (!isRunning) break; // Exit loop if the "Stop" button was clicked
const db = wikisToScan[i];
scannedWikis.push(db);
$('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`);
let successLocal = false;
let continueToken = null;
// Inner loop for handling pagination of the logs on a single wiki
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query', list: 'checkuserlog',
culfrom: START, culto: END, culdir: 'newer',
cullimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
let res = await robustCall(api, params);
// Process each entry in the log batch
const ent = res.query?.checkuserlog?.entries || [];
if (ent.length) {
if (!results[db]) results[db] = {};
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const mKey = e.timestamp.slice(0, 7);
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Check if there are more log pages to fetch
if (res.continue && isRunning) {
continueToken = res.continue;
} else {
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
// Record failures (like 429 after retries or 404) and move to the next wiki
failedWikis.push(db);
successLocal = true;
}
}
// Update the progress bar and wait a bit to be kind to the API
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// Update UI and prepare the Wikitable header and metadata
$('#status-msg').text(`Generating report...`);
const today = new Date().toISOString().split('T')[0];
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' (UTC)';
// Start building the Wikitext report
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n''Report generated on: ${timestamp}''\n\n`;
let rightsLog = `\n== Rights Log (In Period) ==\n`;
// Create table structure with dynamic month columns
wt += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n`;
wt += `! Wiki / User !! Category !! '''Total''' !! per 1k active users ${monthCols.map(m => `!! ${m.label}`).join(' ')}\n`;
// Sort databases alphabetically and begin processing each project's results
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
// Get active user count; if API fails or is 0, display 'Unknown'
const metrics = await fetchWikiMetrics(db);
const fmtActive = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown';
// Prepare interwiki links for the project and its CheckUser log
const iwPrefix = getInterwikiPrefix(db);
const projectLink = `[[${iwPrefix}:|${db}]]`;
const logLink = `[[${iwPrefix}:Special:CheckUserLog|(log)]]`;
wt += `|-\n! colspan="${4 + monthCols.length}" style="background:#eaecf0; text-align:center;" | `;
wt += `${projectLink} <small style="font-weight:normal; color:#54595d;">(Active Users as of ${today}: ${fmtActive})</small> — ${logLink}\n`;
// Initialize local wiki totals and storage for individual user rows
let wikiRows = [];
let wT = 0;
let wMS = {};
monthCols.forEach(col => wMS[col.key] = 0);
// Get users from CU logs AND users who had rights changes (if we tracked them)
// For now, let's at least make sure we don't skip users with 0 actions but active rights
let projectUsers = Object.keys(results[db] || {});
projectUsers.sort();
for (const user of projectUsers) {
// Get role and rights log details for each user
const m = await fetchUserData(user, db, START, END);
// Identify if user belongs to specific global or local groups
const isSteward = m.role.includes("Steward") || m.role.includes("Staff") || m.role.includes("Ombudsman");
const isLocalCU = m.role.includes("Local CheckUser");
// Filter out users if they don't match the selected User Filter (Local/Steward)
if (currentUserFilter === 'local' && !isLocalCU) continue;
if (currentUserFilter === 'steward' && !isSteward) continue;
const uD = results[db][user];
// Aggregate totals for the current wiki
wT += uD.total;
monthCols.forEach(col => wMS[col.key] += (uD.months[col.key] || 0));
// Calculate actions per 1k active users; display 0.0 if project activity is unknown
let uP1kU = metrics.active > 0 ? ((uD.total / metrics.active) * 1000).toFixed(1) : "0.0";
// Build the Wikitext row for the individual user
let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${m.role}</small> || '''${uD.total}''' || ${uP1kU}`;
monthCols.forEach(col => row += ` || ${uD.months[col.key] || 0}`);
// Store the formatted row and its corresponding rights log
wikiRows.push({
html: row + "\n",
log: m.log ? `'''${user}@${db}''':${m.log}\n\n` : ""
});
}
// Create the bold TOTAL row for the current wiki
let wP1kU = metrics.active > 0 ? ((wT / metrics.active) * 1000).toFixed(1) : "0.0";
let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wT} || ${wP1kU}`;
monthCols.forEach(col => totalRow += ` || ${wMS[col.key]}`);
// Append the total row first, then all individual user rows
wt += totalRow + "\n";
wikiRows.forEach(r => {
wt += r.html;
if (r.log) rightsLog += r.log;
});
}
// Append a list of wikis where no CheckUser actions were found
wt += `|}\n\n== Projects with 0 actions ==\n`;
wt += `<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`;
// Append a list of projects that failed to scan (e.g., due to API errors)
if (failedWikis.length > 0) {
wt += `\n== API Errors / Skipped Projects ==\n`;
wt += `<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`;
}
// Finalize the report with the Rights Log section
wt += rightsLog + `\n\n<references />\n`;
// Display the final output and reset UI buttons
$('#out').val(wt).show();
$('#status-msg').text(`Done.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
// Initialize the interface on page load
setupUI();
});
})();
jsc6tdczzwsblsrzoinc0jxcfliuynu
738069
738066
2026-04-15T19:00:05Z
MrJaroslavik
44012
e
738069
javascript
text/javascript
// GlobalCheckUserStats.js
// -------------------------------------------------------
// Features:
// - Global Stats: Scans logs across 800+ projects at once.
// - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles.
// - Deep Scan: Bypasses API limits to get full history (limit 500 entries).
// - Stable: Retries 3x on rate limits so no wiki is missed.
// - Error Logs: Lists failed projects (ex. because of 429) at the end of the report.
// - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]].
// - Full Reporting: Outputs sortable Wikitables and detailed rights change logs.
// - With help of Gemini 3
// -------------------------------------------------------
// Wrap everything so this script doesn't break other Wikipedia scripts
(function () {
'use strict';
// Stop immediately if we are NOT on the specific GlobalCheckUserStats page
if (
mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' ||
mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1
) return;
// A fixed list of all valid Wikimedia projects to scan
const rawWikis = ['abstractwiki', 'abwiki', 'acewiki', 'adywiki', 'afwiki', 'afwikibooks', 'afwikiquote', 'afwiktionary', 'alswiki', 'altwiki', 'amiwiki', 'amwiki', 'amwiktionary', 'angwiki', 'angwiktionary', 'annwiki', 'anpwiki', 'anwiki', 'anwiktionary', 'arcwiki', 'arwiki', 'arwikibooks', 'arwikimedia', 'arwikinews', 'arwikiquote', 'arwikisource', 'arwikiversity', 'arwiktionary', 'arywiki', 'arzwiki', 'astwiki', 'astwiktionary', 'aswiki', 'aswikiquote', 'aswikisource', 'atjwiki', 'avkwiki', 'avwiki', 'awawiki', 'aywiki', 'aywiktionary', 'azbwiki', 'azwiki', 'azwikibooks', 'azwikiquote', 'azwikisource', 'azwiktionary', 'banwiki', 'banwikisource', 'barwiki', 'bat_smgwiki', 'bawiki', 'bawikibooks', 'bbcwiki', 'bclwiki', 'bclwikiquote', 'bclwikisource', 'bclwiktionary', 'bdrwiki', 'bdwikimedia', 'be_x_oldwiki', 'betawikiversity', 'bewiki', 'bewikibooks', 'bewikimedia', 'bewikiquote', 'bewikisource', 'bewiktionary', 'bewwiki', 'bewwiktionary', 'bgwiki', 'bgwikibooks', 'bgwikiquote', 'bgwikisource', 'bgwiktionary', 'bhwiki', 'biwiki', 'bjnwiki', 'bjnwikiquote', 'bjnwiktionary', 'blkwiki', 'blkwiktionary', 'bmwiki', 'bnwiki', 'bnwikibooks', 'bnwikiquote', 'bnwikisource', 'bnwikivoyage', 'bnwiktionary', 'bowiki', 'bpywiki', 'brwiki', 'brwikimedia', 'brwikiquote', 'brwikisource', 'brwiktionary', 'bswiki', 'bswikibooks', 'bswikinews', 'bswikiquote', 'bswikisource', 'bswiktionary', 'btmwiki', 'btmwiktionary', 'bugwiki', 'bxrwiki', 'cawiki', 'cawikibooks', 'cawikimedia', 'cawikinews', 'cawikiquote', 'cawikisource', 'cawiktionary', 'cbk_zamwiki', 'cdowiki', 'cebwiki', 'cewiki', 'chrwiki', 'chrwiktionary', 'chwiki', 'chywiki', 'ckbwiki', 'ckbwiktionary', 'commonswiki', 'cowiki', 'cowikimedia', 'cowiktionary', 'crhwiki', 'csbwiki', 'csbwiktionary', 'cswiki', 'cswikibooks', 'cswikinews', 'cswikiquote', 'cswikisource', 'cswikiversity', 'cswikivoyage', 'cswiktionary', 'cuwiki', 'cvwiki', 'cvwikibooks', 'cywiki', 'cywikibooks', 'cywikiquote', 'cywikisource', 'cywiktionary', 'dagwiki', 'dawiki', 'dawikibooks', 'dawikiquote', 'dawikisource', 'dawiktionary', 'dewiki', 'dewikibooks', 'dewikinews', 'dewikiquote', 'dewikisource', 'dewikiversity', 'dewikivoyage', 'dewiktionary', 'dgawiki', 'dinwiki', 'diqwiki', 'diqwiktionary', 'dkwikimedia', 'dsbwiki', 'dtpwiki', 'dtywiki', 'dvwiki', 'dvwiktionary', 'dzwiki', 'eewiki', 'elwiki', 'elwikibooks', 'elwikinews', 'elwikiquote', 'elwikisource', 'elwikiversity', 'elwikivoyage', 'elwiktionary', 'emlwiki', 'enwiki', 'enwikibooks', 'enwikinews', 'enwikiquote', 'enwikisource', 'enwikiversity', 'enwikivoyage', 'enwiktionary', 'eowiki', 'eowikibooks', 'eowikinews', 'eowikiquote', 'eowikisource', 'eowikivoyage', 'eowiktionary', 'eswiki', 'eswikibooks', 'eswikinews', 'eswikiquote', 'eswikisource', 'eswikiversity', 'eswikivoyage', 'eswiktionary', 'etwiki', 'etwikibooks', 'eewikimedia', 'etwikiquote', 'etwikisource', 'etwiktionary', 'euwiki', 'euwikibooks', 'euwikiquote', 'euwikisource', 'euwiktionary', 'extwiki', 'fatwiki', 'fawiki', 'fawikibooks', 'fawikinews', 'fawikiquote', 'fawikisource', 'fawikivoyage', 'fawiktionary', 'ffwiki', 'fiu_vrowiki', 'fiwiki', 'fiwikibooks', 'fiwikimedia', 'fiwikinews', 'fiwikiquote', 'fiwikisource', 'fiwikiversity', 'fiwikivoyage', 'fiwiktionary', 'fjwiki', 'fjwiktionary', 'fonwiki', 'foundationwiki', 'fowiki', 'fowikisource', 'fowiktionary', 'frpwiki', 'frrwiki', 'frwiki', 'frwikibooks', 'frwikinews', 'frwikiquote', 'frwikisource', 'frwikiversity', 'frwikivoyage', 'frwiktionary', 'furwiki', 'fywiki', 'fywikibooks', 'fywiktionary', 'gagwiki', 'ganwiki', 'gawiki', 'gawiktionary', 'gcrwiki', 'gdwiki', 'gdwiktionary', 'glkwiki', 'glwiki', 'glwikibooks', 'glwikiquote', 'glwikisource', 'glwiktionary', 'gnwiki', 'gnwiktionary', 'gomwiki', 'gomwiktionary', 'gorwiki', 'gorwikiquote', 'gorwiktionary', 'gotwiki', 'gpewiki', 'gucwiki', 'gurwiki', 'guwiki', 'guwikiquote', 'guwikisource', 'guwiktionary', 'guwwiki', 'guwwikinews', 'guwwikiquote', 'guwwiktionary', 'gvwiki', 'gvwiktionary', 'hakwiki', 'hawiki', 'hawiktionary', 'hawwiki', 'hewiki', 'hewikibooks', 'hewikinews', 'hewikiquote', 'hewikisource', 'hewikivoyage', 'hewiktionary', 'hifwiki', 'hifwiktionary', 'hiwiki', 'hiwikibooks', 'hiwikiquote', 'hiwikisource', 'hiwikiversity', 'hiwikivoyage', 'hiwiktionary', 'hrwiki', 'hrwikibooks', 'hrwikiquote', 'hrwikisource', 'hrwiktionary', 'hsbwiki', 'hsbwiktionary', 'htwiki', 'huwiki', 'huwikibooks', 'huwikisource', 'huwiktionary', 'hywiki', 'hywikibooks', 'hywikiquote', 'hywikisource', 'hywiktionary', 'hywwiki', 'iawiki', 'iawikibooks', 'iawiktionary', 'ibawiki', 'idwiki', 'idwikibooks', 'idwikiquote', 'idwikisource', 'idwikivoyage', 'idwiktionary', 'iewiki', 'iewiktionary', 'iglwiki', 'igwiki', 'igwikiquote', 'igwiktionary', 'ikwiki', 'ilowiki', 'incubatorwiki', 'inhwiki', 'iowiki', 'iowiktionary', 'iswiki', 'iswikibooks', 'iswikiquote', 'iswikisource', 'iswiktionary', 'itwiki', 'itwikibooks', 'itwikinews', 'itwikiquote', 'itwikisource', 'itwikiversity', 'itwikivoyage', 'itwiktionary', 'iuwiki', 'iuwiktionary', 'jamwiki', 'jawiki', 'jawikibooks', 'jawikinews', 'jawikisource', 'jawikiversity', 'jawikivoyage', 'jawiktionary', 'jbowiki', 'jbowiktionary', 'jvwiki', 'jvwikisource', 'jvwiktionary', 'kaawiki', 'kaawiktionary', 'kabwiki', 'kaiwiki', 'kajwiki', 'kawiki', 'kawikibooks', 'kawikiquote', 'kawikisource', 'kawiktionary', 'kbdwiki', 'kbdwiktionary', 'kbpwiki', 'kcgwiki', 'kcgwiktionary', 'kgewiki', 'kgwiki', 'kiwiki', 'kkwiki', 'kkwikibooks', 'kkwiktionary', 'klwiktionary', 'kmwiki', 'kmwikibooks', 'kmwiktionary', 'kncwiki', 'knwiki', 'knwikiquote', 'knwikisource', 'knwiktionary', 'koiwiki', 'kowiki', 'kowikibooks', 'kowikinews', 'kowikiquote', 'kowikisource', 'kowikiversity', 'kowiktionary', 'krcwiki', 'kshwiki', 'kswiki', 'kswiktionary', 'kuswiki', 'kuwiki', 'kuwikibooks', 'kuwikiquote', 'kuwiktionary', 'kvwiki', 'kwwiki', 'kwwiktionary', 'kywiki', 'kywikiquote', 'kywiktionary', 'labswiki', 'ladwiki', 'lawiki', 'lawikibooks', 'lawikiquote', 'lawikisource', 'lawiktionary', 'lbewiki', 'lbwiki', 'lbwiktionary', 'lezwiki', 'lfnwiki', 'lgwiki', 'lijwiki', 'lijwikisource', 'liwiki', 'liwikibooks', 'liwikinews', 'liwikiquote', 'liwikisource', 'liwiktionary', 'lldwiki', 'lmowiki', 'lmowiktionary', 'lnwiki', 'lnwiktionary', 'lowiki', 'lowiktionary', 'ltgwiki', 'ltwiki', 'ltwikibooks', 'ltwikiquote', 'ltwikisource', 'ltwiktionary', 'lvwiki', 'lvwiktionary', 'madwiki', 'madwikisource', 'madwiktionary', 'maiwiki', 'map_bmswiki', 'mdfwiki', 'mediawikiwiki', 'metawiki', 'mgwiki', 'mgwikibooks', 'mgwiktionary', 'mhrwiki', 'minwiki', 'minwikibooks', 'minwikisource', 'minwiktionary', 'miwiki', 'miwiktionary', 'mkwiki', 'mkwikibooks', 'mkwikimedia', 'mkwikisource', 'mkwiktionary', 'mlwiki', 'mlwikibooks', 'mlwikiquote', 'mlwikisource', 'mlwiktionary', 'mniwiki', 'mniwiktionary', 'mnwiki', 'mnwiktionary', 'mnwwiki', 'mnwwiktionary', 'moswiki', 'mrjwiki', 'mrwiki', 'mrwikibooks', 'mrwikiquote', 'mrwikisource', 'mrwiktionary', 'mswiki', 'mswikibooks', 'mswikiquote', 'mswikisource', 'mswiktionary', 'mtwiki', 'mtwiktionary', 'mwlwiki', 'mxwikimedia', 'myvwiki', 'mywiki', 'mywikisource', 'mywiktionary', 'mznwiki', 'nahwiki', 'nahwiktionary', 'napwiki', 'napwikisource', 'nawiktionary', 'nds_nlwiki', 'ndswiki', 'ndswiktionary', 'newiki', 'newikibooks', 'newiktionary', 'newwiki', 'niawiki', 'niawiktionary', 'nlwiki', 'nlwikibooks', 'nlwikimedia', 'nlwikinews', 'nlwikiquote', 'nlwikisource', 'nlwikivoyage', 'nlwiktionary', 'nnwiki', 'nnwikiquote', 'nnwiktionary', 'novwiki', 'nowiki', 'nowikibooks', 'nowikimedia', 'nowikinews', 'nowikiquote', 'nowikisource', 'nowiktionary', 'nqowiki', 'nrmwiki', 'nrwiki', 'nsowiki', 'nupwiki', 'nvwiki', 'nycwikimedia', 'nywiki', 'ocwiki', 'ocwikibooks', 'ocwiktionary', 'olowiki', 'omwiki', 'omwiktionary', 'orwiki', 'orwikisource', 'orwiktionary', 'oswiki', 'outreachwiki', 'pagwiki', 'pamwiki', 'papwiki', 'pawiki', 'pawikibooks', 'pawikisource', 'pawiktionary', 'pcdwiki', 'pcmwiki', 'pcmwikiquote', 'pdcwiki', 'pflwiki', 'piwiki', 'plwiki', 'plwikibooks', 'plwikimedia', 'plwikinews', 'plwikiquote', 'plwikisource', 'plwikivoyage', 'plwiktionary', 'pmswiki', 'pmswikisource', 'pnbwiki', 'pnbwiktionary', 'pntwiki', 'pplwiki', 'pswiki', 'pswikivoyage', 'pswiktionary', 'ptwiki', 'ptwikibooks', 'ptwikimedia', 'ptwikinews', 'ptwikiquote', 'ptwikisource', 'ptwikiversity', 'ptwikivoyage', 'ptwiktionary', 'pwnwiki', 'quwiktionary', 'rkiwiki', 'rmwiki', 'rmywiki', 'rnwiki', 'roa_rupwiki', 'roa_rupwiktionary', 'roa_tarawiki', 'rowiki', 'rowikibooks', 'rowikinews', 'rowikiquote', 'rowiktionary', 'rskwiki', 'ruewiki', 'ruwiki', 'ruwikibooks', 'ruwikinews', 'ruwikiquote', 'ruwikisource', 'ruwikiversity', 'ruwikivoyage', 'ruwiktionary', 'rwwiki', 'rwwiktionary', 'sahwiki', 'sahwikiquote', 'sahwikisource', 'satwiki', 'satwiktionary', 'sawiki', 'sawikibooks', 'sawikiquote', 'sawikisource', 'sawiktionary', 'scnwiki', 'scnwiktionary', 'scowiki', 'scwiki', 'sdwiki', 'sdwiktionary', 'sewiki', 'sewikimedia', 'sgwiki', 'sgwiktionary', 'shiwiki', 'shnwiki', 'shnwikibooks', 'shnwikinews', 'shnwikivoyage', 'shnwiktionary', 'shwiki', 'shwiktionary', 'shywiktionary', 'simplewiki', 'simplewiktionary', 'siwiki', 'siwikibooks', 'siwiktionary', 'skwiki', 'skrwiki', 'skrwiktionary', 'slwiki', 'slwikibooks', 'slwikiquote', 'slwikisource', 'slwikiversity', 'slwiktionary', 'smnwiki', 'smwiki', 'smwiktionary', 'snwiki', 'sourceswiki', 'sowiki', 'sowiktionary', 'specieswiki', 'sqwiki', 'sqwikibooks', 'sqwikinews', 'sqwikiquote', 'sqwiktionary', 'srnwiki', 'srwiki', 'srwikibooks', 'srwikinews', 'srwikiquote', 'srwikisource', 'srwiktionary', 'sswiki', 'sswiktionary', 'stqwiki', 'stwiki', 'stwiktionary', 'suwiki', 'suwikiquote', 'suwikisource', 'suwiktionary', 'svwiki', 'svwikibooks', 'svwikinews', 'svwikiquote', 'svwikisource', 'svwikiversity', 'svwikivoyage', 'svwiktionary', 'swwiki', 'swwiktionary', 'sylwiki', 'szlwiki', 'szywiki', 'tawiki', 'tawikibooks', 'tawikinews', 'tawikiquote', 'tawikisource', 'tawiktionary', 'taywiki', 'tcywiki', 'tcywikisource', 'tcywiktionary', 'tddwiki', 'tewiki', 'tewikibooks', 'tewikiquote', 'tewikisource', 'tewiktionary', 'tgwiki', 'tgwikibooks', 'tgwiktionary', 'thwiki', 'thwikibooks', 'thwikimedia', 'thwikiquote', 'thwikisource', 'thwiktionary', 'tigwiki', 'tiwiki', 'tiwiktionary', 'tkwiki', 'tkwiktionary', 'tlwiki', 'tlwikibooks', 'tlwikiquote', 'tlwikisource', 'tlwiktionary', 'tlywiki', 'tnwiki', 'tnwiktionary', 'tokwiki', 'towiki', 'tpiwiki', 'tpiwiktionary', 'trvwiki', 'trwiki', 'trwikibooks', 'trwikimedia', 'trwikinews', 'trwikiquote', 'trwikisource', 'trwikivoyage', 'trwiktionary', 'tswiki', 'tswiktionary', 'ttwiki', 'ttwikibooks', 'ttwikiquote', 'ttwiktionary', 'tumwiki', 'twwiki', 'twwiktionary', 'tyvwiki', 'tywiki', 'uawikimedia', 'udmwiki', 'ugwiki', 'ugwiktionary', 'ukwiki', 'ukwikibooks', 'ukwikinews', 'ukwikiquote', 'ukwikisource', 'ukwikivoyage', 'ukwiktionary', 'urwiki', 'urwikibooks', 'urwikiquote', 'urwikisource', 'urwiktionary', 'uzwiki', 'uzwikiquote', 'uzwiktionary', 'vecwiki', 'vecwikisource', 'vecwiktionary', 'vepwiki', 'vewiki', 'viwiki', 'viwikibooks', 'viwikiquote', 'viwikisource', 'viwikivoyage', 'viwiktionary', 'vlswiki', 'vowiki', 'vowiktionary', 'warwiki', 'wawiki', 'wawikisource', 'wawiktionary', 'wikidatawiki', 'wikimaniawiki', 'wowiki', 'wowiktionary', 'wuuwiki', 'xalwiki', 'xhwiki', 'xmfwiki', 'yiwiki', 'yiwikisource', 'yiwiktionary', 'yowiki', 'zawiki', 'zeawiki', 'zghwiki', 'zghwiktionary', 'zh_classicalwiki', 'zh_min_nanwiki', 'zh_min_nanwikisource', 'zh_min_nanwiktionary', 'zh_yuewiki', 'zhwiki', 'zhwikibooks', 'zhwikinews', 'zhwikiquote', 'zhwikisource', 'zhwikiversity', 'zhwikivoyage', 'zhwiktionary', 'zuwiki', 'zuwiktionary', 'test2wiki', 'testwiki'];
// Time delay between API requests to prevent rate limiting (429 errors)
const DELAY_MS = 400;
// Helper function to pause execution for a specified number of milliseconds
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Cache for global user group information to avoid redundant API calls
let userCache = {};
// Cache for historical global rights changes within the audit period
let historyCache = {};
// Helper function to handle API calls with up to 3 retries on 429 errors
async function robustCall(api, params) {
let retries = 0;
const maxRetries = 3;
// Continue attempting the API request as long as we haven't hit the maximum retry limit
while (retries < maxRetries) {
try {
// Attempt to execute the API GET request
return await api.get(params);
} catch (err) {
// If the error is "429 Too Many Requests" (rate limit)
if (err?.status === 429) {
retries++;
// Get wait time from 'Retry-After' header, or use a default backoff (30s, 60s, 90s)
const retryAfter = parseInt(err.xhr?.getResponseHeader('Retry-After')) || (30 * retries);
// Update UI status message with the remaining wait time
$('#status-msg').html(
`<span style="color:orange; font-weight:bold;">Rate limit hit! Resting ${retryAfter}s... (Attempt ${retries}/${maxRetries})</span>`
);
// Pause execution before the next retry
await sleep(retryAfter * 1000);
} else {
// Rethrow any error that is not a rate limit (e.g., 404, 403, 500)
throw err;
}
}
}
// Fail if max retries are exceeded
throw new Error("Max retries reached");
}
// Converts a database name (like 'enwiki') into a real web address.
function getDomain(db) {
// Static mapping for projects with non-standard naming conventions
const mapping = {
'commonswiki': 'commons.wikimedia.org',
'metawiki': 'meta.wikimedia.org',
'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org',
'sourceswiki': 'wikisource.org',
'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org',
'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org',
'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org',
'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org',
'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org',
'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
// Return early if an explicit mapping exists
if (mapping[db]) return mapping[db];
// Format language codes: replace underscores with hyphens (e.g., zh_yue -> zh-yue)
const name = db.replace(/_/g, '-');
// Handle sister project families by trimming suffixes and appending correct domains
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
// Default logic for Wikipedias: remove 'wiki' suffix and append domain
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
// Generic fallback
return name + '.wikipedia.org';
}
function getInterwikiPrefix(db) {
// Mapping for special projects that use non-standard interwiki shortcuts
const specials = {
'commonswiki': 'commons',
'metawiki': 'm',
'wikidatawiki': 'd',
'mediawikiwiki': 'mw',
'specieswiki': 'wikispecies',
'foundationwiki': 'wikimedia',
'sourceswiki': 'wikisource',
};
// Return early if an explicit special prefix is defined
if (specials[db]) return specials[db];
// Format name for interwiki consistency (underscores to hyphens)
const name = db.replace(/_/g, '-');
// Logic for sister project families: trim suffixes and append the standard shorthand
if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10);
if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11);
if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10);
if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10);
if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9);
if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9);
if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8);
if (name.endsWith('wikimedia')) return 'wikimedia:' + name.slice(0, -9);
// Standard Wikipedia logic: removes 'wiki' suffix and adds 'w:' prefix
if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4);
// Default fallback for any other database patterns
return 'w:' + name;
}
// Ensure required MediaWiki API and ForeignApi modules are loaded before execution
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function () {
// Initialize API connection to Meta-Wiki for global user information and rights logs
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
// Capture current date/time for report generation and timestamping
const now = new Date();
// State management variables:
let results = {}, // Stores aggregated CU action counts per wiki/user
emptyWikis = [], // List of projects with no recorded CU actions
failedWikis = [], // List of projects that encountered API errors
scannedWikis = [], // Tracking list of successfully processed projects
isRunning = false; // Flag to control the start/stop state of the audit
// UI state variables for filtering wikis and user roles
let currentFilterMode = 'all';
let currentUserFilter = 'all';
// Initializes and builds the User Interface (UI) elements on the page.
function setupUI() {
// Get the current year to ensure the date range is always up-to-date
const currentYear = now.getFullYear();
// Generate HTML options for the year selection (starting from 2005 to current)
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
// Prepare HTML options for month selection (1-12)
let monthOptsFrom = '';
let monthOptsTo = '';
for (let m = 1; m <= 12; m++) {
// Set default: 'From' dropdown defaults to January (1)
monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`;
// Set default: 'To' dropdown defaults to December (12)
monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`;
}
// Set the page title to identify the script
$('#firstHeading').text('GlobalCheckUserStats.js');
// Clear the main content area and append the control panel HTML
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Stats Range</h3>
From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>Wiki Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-all" checked> All wikis</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-except"> All wikis except</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-only"> Only these wikis</label>
<span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold; font-size:12px; line-height:18px;" title="Show all wiki DB names">?</span>
</div>
<div id="filter-input-container" style="display:none; margin-top:10px;">
<input id="wiki-filter" type="text" style="width:100%; max-width:600px;" placeholder="">
</div>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>User Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-all" checked> All users</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-local"> Only Local CUs</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-steward"> Only Stewards</label>
</div>
<div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace; color:#202122;">
<strong>Available Wikis (alphabetical):</strong><br>${[...rawWikis].sort().join(', ')}
</div>
<div style="margin-top:20px;">
<button id="start" class="mw-ui-button mw-ui-progressive">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
</div>
<div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div>
<div style="margin-top:5px;"><progress id="bar" value="0" max="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
// Handle clicking 'All wikis': resets filter mode and hides the input field
$('#btn-all').click(() => {
currentFilterMode = 'all';
$('#filter-input-container').hide();
});
// Handle clicking 'All wikis except': shows input field, sets mode to 'exclude', and focuses the box
$('#btn-except').click(() => {
currentFilterMode = 'exclude';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Exclude (dbname1, dbname2)...').focus();
});
// Handle clicking 'Only these wikis': shows input field, sets mode to 'include', and focuses the box
$('#btn-only').click(() => {
currentFilterMode = 'include';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Only (dbname1, dbname2)...').focus();
});
// User role filter buttons: define which user categories will appear in the final table
$('#u-all').click(() => { currentUserFilter = 'all'; });
$('#u-local').click(() => { currentUserFilter = 'local'; });
$('#u-steward').click(() => { currentUserFilter = 'steward'; });
// Toggle visibility of the 'Available Wikis' help list when the question mark icon is clicked
$('#wiki-help-trigger').click(() => $('#wiki-list-help').toggle());
// Main execution trigger: starts the audit using the selected date range from dropdowns
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
// Stop button: sets isRunning to false to halt the loop between wiki scans
$('#stop').click(() => isRunning = false);
}
// Fetches general statistics for a specific wiki, primarily to get the active user count.
async function fetchWikiMetrics(db) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
// Request site statistics via the 'siteinfo' meta module
const res = await robustCall(api, {
action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2
});
// Extract the active users count; default to 0 if the data is missing
return { active: res.query.statistics.activeusers || 0 };
} catch (e) {
// Graceful fallback: return zero if the project is unreachable or API fails
return { active: 0 };
}
}
// Checks if a user held global roles (Steward, Staff, Ombuds) during the audit period.
async function checkGlobalHistory(user, start, end) {
try {
// Fetch global rights logs from Meta-Wiki for the specific user
const res = await robustCall(metaApi, {
action: 'query', list: 'logevents', letype: 'gblrights',
letitle: 'User:' + user, lelimit: 'max', formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let state = { steward: false, staff: false, ombuds: false };
// Look at the last log entry BEFORE the audit started to see the initial status
const priorLogs = logs
.filter(l => new Date(l.timestamp) <= auditStart)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (priorLogs.length > 0) {
const p = priorLogs[0].params || {};
const groupsInLog = p.newGroups || p[1] || [];
state.steward = groupsInLog.includes('steward');
state.staff = groupsInLog.includes('staff');
state.ombuds = groupsInLog.includes('ombuds');
}
// Keep track of any global roles gained during the audit period itself
let held = { wasSteward: state.steward, wasStaff: state.staff, wasOmbuds: state.ombuds };
logs
.filter(l => {
const ts = new Date(l.timestamp);
return ts > auditStart && ts < auditEnd;
})
.forEach(e => {
const p = e.params || {};
const added = p.newGroups || p[1] || [];
if (added.includes('steward')) held.wasSteward = true;
if (added.includes('staff')) held.wasStaff = true;
if (added.includes('ombuds')) held.wasOmbuds = true;
});
return held;
} catch (e) {
// If anything fails, assume no global roles were held
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
// Gathers all necessary data for a specific user on a specific wiki.
async function fetchUserData(user, db, start, end) {
// Convert start and end strings into Date objects for easier comparison
const auditStart = new Date(start);
const auditEnd = new Date(end);
// 1. FETCH CURRENT GLOBAL GROUPS (Using cache to save time)
if (!userCache[user]) {
try {
// Ask Meta-Wiki for the user's current global groups (steward, staff, etc.)
const gres = await robustCall(metaApi, {
action: 'query', meta: 'globaluserinfo', guiprop: 'groups',
guiuser: user, formatversion: 2
});
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch (e) {
// Fallback to an empty list if the request fails
userCache[user] = [];
}
}
// Check if the user currently holds any key global roles
const gGroups = userCache[user];
const isGloballySteward = gGroups.includes('steward');
const isGloballyStaff = gGroups.includes('staff');
const isGloballyOmbuds = gGroups.includes('ombuds') || gGroups.includes('ombudsman');
// 2. FETCH GLOBAL HISTORY (Identify roles held during the period but lost since)
if (!historyCache[user]) {
historyCache[user] = await checkGlobalHistory(user, start, end);
}
const historyRes = historyCache[user];
// 3. CHECK CURRENT LOCAL CHECKUSER STATUS
let isCurrentLocal = false;
try {
// Connect to the local wiki to see if the user is currently a CheckUser
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await robustCall(localApi, {
action: 'query', list: 'users', ususers: user,
usprop: 'groups', formatversion: 2
});
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch (e) {
// Ignore errors; defaults to false
}
// Define the target string for rights log searching
const target = 'User:' + user + '@' + db;
// Initialize variables for log analysis and role calculation
let logText = '',
isSelfAssign = false,
maxDurationMins = -1,
longestTimeStr = "",
assignCount = 0,
hasLocalRightsInPeriod = false;
// 4. DEEP RIGHTS LOG SCAN (Pagination to bypass 500-limit)
let events = [];
let continueToken = null;
let finished = false;
try {
while (!finished) {
let params = {
action: 'query', list: 'logevents', letype: 'rights',
letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
const res = await robustCall(metaApi, params);
const batch = res.query.logevents || [];
events = events.concat(batch);
if (res.continue) {
continueToken = res.continue;
} else {
finished = true;
}
}
let logEntries = [];
let pendingRemoved = null;
let lastPairedDate = null;
for (let i = 0; i < events.length; i++) {
const e = events[i];
const p = e.params || {};
// Clean the actor name by removing project prefixes (e.g., "metawiki>User" becomes "User")
const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user;
// Check if checkuser group actually changed state
const hadCU = (p.oldgroups || []).includes('checkuser');
const hasCU = (p.newgroups || []).includes('checkuser');
const addedInList = (p.add || []).includes('checkuser');
const removedInList = (p.remove || []).includes('checkuser');
// Detect if an expiry was added to a previously permanent group
let expiryAddedLater = false;
if (hasCU && hadCU && p.newmetadata && p.oldmetadata) {
const oldM = p.oldmetadata.find(m => m.group === 'checkuser');
const newM = p.newmetadata.find(m => m.group === 'checkuser');
if (oldM && newM && oldM.expiry === 'infinity' && newM.expiry !== 'infinity') {
expiryAddedLater = true;
}
}
// Define if rights were gained or lost in this specific log entry
let cuAdded = addedInList || (hasCU && !hadCU) || expiryAddedLater;
const cuRemoved = removedInList || (hadCU && !hasCU);
if (cuAdded || cuRemoved) {
const eventDate = new Date(e.timestamp);
const exactTime = e.timestamp.replace('T', ' ').replace('Z', '');
// FIX: If we see ANY CU change happening within the period, user is NOT unknown
const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd);
if (isInPeriod) {
hasLocalRightsInPeriod = true;
}
// Process events within period, or paired events (Deep Scan)
if ((isInPeriod && (cuAdded || cuRemoved)) || (pendingRemoved && cuAdded)) {
let expiryDate = null;
if (p.newmetadata) {
const cuMeta = p.newmetadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') {
expiryDate = new Date(cuMeta.expiry);
}
}
// Check if the action was performed by the user themselves
const eventBySelf = (actor === user || !actor);
if (cuAdded) {
if (pendingRemoved || expiryDate) {
const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate;
const diffMs = Math.abs(checkDate - eventDate);
const totalSecs = Math.floor(diffMs / 1000);
let durStr = "";
// Format duration: show seconds if under 1 minute, otherwise use D H M
if (totalSecs < 60) {
durStr = totalSecs + "s";
} else {
const diffMins = Math.floor(totalSecs / 60);
const d = Math.floor(diffMins / 1440);
const h = Math.floor((diffMins % 1440) / 60);
const m = diffMins % 60;
durStr = `${d > 0 ? d + 'd ' : ''}${h > 0 ? h + 'h ' : ''}${m}m`;
}
if (pendingRemoved) {
// Mark as self-assign if both ADD and REMOVE were performed by the user
if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true;
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user} (Duration: ${durStr})`);
pendingRemoved = null;
} else {
if (eventBySelf) isSelfAssign = true;
const exactExpiryTime = expiryDate.toISOString().replace('T', ' ').substring(0, 19);
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | EXPIRED: ${exactExpiryTime} (Duration: ${durStr})`);
}
// Update max duration for role classification
const durationForComparison = totalSecs / 60;
if (durationForComparison > maxDurationMins) {
maxDurationMins = durationForComparison;
longestTimeStr = durStr;
}
assignCount++;
lastPairedDate = eventDate;
} else {
// Handle cases where group is still active
if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) {
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`);
lastPairedDate = eventDate;
}
}
} else if (cuRemoved) {
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf };
}
}
}
}
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
if (logEntries.length) logText = '\n' + logEntries.join('\n');
// 5. ROLE CLASSIFICATION
let roles = [];
const isTemporaryMission = (longestTimeStr && isSelfAssign && (isGloballySteward || historyRes.wasSteward));
// 1. WMF Staff status
if (isGloballyStaff || historyRes.wasStaff) {
roles.push(isGloballyStaff ? "Current Staff" : "Former Staff (in period)");
}
// 2. Steward status
if (isTemporaryMission) {
const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : "";
roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`);
} else if (isGloballySteward || historyRes.wasSteward) {
roles.push(isGloballySteward ? "Current Steward" : "Former Steward (in period)");
}
// 3. Ombudsman status
if (isGloballyOmbuds || historyRes.wasOmbuds) {
roles.push(isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)");
}
// 4. Local CheckUser status
if (isCurrentLocal) {
roles.push("Current Local CheckUser");
}
// Strict check: Only show Former Local CU if they actually HELD the group in the past
// (hasLocalRightsInPeriod = true from rights log), NOT just because they have actions.
if (!isCurrentLocal && hasLocalRightsInPeriod && !isTemporaryMission) {
roles.push("Former Local CheckUser (in period)");
}
let uniqueRoles = [...new Set(roles)];
let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role";
return { role: roleLabel, log: logText };
}
catch (err) {
return { role: "Error fetching data", log: "" };
}
}
// The main engine that iterates through wikis...
// The main engine that iterates through wikis and collects CheckUser logs.
async function runAudit(yf, mf, yt, mt) {
// Reset all data and state variables for a fresh run
isRunning = true;
userCache = {};
historyCache = {};
results = {};
emptyWikis = [];
failedWikis = [];
scannedWikis = [];
// Parse and prepare the wiki filter list from the UI input
const filterText = $('#wiki-filter').val().trim().toLowerCase();
const filterList = filterText
? filterText.split(',').map(s => s.trim()).filter(s => s !== "")
: [];
// Apply include/exclude logic to the master wiki list
let wikisToScan = rawWikis;
if (currentFilterMode === 'include') {
wikisToScan = rawWikis.filter(w => filterList.includes(w));
} else if (currentFilterMode === 'exclude') {
wikisToScan = rawWikis.filter(w => !filterList.includes(w));
}
if (wikisToScan.length === 0) {
alert("No wikis selected!");
return;
}
// Update UI buttons and progress bar to "running" state
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
$('#bar').attr('max', wikisToScan.length).val(0);
// Format the date strings into ISO format for the MediaWiki API
const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`;
const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`;
// Generate the columns for the table based on the selected month range
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
let endY = parseInt(yt), endM = parseInt(mt);
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}`
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
// Loop through each selected wiki and fetch its logs
for (let i = 0; i < wikisToScan.length; i++) {
if (!isRunning) break; // Exit loop if the "Stop" button was clicked
const db = wikisToScan[i];
scannedWikis.push(db);
$('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`);
let successLocal = false;
let continueToken = null;
// Inner loop for handling pagination of the logs on a single wiki
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query', list: 'checkuserlog',
culfrom: START, culto: END, culdir: 'newer',
cullimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
let res = await robustCall(api, params);
// Process each entry in the log batch
const ent = res.query?.checkuserlog?.entries || [];
if (ent.length) {
if (!results[db]) results[db] = {};
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const mKey = e.timestamp.slice(0, 7);
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Check if there are more log pages to fetch
if (res.continue && isRunning) {
continueToken = res.continue;
} else {
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
// Record failures (like 429 after retries or 404) and move to the next wiki
failedWikis.push(db);
successLocal = true;
}
}
// Update the progress bar and wait a bit to be kind to the API
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// Update UI and prepare the Wikitable header and metadata
$('#status-msg').text(`Generating report...`);
const today = new Date().toISOString().split('T')[0];
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' (UTC)';
// Start building the Wikitext report
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n''Report generated on: ${timestamp}''\n\n`;
let rightsLog = `\n== Rights Log (In Period) ==\n`;
// Create table structure with dynamic month columns
wt += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n`;
wt += `! Wiki / User !! Category !! '''Total''' !! per 1k active users ${monthCols.map(m => `!! ${m.label}`).join(' ')}\n`;
// Sort databases alphabetically and begin processing each project's results
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
// Get active user count; if API fails or is 0, display 'Unknown'
const metrics = await fetchWikiMetrics(db);
const fmtActive = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown';
// Prepare interwiki links for the project and its CheckUser log
const iwPrefix = getInterwikiPrefix(db);
const projectLink = `[[${iwPrefix}:|${db}]]`;
const logLink = `[[${iwPrefix}:Special:CheckUserLog|(log)]]`;
wt += `|-\n! colspan="${4 + monthCols.length}" style="background:#eaecf0; text-align:center;" | `;
wt += `${projectLink} <small style="font-weight:normal; color:#54595d;">(Active Users as of ${today}: ${fmtActive})</small> — ${logLink}\n`;
// Initialize local wiki totals and storage for individual user rows
let wikiRows = [];
let wT = 0;
let wMS = {};
monthCols.forEach(col => wMS[col.key] = 0);
// Get users from CU logs AND users who had rights changes (if we tracked them)
// For now, let's at least make sure we don't skip users with 0 actions but active rights
let projectUsers = Object.keys(results[db] || {});
projectUsers.sort();
for (const user of projectUsers) {
// Get role and rights log details for each user
const m = await fetchUserData(user, db, START, END);
// Identify if user belongs to specific global or local groups
const isSteward = m.role.includes("Steward") || m.role.includes("Staff") || m.role.includes("Ombudsman");
const isLocalCU = m.role.includes("Local CheckUser");
// Filter out users if they don't match the selected User Filter (Local/Steward)
if (currentUserFilter === 'local' && !isLocalCU) continue;
if (currentUserFilter === 'steward' && !isSteward) continue;
const uD = results[db][user];
// Aggregate totals for the current wiki
wT += uD.total;
monthCols.forEach(col => wMS[col.key] += (uD.months[col.key] || 0));
// Calculate actions per 1k active users; display 0.0 if project activity is unknown
let uP1kU = metrics.active > 0 ? ((uD.total / metrics.active) * 1000).toFixed(1) : "0.0";
// Build the Wikitext row for the individual user
let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${m.role}</small> || '''${uD.total}''' || ${uP1kU}`;
monthCols.forEach(col => row += ` || ${uD.months[col.key] || 0}`);
// Store the formatted row and its corresponding rights log
wikiRows.push({
html: row + "\n",
log: m.log ? `'''${user}@${db}''':${m.log}\n\n` : ""
});
}
// Create the bold TOTAL row for the current wiki
let wP1kU = metrics.active > 0 ? ((wT / metrics.active) * 1000).toFixed(1) : "0.0";
let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wT} || ${wP1kU}`;
monthCols.forEach(col => totalRow += ` || ${wMS[col.key]}`);
// Append the total row first, then all individual user rows
wt += totalRow + "\n";
wikiRows.forEach(r => {
wt += r.html;
if (r.log) rightsLog += r.log;
});
}
// Append a list of wikis where no CheckUser actions were found
wt += `|}\n\n== Projects with 0 actions ==\n`;
wt += `<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`;
// Append a list of projects that failed to scan (e.g., due to API errors)
if (failedWikis.length > 0) {
wt += `\n== API Errors / Skipped Projects ==\n`;
wt += `<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`;
}
// Finalize the report with the Rights Log section
wt += rightsLog + `\n\n<references />\n`;
// Display the final output and reset UI buttons
$('#out').val(wt).show();
$('#status-msg').text(`Done.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
// Initialize the interface on page load
setupUI();
});
})();
ngmo67kh1zidtnes32owq6bubtgoa7v
738073
738069
2026-04-15T19:52:00Z
MrJaroslavik
44012
e
738073
javascript
text/javascript
// GlobalCheckUserStats.js
// -------------------------------------------------------
// Features:
// - Global Stats: Scans logs across 800+ projects at once.
// - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles.
// - Deep Scan: Bypasses API limits to get full history (limit 500 entries).
// - Stable: Retries 3x on rate limits so no wiki is missed.
// - Error Logs: Lists failed projects (ex. because of 429) at the end of the report.
// - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]].
// - Full Reporting: Outputs sortable Wikitables and detailed rights change logs.
// - With help of Gemini 3
// -------------------------------------------------------
// Wrap everything so this script doesn't break other Wikipedia scripts
(function () {
'use strict';
// Stop immediately if we are NOT on the specific GlobalCheckUserStats page
if (
mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' ||
mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1
) return;
// A fixed list of all valid Wikimedia projects to scan
const rawWikis = ['abstractwiki', 'abwiki', 'acewiki', 'adywiki', 'afwiki', 'afwikibooks', 'afwikiquote', 'afwiktionary', 'alswiki', 'altwiki', 'amiwiki', 'amwiki', 'amwiktionary', 'angwiki', 'angwiktionary', 'annwiki', 'anpwiki', 'anwiki', 'anwiktionary', 'arcwiki', 'arwiki', 'arwikibooks', 'arwikimedia', 'arwikinews', 'arwikiquote', 'arwikisource', 'arwikiversity', 'arwiktionary', 'arywiki', 'arzwiki', 'astwiki', 'astwiktionary', 'aswiki', 'aswikiquote', 'aswikisource', 'atjwiki', 'avkwiki', 'avwiki', 'awawiki', 'aywiki', 'aywiktionary', 'azbwiki', 'azwiki', 'azwikibooks', 'azwikiquote', 'azwikisource', 'azwiktionary', 'banwiki', 'banwikisource', 'barwiki', 'bat_smgwiki', 'bawiki', 'bawikibooks', 'bbcwiki', 'bclwiki', 'bclwikiquote', 'bclwikisource', 'bclwiktionary', 'bdrwiki', 'bdwikimedia', 'be_x_oldwiki', 'betawikiversity', 'bewiki', 'bewikibooks', 'bewikimedia', 'bewikiquote', 'bewikisource', 'bewiktionary', 'bewwiki', 'bewwiktionary', 'bgwiki', 'bgwikibooks', 'bgwikiquote', 'bgwikisource', 'bgwiktionary', 'bhwiki', 'biwiki', 'bjnwiki', 'bjnwikiquote', 'bjnwiktionary', 'blkwiki', 'blkwiktionary', 'bmwiki', 'bnwiki', 'bnwikibooks', 'bnwikiquote', 'bnwikisource', 'bnwikivoyage', 'bnwiktionary', 'bowiki', 'bpywiki', 'brwiki', 'brwikimedia', 'brwikiquote', 'brwikisource', 'brwiktionary', 'bswiki', 'bswikibooks', 'bswikinews', 'bswikiquote', 'bswikisource', 'bswiktionary', 'btmwiki', 'btmwiktionary', 'bugwiki', 'bxrwiki', 'cawiki', 'cawikibooks', 'cawikimedia', 'cawikinews', 'cawikiquote', 'cawikisource', 'cawiktionary', 'cbk_zamwiki', 'cdowiki', 'cebwiki', 'cewiki', 'chrwiki', 'chrwiktionary', 'chwiki', 'chywiki', 'ckbwiki', 'ckbwiktionary', 'commonswiki', 'cowiki', 'cowikimedia', 'cowiktionary', 'crhwiki', 'csbwiki', 'csbwiktionary', 'cswiki', 'cswikibooks', 'cswikinews', 'cswikiquote', 'cswikisource', 'cswikiversity', 'cswikivoyage', 'cswiktionary', 'cuwiki', 'cvwiki', 'cvwikibooks', 'cywiki', 'cywikibooks', 'cywikiquote', 'cywikisource', 'cywiktionary', 'dagwiki', 'dawiki', 'dawikibooks', 'dawikiquote', 'dawikisource', 'dawiktionary', 'dewiki', 'dewikibooks', 'dewikinews', 'dewikiquote', 'dewikisource', 'dewikiversity', 'dewikivoyage', 'dewiktionary', 'dgawiki', 'dinwiki', 'diqwiki', 'diqwiktionary', 'dkwikimedia', 'dsbwiki', 'dtpwiki', 'dtywiki', 'dvwiki', 'dvwiktionary', 'dzwiki', 'eewiki', 'elwiki', 'elwikibooks', 'elwikinews', 'elwikiquote', 'elwikisource', 'elwikiversity', 'elwikivoyage', 'elwiktionary', 'emlwiki', 'enwiki', 'enwikibooks', 'enwikinews', 'enwikiquote', 'enwikisource', 'enwikiversity', 'enwikivoyage', 'enwiktionary', 'eowiki', 'eowikibooks', 'eowikinews', 'eowikiquote', 'eowikisource', 'eowikivoyage', 'eowiktionary', 'eswiki', 'eswikibooks', 'eswikinews', 'eswikiquote', 'eswikisource', 'eswikiversity', 'eswikivoyage', 'eswiktionary', 'etwiki', 'etwikibooks', 'eewikimedia', 'etwikiquote', 'etwikisource', 'etwiktionary', 'euwiki', 'euwikibooks', 'euwikiquote', 'euwikisource', 'euwiktionary', 'extwiki', 'fatwiki', 'fawiki', 'fawikibooks', 'fawikinews', 'fawikiquote', 'fawikisource', 'fawikivoyage', 'fawiktionary', 'ffwiki', 'fiu_vrowiki', 'fiwiki', 'fiwikibooks', 'fiwikimedia', 'fiwikinews', 'fiwikiquote', 'fiwikisource', 'fiwikiversity', 'fiwikivoyage', 'fiwiktionary', 'fjwiki', 'fjwiktionary', 'fonwiki', 'foundationwiki', 'fowiki', 'fowikisource', 'fowiktionary', 'frpwiki', 'frrwiki', 'frwiki', 'frwikibooks', 'frwikinews', 'frwikiquote', 'frwikisource', 'frwikiversity', 'frwikivoyage', 'frwiktionary', 'furwiki', 'fywiki', 'fywikibooks', 'fywiktionary', 'gagwiki', 'ganwiki', 'gawiki', 'gawiktionary', 'gcrwiki', 'gdwiki', 'gdwiktionary', 'glkwiki', 'glwiki', 'glwikibooks', 'glwikiquote', 'glwikisource', 'glwiktionary', 'gnwiki', 'gnwiktionary', 'gomwiki', 'gomwiktionary', 'gorwiki', 'gorwikiquote', 'gorwiktionary', 'gotwiki', 'gpewiki', 'gucwiki', 'gurwiki', 'guwiki', 'guwikiquote', 'guwikisource', 'guwiktionary', 'guwwiki', 'guwwikinews', 'guwwikiquote', 'guwwiktionary', 'gvwiki', 'gvwiktionary', 'hakwiki', 'hawiki', 'hawiktionary', 'hawwiki', 'hewiki', 'hewikibooks', 'hewikinews', 'hewikiquote', 'hewikisource', 'hewikivoyage', 'hewiktionary', 'hifwiki', 'hifwiktionary', 'hiwiki', 'hiwikibooks', 'hiwikiquote', 'hiwikisource', 'hiwikiversity', 'hiwikivoyage', 'hiwiktionary', 'hrwiki', 'hrwikibooks', 'hrwikiquote', 'hrwikisource', 'hrwiktionary', 'hsbwiki', 'hsbwiktionary', 'htwiki', 'huwiki', 'huwikibooks', 'huwikisource', 'huwiktionary', 'hywiki', 'hywikibooks', 'hywikiquote', 'hywikisource', 'hywiktionary', 'hywwiki', 'iawiki', 'iawikibooks', 'iawiktionary', 'ibawiki', 'idwiki', 'idwikibooks', 'idwikiquote', 'idwikisource', 'idwikivoyage', 'idwiktionary', 'iewiki', 'iewiktionary', 'iglwiki', 'igwiki', 'igwikiquote', 'igwiktionary', 'ikwiki', 'ilowiki', 'incubatorwiki', 'inhwiki', 'iowiki', 'iowiktionary', 'iswiki', 'iswikibooks', 'iswikiquote', 'iswikisource', 'iswiktionary', 'itwiki', 'itwikibooks', 'itwikinews', 'itwikiquote', 'itwikisource', 'itwikiversity', 'itwikivoyage', 'itwiktionary', 'iuwiki', 'iuwiktionary', 'jamwiki', 'jawiki', 'jawikibooks', 'jawikinews', 'jawikisource', 'jawikiversity', 'jawikivoyage', 'jawiktionary', 'jbowiki', 'jbowiktionary', 'jvwiki', 'jvwikisource', 'jvwiktionary', 'kaawiki', 'kaawiktionary', 'kabwiki', 'kaiwiki', 'kajwiki', 'kawiki', 'kawikibooks', 'kawikiquote', 'kawikisource', 'kawiktionary', 'kbdwiki', 'kbdwiktionary', 'kbpwiki', 'kcgwiki', 'kcgwiktionary', 'kgewiki', 'kgwiki', 'kiwiki', 'kkwiki', 'kkwikibooks', 'kkwiktionary', 'klwiktionary', 'kmwiki', 'kmwikibooks', 'kmwiktionary', 'kncwiki', 'knwiki', 'knwikiquote', 'knwikisource', 'knwiktionary', 'koiwiki', 'kowiki', 'kowikibooks', 'kowikinews', 'kowikiquote', 'kowikisource', 'kowikiversity', 'kowiktionary', 'krcwiki', 'kshwiki', 'kswiki', 'kswiktionary', 'kuswiki', 'kuwiki', 'kuwikibooks', 'kuwikiquote', 'kuwiktionary', 'kvwiki', 'kwwiki', 'kwwiktionary', 'kywiki', 'kywikiquote', 'kywiktionary', 'labswiki', 'ladwiki', 'lawiki', 'lawikibooks', 'lawikiquote', 'lawikisource', 'lawiktionary', 'lbewiki', 'lbwiki', 'lbwiktionary', 'lezwiki', 'lfnwiki', 'lgwiki', 'lijwiki', 'lijwikisource', 'liwiki', 'liwikibooks', 'liwikinews', 'liwikiquote', 'liwikisource', 'liwiktionary', 'lldwiki', 'lmowiki', 'lmowiktionary', 'lnwiki', 'lnwiktionary', 'lowiki', 'lowiktionary', 'ltgwiki', 'ltwiki', 'ltwikibooks', 'ltwikiquote', 'ltwikisource', 'ltwiktionary', 'lvwiki', 'lvwiktionary', 'madwiki', 'madwikisource', 'madwiktionary', 'maiwiki', 'map_bmswiki', 'mdfwiki', 'mediawikiwiki', 'metawiki', 'mgwiki', 'mgwikibooks', 'mgwiktionary', 'mhrwiki', 'minwiki', 'minwikibooks', 'minwikisource', 'minwiktionary', 'miwiki', 'miwiktionary', 'mkwiki', 'mkwikibooks', 'mkwikimedia', 'mkwikisource', 'mkwiktionary', 'mlwiki', 'mlwikibooks', 'mlwikiquote', 'mlwikisource', 'mlwiktionary', 'mniwiki', 'mniwiktionary', 'mnwiki', 'mnwiktionary', 'mnwwiki', 'mnwwiktionary', 'moswiki', 'mrjwiki', 'mrwiki', 'mrwikibooks', 'mrwikiquote', 'mrwikisource', 'mrwiktionary', 'mswiki', 'mswikibooks', 'mswikiquote', 'mswikisource', 'mswiktionary', 'mtwiki', 'mtwiktionary', 'mwlwiki', 'mxwikimedia', 'myvwiki', 'mywiki', 'mywikisource', 'mywiktionary', 'mznwiki', 'nahwiki', 'nahwiktionary', 'napwiki', 'napwikisource', 'nawiktionary', 'nds_nlwiki', 'ndswiki', 'ndswiktionary', 'newiki', 'newikibooks', 'newiktionary', 'newwiki', 'niawiki', 'niawiktionary', 'nlwiki', 'nlwikibooks', 'nlwikimedia', 'nlwikinews', 'nlwikiquote', 'nlwikisource', 'nlwikivoyage', 'nlwiktionary', 'nnwiki', 'nnwikiquote', 'nnwiktionary', 'novwiki', 'nowiki', 'nowikibooks', 'nowikimedia', 'nowikinews', 'nowikiquote', 'nowikisource', 'nowiktionary', 'nqowiki', 'nrmwiki', 'nrwiki', 'nsowiki', 'nupwiki', 'nvwiki', 'nycwikimedia', 'nywiki', 'ocwiki', 'ocwikibooks', 'ocwiktionary', 'olowiki', 'omwiki', 'omwiktionary', 'orwiki', 'orwikisource', 'orwiktionary', 'oswiki', 'outreachwiki', 'pagwiki', 'pamwiki', 'papwiki', 'pawiki', 'pawikibooks', 'pawikisource', 'pawiktionary', 'pcdwiki', 'pcmwiki', 'pcmwikiquote', 'pdcwiki', 'pflwiki', 'piwiki', 'plwiki', 'plwikibooks', 'plwikimedia', 'plwikinews', 'plwikiquote', 'plwikisource', 'plwikivoyage', 'plwiktionary', 'pmswiki', 'pmswikisource', 'pnbwiki', 'pnbwiktionary', 'pntwiki', 'pplwiki', 'pswiki', 'pswikivoyage', 'pswiktionary', 'ptwiki', 'ptwikibooks', 'ptwikimedia', 'ptwikinews', 'ptwikiquote', 'ptwikisource', 'ptwikiversity', 'ptwikivoyage', 'ptwiktionary', 'pwnwiki', 'quwiktionary', 'rkiwiki', 'rmwiki', 'rmywiki', 'rnwiki', 'roa_rupwiki', 'roa_rupwiktionary', 'roa_tarawiki', 'rowiki', 'rowikibooks', 'rowikinews', 'rowikiquote', 'rowiktionary', 'rskwiki', 'ruewiki', 'ruwiki', 'ruwikibooks', 'ruwikinews', 'ruwikiquote', 'ruwikisource', 'ruwikiversity', 'ruwikivoyage', 'ruwiktionary', 'rwwiki', 'rwwiktionary', 'sahwiki', 'sahwikiquote', 'sahwikisource', 'satwiki', 'satwiktionary', 'sawiki', 'sawikibooks', 'sawikiquote', 'sawikisource', 'sawiktionary', 'scnwiki', 'scnwiktionary', 'scowiki', 'scwiki', 'sdwiki', 'sdwiktionary', 'sewiki', 'sewikimedia', 'sgwiki', 'sgwiktionary', 'shiwiki', 'shnwiki', 'shnwikibooks', 'shnwikinews', 'shnwikivoyage', 'shnwiktionary', 'shwiki', 'shwiktionary', 'shywiktionary', 'simplewiki', 'simplewiktionary', 'siwiki', 'siwikibooks', 'siwiktionary', 'skwiki', 'skrwiki', 'skrwiktionary', 'slwiki', 'slwikibooks', 'slwikiquote', 'slwikisource', 'slwikiversity', 'slwiktionary', 'smnwiki', 'smwiki', 'smwiktionary', 'snwiki', 'sourceswiki', 'sowiki', 'sowiktionary', 'specieswiki', 'sqwiki', 'sqwikibooks', 'sqwikinews', 'sqwikiquote', 'sqwiktionary', 'srnwiki', 'srwiki', 'srwikibooks', 'srwikinews', 'srwikiquote', 'srwikisource', 'srwiktionary', 'sswiki', 'sswiktionary', 'stqwiki', 'stwiki', 'stwiktionary', 'suwiki', 'suwikiquote', 'suwikisource', 'suwiktionary', 'svwiki', 'svwikibooks', 'svwikinews', 'svwikiquote', 'svwikisource', 'svwikiversity', 'svwikivoyage', 'svwiktionary', 'swwiki', 'swwiktionary', 'sylwiki', 'szlwiki', 'szywiki', 'tawiki', 'tawikibooks', 'tawikinews', 'tawikiquote', 'tawikisource', 'tawiktionary', 'taywiki', 'tcywiki', 'tcywikisource', 'tcywiktionary', 'tddwiki', 'tewiki', 'tewikibooks', 'tewikiquote', 'tewikisource', 'tewiktionary', 'tgwiki', 'tgwikibooks', 'tgwiktionary', 'thwiki', 'thwikibooks', 'thwikimedia', 'thwikiquote', 'thwikisource', 'thwiktionary', 'tigwiki', 'tiwiki', 'tiwiktionary', 'tkwiki', 'tkwiktionary', 'tlwiki', 'tlwikibooks', 'tlwikiquote', 'tlwikisource', 'tlwiktionary', 'tlywiki', 'tnwiki', 'tnwiktionary', 'tokwiki', 'towiki', 'tpiwiki', 'tpiwiktionary', 'trvwiki', 'trwiki', 'trwikibooks', 'trwikimedia', 'trwikinews', 'trwikiquote', 'trwikisource', 'trwikivoyage', 'trwiktionary', 'tswiki', 'tswiktionary', 'ttwiki', 'ttwikibooks', 'ttwikiquote', 'ttwiktionary', 'tumwiki', 'twwiki', 'twwiktionary', 'tyvwiki', 'tywiki', 'uawikimedia', 'udmwiki', 'ugwiki', 'ugwiktionary', 'ukwiki', 'ukwikibooks', 'ukwikinews', 'ukwikiquote', 'ukwikisource', 'ukwikivoyage', 'ukwiktionary', 'urwiki', 'urwikibooks', 'urwikiquote', 'urwikisource', 'urwiktionary', 'uzwiki', 'uzwikiquote', 'uzwiktionary', 'vecwiki', 'vecwikisource', 'vecwiktionary', 'vepwiki', 'vewiki', 'viwiki', 'viwikibooks', 'viwikiquote', 'viwikisource', 'viwikivoyage', 'viwiktionary', 'vlswiki', 'vowiki', 'vowiktionary', 'warwiki', 'wawiki', 'wawikisource', 'wawiktionary', 'wikidatawiki', 'wikimaniawiki', 'wowiki', 'wowiktionary', 'wuuwiki', 'xalwiki', 'xhwiki', 'xmfwiki', 'yiwiki', 'yiwikisource', 'yiwiktionary', 'yowiki', 'zawiki', 'zeawiki', 'zghwiki', 'zghwiktionary', 'zh_classicalwiki', 'zh_min_nanwiki', 'zh_min_nanwikisource', 'zh_min_nanwiktionary', 'zh_yuewiki', 'zhwiki', 'zhwikibooks', 'zhwikinews', 'zhwikiquote', 'zhwikisource', 'zhwikiversity', 'zhwikivoyage', 'zhwiktionary', 'zuwiki', 'zuwiktionary', 'test2wiki', 'testwiki'];
// Time delay between API requests to prevent rate limiting (429 errors)
const DELAY_MS = 400;
// Helper function to pause execution for a specified number of milliseconds
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Cache for global user group information to avoid redundant API calls
let userCache = {};
// Cache for historical global rights changes within the audit period
let historyCache = {};
// Helper function to handle API calls with up to 3 retries on 429 errors
async function robustCall(api, params) {
let retries = 0;
const maxRetries = 3;
// Continue attempting the API request as long as we haven't hit the maximum retry limit
while (retries < maxRetries) {
try {
// Attempt to execute the API GET request
return await api.get(params);
} catch (err) {
// If the error is "429 Too Many Requests" (rate limit)
if (err?.status === 429) {
retries++;
// Get wait time from 'Retry-After' header, or use a default backoff (30s, 60s, 90s)
const retryAfter = parseInt(err.xhr?.getResponseHeader('Retry-After')) || (30 * retries);
// Update UI status message with the remaining wait time
$('#status-msg').html(
`<span style="color:orange; font-weight:bold;">Rate limit hit! Resting ${retryAfter}s... (Attempt ${retries}/${maxRetries})</span>`
);
// Pause execution before the next retry
await sleep(retryAfter * 1000);
} else {
// Rethrow any error that is not a rate limit (e.g., 404, 403, 500)
throw err;
}
}
}
// Fail if max retries are exceeded
throw new Error("Max retries reached");
}
// Converts a database name (like 'enwiki') into a real web address.
function getDomain(db) {
// Static mapping for projects with non-standard naming conventions
const mapping = {
'commonswiki': 'commons.wikimedia.org',
'metawiki': 'meta.wikimedia.org',
'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org',
'sourceswiki': 'wikisource.org',
'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org',
'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org',
'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org',
'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org',
'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org',
'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
// Return early if an explicit mapping exists
if (mapping[db]) return mapping[db];
// Format language codes: replace underscores with hyphens (e.g., zh_yue -> zh-yue)
const name = db.replace(/_/g, '-');
// Handle sister project families by trimming suffixes and appending correct domains
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
// Default logic for Wikipedias: remove 'wiki' suffix and append domain
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
// Generic fallback
return name + '.wikipedia.org';
}
function getInterwikiPrefix(db) {
// Mapping for special projects that use non-standard interwiki shortcuts
const specials = {
'commonswiki': 'commons',
'metawiki': 'm',
'wikidatawiki': 'd',
'mediawikiwiki': 'mw',
'specieswiki': 'wikispecies',
'foundationwiki': 'wikimedia',
'sourceswiki': 'wikisource',
};
// Return early if an explicit special prefix is defined
if (specials[db]) return specials[db];
// Format name for interwiki consistency (underscores to hyphens)
const name = db.replace(/_/g, '-');
// Logic for sister project families: trim suffixes and append the standard shorthand
if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10);
if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11);
if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10);
if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10);
if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9);
if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9);
if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8);
if (name.endsWith('wikimedia')) return 'wikimedia:' + name.slice(0, -9);
// Standard Wikipedia logic: removes 'wiki' suffix and adds 'w:' prefix
if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4);
// Default fallback for any other database patterns
return 'w:' + name;
}
// Ensure required MediaWiki API and ForeignApi modules are loaded before execution
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function () {
// Initialize API connection to Meta-Wiki for global user information and rights logs
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
// Capture current date/time for report generation and timestamping
const now = new Date();
// State management variables:
let results = {}, // Stores aggregated CU action counts per wiki/user
emptyWikis = [], // List of projects with no recorded CU actions
failedWikis = [], // List of projects that encountered API errors
scannedWikis = [], // Tracking list of successfully processed projects
isRunning = false; // Flag to control the start/stop state of the audit
// UI state variables for filtering wikis and user roles
let currentFilterMode = 'all';
let currentUserFilter = 'all';
// Initializes and builds the User Interface (UI) elements on the page.
function setupUI() {
// Get the current year to ensure the date range is always up-to-date
const currentYear = now.getFullYear();
// Generate HTML options for the year selection (starting from 2005 to current)
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
// Prepare HTML options for month selection (1-12)
let monthOptsFrom = '';
let monthOptsTo = '';
for (let m = 1; m <= 12; m++) {
// Set default: 'From' dropdown defaults to January (1)
monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`;
// Set default: 'To' dropdown defaults to December (12)
monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`;
}
// Set the page title to identify the script
$('#firstHeading').text('GlobalCheckUserStats.js');
// Clear the main content area and append the control panel HTML
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Stats Range</h3>
From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>Wiki Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-all" checked> All wikis</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-except"> All wikis except</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="wiki-mode" id="btn-only"> Only these wikis</label>
<span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold; font-size:12px; line-height:18px;" title="Show all wiki DB names">?</span>
</div>
<div id="filter-input-container" style="display:none; margin-top:10px;">
<input id="wiki-filter" type="text" style="width:100%; max-width:600px;" placeholder="">
</div>
<div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;">
<strong>User Filter:</strong>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-all" checked> All users</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-local"> Only Local CUs</label>
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;"><input type="radio" name="user-mode" id="u-steward"> Only Stewards</label>
</div>
<div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace; color:#202122;">
<strong>Available Wikis (alphabetical):</strong><br>${[...rawWikis].sort().join(', ')}
</div>
<div style="margin-top:20px;">
<button id="start" class="mw-ui-button mw-ui-progressive">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
</div>
<div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div>
<div style="margin-top:5px;"><progress id="bar" value="0" max="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
// Handle clicking 'All wikis': resets filter mode and hides the input field
$('#btn-all').click(() => {
currentFilterMode = 'all';
$('#filter-input-container').hide();
});
// Handle clicking 'All wikis except': shows input field, sets mode to 'exclude', and focuses the box
$('#btn-except').click(() => {
currentFilterMode = 'exclude';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Exclude (dbname1, dbname2)...').focus();
});
// Handle clicking 'Only these wikis': shows input field, sets mode to 'include', and focuses the box
$('#btn-only').click(() => {
currentFilterMode = 'include';
$('#filter-input-container').show();
$('#wiki-filter').attr('placeholder', 'Only (dbname1, dbname2)...').focus();
});
// User role filter buttons: define which user categories will appear in the final table
$('#u-all').click(() => { currentUserFilter = 'all'; });
$('#u-local').click(() => { currentUserFilter = 'local'; });
$('#u-steward').click(() => { currentUserFilter = 'steward'; });
// Toggle visibility of the 'Available Wikis' help list when the question mark icon is clicked
$('#wiki-help-trigger').click(() => $('#wiki-list-help').toggle());
// Main execution trigger: starts the audit using the selected date range from dropdowns
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
// Stop button: sets isRunning to false to halt the loop between wiki scans
$('#stop').click(() => isRunning = false);
}
// Fetches general statistics for a specific wiki, primarily to get the active user count.
async function fetchWikiMetrics(db) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
// Request site statistics via the 'siteinfo' meta module
const res = await robustCall(api, {
action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2
});
// Extract the active users count; default to 0 if the data is missing
return { active: res.query.statistics.activeusers || 0 };
} catch (e) {
// Graceful fallback: return zero if the project is unreachable or API fails
return { active: 0 };
}
}
// Checks if a user held global roles (Steward, Staff, Ombuds) during the audit period.
async function checkGlobalHistory(user, start, end) {
try {
// Fetch global rights logs from Meta-Wiki for the specific user
const res = await robustCall(metaApi, {
action: 'query', list: 'logevents', letype: 'gblrights',
letitle: 'User:' + user, lelimit: 'max', formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let state = { steward: false, staff: false, ombuds: false };
// Look at the last log entry BEFORE the audit started to see the initial status
const priorLogs = logs
.filter(l => new Date(l.timestamp) <= auditStart)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (priorLogs.length > 0) {
const p = priorLogs[0].params || {};
const groupsInLog = p.newGroups || p[1] || [];
state.steward = groupsInLog.includes('steward');
state.staff = groupsInLog.includes('staff');
state.ombuds = groupsInLog.includes('ombuds');
}
// Keep track of any global roles gained during the audit period itself
let held = { wasSteward: state.steward, wasStaff: state.staff, wasOmbuds: state.ombuds };
logs
.filter(l => {
const ts = new Date(l.timestamp);
return ts > auditStart && ts < auditEnd;
})
.forEach(e => {
const p = e.params || {};
const added = p.newGroups || p[1] || [];
if (added.includes('steward')) held.wasSteward = true;
if (added.includes('staff')) held.wasStaff = true;
if (added.includes('ombuds')) held.wasOmbuds = true;
});
return held;
} catch (e) {
// If anything fails, assume no global roles were held
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
// Gathers all necessary data for a specific user on a specific wiki.
async function fetchUserData(user, db, start, end) {
// Convert start and end strings into Date objects for easier comparison
const auditStart = new Date(start);
const auditEnd = new Date(end);
// 1. FETCH CURRENT GLOBAL GROUPS (Using cache to save time)
if (!userCache[user]) {
try {
// Ask Meta-Wiki for the user's current global groups (steward, staff, etc.)
const gres = await robustCall(metaApi, {
action: 'query', meta: 'globaluserinfo', guiprop: 'groups',
guiuser: user, formatversion: 2
});
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch (e) {
// Fallback to an empty list if the request fails
userCache[user] = [];
}
}
// Check if the user currently holds any key global roles
const gGroups = userCache[user];
const isGloballySteward = gGroups.includes('steward');
const isGloballyStaff = gGroups.includes('staff');
const isGloballyOmbuds = gGroups.includes('ombuds') || gGroups.includes('ombudsman');
// 2. FETCH GLOBAL HISTORY (Identify roles held during the period but lost since)
if (!historyCache[user]) {
historyCache[user] = await checkGlobalHistory(user, start, end);
}
const historyRes = historyCache[user];
// 3. CHECK CURRENT LOCAL CHECKUSER STATUS
let isCurrentLocal = false;
try {
// Connect to the local wiki to see if the user is currently a CheckUser
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await robustCall(localApi, {
action: 'query', list: 'users', ususers: user,
usprop: 'groups', formatversion: 2
});
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch (e) {
// Ignore errors; defaults to false
}
// Define the target string for rights log searching
const target = 'User:' + user + '@' + db;
// Initialize variables for log analysis and role calculation
let logText = '',
isSelfAssign = false,
maxDurationMins = -1,
longestTimeStr = "",
assignCount = 0,
hasLocalRightsInPeriod = false;
// 4. DEEP RIGHTS LOG SCAN (Pagination to bypass 500-limit)
let events = [];
let continueToken = null;
let finished = false;
try {
while (!finished) {
let params = {
action: 'query', list: 'logevents', letype: 'rights',
letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
const res = await robustCall(metaApi, params);
const batch = res.query.logevents || [];
events = events.concat(batch);
if (res.continue) {
continueToken = res.continue;
} else {
finished = true;
}
}
let logEntries = [];
let pendingRemoved = null;
let lastPairedDate = null;
for (let i = 0; i < events.length; i++) {
const e = events[i];
const p = e.params || {};
// Clean the actor name by removing project prefixes (e.g., "metawiki>User" becomes "User")
const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user;
// Check if checkuser group actually changed state
const hadCU = (p.oldgroups || []).includes('checkuser');
const hasCU = (p.newgroups || []).includes('checkuser');
const addedInList = (p.add || []).includes('checkuser');
const removedInList = (p.remove || []).includes('checkuser');
// Detect if an expiry was added to a previously permanent group
let expiryAddedLater = false;
if (hasCU && hadCU && p.newmetadata && p.oldmetadata) {
const oldM = p.oldmetadata.find(m => m.group === 'checkuser');
const newM = p.newmetadata.find(m => m.group === 'checkuser');
if (oldM && newM && oldM.expiry === 'infinity' && newM.expiry !== 'infinity') {
expiryAddedLater = true;
}
}
// Define if rights were gained or lost in this specific log entry
let cuAdded = addedInList || (hasCU && !hadCU) || expiryAddedLater;
const cuRemoved = removedInList || (hadCU && !hasCU);
if (cuAdded || cuRemoved) {
const eventDate = new Date(e.timestamp);
const exactTime = e.timestamp.replace('T', ' ').replace('Z', '');
// FLAG: Is this specific event within the audited year?
const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd);
// FLAG: Is this event from the future (relative to the audited year)?
const isFuture = (eventDate > auditEnd);
if (isInPeriod) {
hasLocalRightsInPeriod = true;
}
// GATEKEEPER: Process if event is in period, OR if it's a future removal
// that we need to pair with a potential addition inside the period.
if (isInPeriod || isFuture || (pendingRemoved && cuAdded)) {
let expiryDate = null;
if (p.newmetadata) {
const cuMeta = p.newmetadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') {
expiryDate = new Date(cuMeta.expiry);
}
}
// Check if the action was performed by the user themselves
const eventBySelf = (actor === user || !actor);
if (cuAdded) {
if (pendingRemoved || expiryDate) {
const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate;
const diffMs = Math.abs(checkDate - eventDate);
const totalSecs = Math.floor(diffMs / 1000);
let durStr = "";
// Format duration: show seconds if under 1 minute, otherwise use D H M
if (totalSecs < 60) {
durStr = totalSecs + "s";
} else {
const diffMins = Math.floor(totalSecs / 60);
const d = Math.floor(diffMins / 1440);
const h = Math.floor((diffMins % 1440) / 60);
const m = diffMins % 60;
durStr = `${d > 0 ? d + 'd ' : ''}${h > 0 ? h + 'h ' : ''}${m}m`;
}
if (pendingRemoved) {
// Mark as self-assign if both ADD and REMOVE were performed by the user
if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true;
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user} (Duration: ${durStr})`);
pendingRemoved = null;
} else {
if (eventBySelf) isSelfAssign = true;
const exactExpiryTime = expiryDate.toISOString().replace('T', ' ').substring(0, 19);
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | EXPIRED: ${exactExpiryTime} (Duration: ${durStr})`);
}
// Update max duration for role classification
const durationForComparison = totalSecs / 60;
if (durationForComparison > maxDurationMins) {
maxDurationMins = durationForComparison;
longestTimeStr = durStr;
}
assignCount++;
lastPairedDate = eventDate;
} else {
// Handle cases where group is still active
if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) {
logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`);
lastPairedDate = eventDate;
}
}
} else if (cuRemoved) {
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf };
}
}
}
}
if (pendingRemoved) {
logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`);
}
if (logEntries.length) logText = '\n' + logEntries.join('\n');
// 5. ROLE CLASSIFICATION
let roles = [];
const isTemporaryMission = (longestTimeStr && isSelfAssign && (isGloballySteward || historyRes.wasSteward));
// 1. WMF Staff status
if (isGloballyStaff || historyRes.wasStaff) {
roles.push(isGloballyStaff ? "Current Staff" : "Former Staff (in period)");
}
// 2. Steward status
if (isTemporaryMission) {
const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : "";
roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`);
} else if (isGloballySteward || historyRes.wasSteward) {
roles.push(isGloballySteward ? "Current Steward" : "Former Steward (in period)");
}
// 3. Ombudsman status
if (isGloballyOmbuds || historyRes.wasOmbuds) {
roles.push(isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)");
}
// 4. Local CheckUser status
if (isCurrentLocal) {
roles.push("Current Local CheckUser");
}
// Strict check: Only show Former Local CU if they actually HELD the group in the past
// (hasLocalRightsInPeriod = true from rights log), NOT just because they have actions.
if (!isCurrentLocal && hasLocalRightsInPeriod && !isTemporaryMission) {
roles.push("Former Local CheckUser (in period)");
}
let uniqueRoles = [...new Set(roles)];
let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role";
return { role: roleLabel, log: logText };
}
catch (err) {
return { role: "Error fetching data", log: "" };
}
}
// The main engine that iterates through wikis...
// The main engine that iterates through wikis and collects CheckUser logs.
async function runAudit(yf, mf, yt, mt) {
// Reset all data and state variables for a fresh run
isRunning = true;
userCache = {};
historyCache = {};
results = {};
emptyWikis = [];
failedWikis = [];
scannedWikis = [];
// Parse and prepare the wiki filter list from the UI input
const filterText = $('#wiki-filter').val().trim().toLowerCase();
const filterList = filterText
? filterText.split(',').map(s => s.trim()).filter(s => s !== "")
: [];
// Apply include/exclude logic to the master wiki list
let wikisToScan = rawWikis;
if (currentFilterMode === 'include') {
wikisToScan = rawWikis.filter(w => filterList.includes(w));
} else if (currentFilterMode === 'exclude') {
wikisToScan = rawWikis.filter(w => !filterList.includes(w));
}
if (wikisToScan.length === 0) {
alert("No wikis selected!");
return;
}
// Update UI buttons and progress bar to "running" state
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
$('#bar').attr('max', wikisToScan.length).val(0);
// Format the date strings into ISO format for the MediaWiki API
const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`;
const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`;
// Generate the columns for the table based on the selected month range
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
let endY = parseInt(yt), endM = parseInt(mt);
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}`
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
// Loop through each selected wiki and fetch its logs
for (let i = 0; i < wikisToScan.length; i++) {
if (!isRunning) break; // Exit loop if the "Stop" button was clicked
const db = wikisToScan[i];
scannedWikis.push(db);
$('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`);
let successLocal = false;
let continueToken = null;
// Inner loop for handling pagination of the logs on a single wiki
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query', list: 'checkuserlog',
culfrom: START, culto: END, culdir: 'newer',
cullimit: 'max', formatversion: 2
};
if (continueToken) Object.assign(params, continueToken);
let res = await robustCall(api, params);
// Process each entry in the log batch
const ent = res.query?.checkuserlog?.entries || [];
if (ent.length) {
if (!results[db]) results[db] = {};
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const mKey = e.timestamp.slice(0, 7);
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Check if there are more log pages to fetch
if (res.continue && isRunning) {
continueToken = res.continue;
} else {
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
// Record failures (like 429 after retries or 404) and move to the next wiki
failedWikis.push(db);
successLocal = true;
}
}
// Update the progress bar and wait a bit to be kind to the API
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// Update UI and prepare the Wikitable header and metadata
$('#status-msg').text(`Generating report...`);
const today = new Date().toISOString().split('T')[0];
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' (UTC)';
// Start building the Wikitext report
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n''Report generated on: ${timestamp}''\n\n`;
let rightsLog = `\n== Rights Log (In Period) ==\n`;
// Create table structure with dynamic month columns
wt += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n`;
wt += `! Wiki / User !! Category !! '''Total''' !! per 1k active users ${monthCols.map(m => `!! ${m.label}`).join(' ')}\n`;
// Sort databases alphabetically and begin processing each project's results
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
// Get active user count; if API fails or is 0, display 'Unknown'
const metrics = await fetchWikiMetrics(db);
const fmtActive = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown';
// Prepare interwiki links for the project and its CheckUser log
const iwPrefix = getInterwikiPrefix(db);
const projectLink = `[[${iwPrefix}:|${db}]]`;
const logLink = `[[${iwPrefix}:Special:CheckUserLog|(log)]]`;
wt += `|-\n! colspan="${4 + monthCols.length}" style="background:#eaecf0; text-align:center;" | `;
wt += `${projectLink} <small style="font-weight:normal; color:#54595d;">(Active Users as of ${today}: ${fmtActive})</small> — ${logLink}\n`;
// Initialize local wiki totals and storage for individual user rows
let wikiRows = [];
let wT = 0;
let wMS = {};
monthCols.forEach(col => wMS[col.key] = 0);
// Get users from CU logs AND users who had rights changes (if we tracked them)
// For now, let's at least make sure we don't skip users with 0 actions but active rights
let projectUsers = Object.keys(results[db] || {});
projectUsers.sort();
for (const user of projectUsers) {
// Get role and rights log details for each user
const m = await fetchUserData(user, db, START, END);
// Identify if user belongs to specific global or local groups
const isSteward = m.role.includes("Steward") || m.role.includes("Staff") || m.role.includes("Ombudsman");
const isLocalCU = m.role.includes("Local CheckUser");
// Filter out users if they don't match the selected User Filter (Local/Steward)
if (currentUserFilter === 'local' && !isLocalCU) continue;
if (currentUserFilter === 'steward' && !isSteward) continue;
const uD = results[db][user];
// Aggregate totals for the current wiki
wT += uD.total;
monthCols.forEach(col => wMS[col.key] += (uD.months[col.key] || 0));
// Calculate actions per 1k active users; display 0.0 if project activity is unknown
let uP1kU = metrics.active > 0 ? ((uD.total / metrics.active) * 1000).toFixed(1) : "0.0";
// Build the Wikitext row for the individual user
let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${m.role}</small> || '''${uD.total}''' || ${uP1kU}`;
monthCols.forEach(col => row += ` || ${uD.months[col.key] || 0}`);
// Store the formatted row and its corresponding rights log
wikiRows.push({
html: row + "\n",
log: m.log ? `'''${user}@${db}''':${m.log}\n\n` : ""
});
}
// Create the bold TOTAL row for the current wiki
let wP1kU = metrics.active > 0 ? ((wT / metrics.active) * 1000).toFixed(1) : "0.0";
let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wT} || ${wP1kU}`;
monthCols.forEach(col => totalRow += ` || ${wMS[col.key]}`);
// Append the total row first, then all individual user rows
wt += totalRow + "\n";
wikiRows.forEach(r => {
wt += r.html;
if (r.log) rightsLog += r.log;
});
}
// Append a list of wikis where no CheckUser actions were found
wt += `|}\n\n== Projects with 0 actions ==\n`;
wt += `<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`;
// Append a list of projects that failed to scan (e.g., due to API errors)
if (failedWikis.length > 0) {
wt += `\n== API Errors / Skipped Projects ==\n`;
wt += `<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`;
}
// Finalize the report with the Rights Log section
wt += rightsLog + `\n\n<references />\n`;
// Display the final output and reset UI buttons
$('#out').val(wt).show();
$('#status-msg').text(`Done.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
// Initialize the interface on page load
setupUI();
});
})();
0q80thn3zxgnntfjzxklyumv584x9ls
User:Gnoeee/sandbox3
2
174769
738114
737454
2026-04-16T08:07:30Z
Gnoeee
39484
738114
wikitext
text/x-wiki
<translate>
<!--T:1-->
Test Page
</translate>
=== OK ===
=== Testing ===
This is a test
fapvzf3lkrvmhgdawkioohkh97js099
738115
738114
2026-04-16T08:08:28Z
Gnoeee
39484
738115
wikitext
text/x-wiki
<translate>
<!--T:1-->
Test Page
</translate>
<translate>
=== OK ===
</translate>
<translate>
=== Testing ===
This is a test
</translate>
ex7w78uxlwd9syhxm710g7y7rb9g7h3
738116
738115
2026-04-16T08:08:44Z
Gnoeee
39484
Marked this version for translation
738116
wikitext
text/x-wiki
<translate>
<!--T:1-->
Test Page
</translate>
<translate>
=== OK === <!--T:2-->
</translate>
<translate>
<!--T:3-->
=== Testing ===
This is a test
</translate>
a1wltm47v4c77sjrfel9rs1wph9ufsw
User:Gnoeee/sandbox3/ml
2
174773
738120
737458
2026-04-16T08:08:45Z
FuzzyBot
18251
Updating to match new version of source page
738120
wikitext
text/x-wiki
ടെസ്റ്റ് പേജ്
<div lang="en" dir="ltr" class="mw-content-ltr">
=== OK ===
</div>
<div lang="en" dir="ltr" class="mw-content-ltr">
=== Testing ===
This is a test
</div>
t7i570932ybpkcungi7gwt3f58xvs5j
User:Solidest/remover-core.js
2
174846
738075
737970
2026-04-15T22:03:51Z
Solidest
54422
738075
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</div>';
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
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 + ';';
}
return '<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;"></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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'padding:10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: multiNominationGap,
minHeight: 90,
embedded: true
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
qm8x8afwy89c3iv16woz1xaguvsn45c
738076
738075
2026-04-15T22:05:14Z
Solidest
54422
738076
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</div>';
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
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 + ';';
}
return '<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;"></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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'padding:6px 8px;border:1px solid ' + tk.bSubS + ';border-radius:5px;background:' + tk.bgBase + ';max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: multiNominationGap,
minHeight: 90,
embedded: true
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
mpu2biz7rl6mcacqswolug9ocd0oqvf
738077
738076
2026-04-15T22:07:32Z
Solidest
54422
738077
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</div>';
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
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 + ';';
}
return '<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;"></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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'padding:8px;border:0;border-radius:8px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);max-width:100%;box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: multiNominationGap,
minHeight: 90,
embedded: true
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
b747jlt89fk0b6pd1i0d5095j8rtcug
738078
738077
2026-04-15T22:28:00Z
Solidest
54422
738078
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 multiNominationGap = '6px';
var multiNominationCardGap = '3px';
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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 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 + ';';
}
return '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'padding:5px 6px;border:0;border-radius:8px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);max-width:100%;box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '4px',
minHeight: 90,
embedded: true,
textareaStyleExtra: 'border-color:' + tk.bSub + ';border-radius:4px;box-shadow:0 0 0 1px rgba(162,169,177,.12),0 1px 2px rgba(0,0,0,.04);'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationCardGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
ckyf02h9plwgdch7zmfoexeuoe8x27y
738079
738078
2026-04-15T22:46:28Z
Solidest
54422
738079
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '4px',
minHeight: 90,
wrapStyleExtra: 'padding:6px 8px;border-color:' + tk.bSub + ';background:' + tk.bgBase + ';',
textareaStyleExtra: 'border-color:' + tk.bSub + ';border-radius:4px;'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
c0682z2yzl3auz1f77trvl54wwz72qz
738080
738079
2026-04-15T22:48:50Z
Solidest
54422
738080
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;padding:6px;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgBase + ';overflow:hidden;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '0',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:6px;border-top:1px solid ' + tk.bSubS + ';background:' + tk.bgNSub + ';',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
6ieacv8dk42jdjy2fr5ogwlexgjg3zz
738081
738080
2026-04-15T22:54:17Z
Solidest
54422
738081
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '0',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:6px 0 0;border-top:1px solid ' + tk.bSubS + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var $block = $btn.closest('.rmMultiArticleBlock');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if ($block.length) {
$block.css(expanded ? {
padding: '6px',
border: '1px solid ' + tk.bSubS,
'border-radius': '6px',
background: tk.bgBase
} : {
padding: '',
border: '',
'border-radius': '',
background: ''
});
}
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
4mnjc55mf4dar8bdi036qkig4jwf1n7
738082
738081
2026-04-15T22:57:56Z
Solidest
54422
738082
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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.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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" style="' + stToolBtn + '">Добавить статью</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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;
}
renderModalFooter('reload', { reloadText: 'Перезагрузить страницу' });
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) { if (err) { unlockModalSubmit(); logStatus('Ошибка записи.', err); } else location.reload(); });
} 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 () { finalizeSuccess(null, false); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
pmlfhhd84flzl163pyp91urnaup9bs2
738083
738082
2026-04-15T23:18:30Z
Solidest
54422
738083
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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 .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:' + tk.cBase + '!important}' +
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:' + tk.cBase + '!important;filter:none}' +
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:' + tk.cBase + '!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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" title="Добавить статью" aria-label="Добавить статью" style="' + stToolBtn + '">+</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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();
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
renderModalFooter('reload');
}
});
} 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 () { finalizeSuccess(null, true); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
oyxlr8825am4xuphq43he3gsss72fac
738085
738083
2026-04-15T23:28:52Z
Solidest
54422
738085
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
try {
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = 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 = initialSettings.signatureSeparator;
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 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]+)\s+(\d{4})$/;
var RE_DATE_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
var RE_DATE_DMY_DASH = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
var RE_DATE_YMD_DOT = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
var DEBUG_NO_PUBLISH = true;
// ─── Глобальные переменные сессии ────────────────────────────────────────
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)',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: '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)',
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 stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'padding:4px 0;width:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stToggleBtn= 'padding:4px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;';
var stCompactGroup = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
var stCompactBtn = 'display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border:1px solid ' + tk.bSub + ';border-radius:4px;cursor:pointer;font-size:12px;background:' + tk.bgNSub + ';color:inherit;user-select:none;';
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 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,
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; },
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 ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function monthToNumber(name) {
var lower = name.toLowerCase();
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
return idx + 1;
}
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) {
if (RE_DATE_ISO.test(dateStr)) return dateStr;
var m = dateStr.match(RE_DATE_RUSSIAN);
if (m) { var mo = monthToNumber(m[2]); if (mo) return m[3] + '-' + padTwo(mo) + '-' + padTwo(parseInt(m[1], 10)); }
m = dateStr.match(RE_DATE_DOT);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_DMY_DASH);
if (m) return parseInt(m[3], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[1], 10));
m = dateStr.match(RE_DATE_YMD_DOT);
if (m) return parseInt(m[1], 10) + '-' + padTwo(parseInt(m[2], 10)) + '-' + padTwo(parseInt(m[3], 10));
return dateStr;
}
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(/^Категория:\s*/i, ''); }
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 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 v2: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object') 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 = settingsItemLabelOrder.hasOwnProperty(a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = settingsItemLabelOrder.hasOwnProperty(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 formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
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 '<div class="rmQuickPhrasesPanel ' + RESIZE_CLASS + '" data-rm-target="' + textareaId + '">' +
phrases.map(function (phrase) {
return '<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="' + textareaId + '" data-rm-phrase="' + escapeHtml(phrase) + '">' +
escapeHtml(phrase) + '</button>';
}).join('') +
'</div>';
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle) {
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n=== [[:' + a + ']] ===\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n=== По всем ===\n' + commonSectionText;
}
function collectMultiNominationComments() {
var comments = {};
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normTitle(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = 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 || 'КУ') + '}}'
};
}
};
}
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 (DEBUG_NO_PUBLISH && mode === 'edit') {
console.log('[DEBUG] Публикация заблокирована. Параметры:', reqParams);
if (callback) callback({ edit: { result: 'Success' } });
return;
}
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 getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.missing === undefined && page.revisions && page.revisions.length) {
callback(extractRevisionContent(page.revisions[0]), page.revisions[0].timestamp || null);
} else {
callback(null, null);
}
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text) { callback(text); });
}
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) {
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 stripTemplatesByPattern(text, namePattern) {
var re = new RegExp('(<noinclude>\\s*)?\\{\\{\\s*(?:subst\\s*:\\s*)?(?:' + namePattern + ')(?:\\|[\\s\\S]*?)?\\}\\}\\s*(<\\/noinclude>\\s*)?\\n?', 'gi');
var cleaned = text.replace(re, '');
return { text: cleaned, removed: cleaned !== text };
}
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]*(?:\||\}\})/);
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
var depth = 1, i = pos + 2;
while (i < len - 1 && depth > 0) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') { depth++; i += 2; }
else if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') { depth--; i += 2; }
else { i++; }
}
if (depth !== 0) break;
pos = i;
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 () {};
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;
}
// Вставка в существующую страницу
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 statusId = logStatus('Отправляется уведомление создателю страницы «' + pg + '»...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы «' + pg + '».' : 'Создатель страницы «' + pg + '» уведомлён.', 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{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,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 .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:' + tk.cBase + '!important}' +
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:' + tk.cBase + '!important;filter:none}' +
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:' + tk.cBase + '!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):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}' +
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):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 .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 .rmSettingsToggle{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;margin:0 0 10px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}' +
'#removerModal .rmSettingsToggle:last-child{margin-bottom:0}' +
'#removerModal .rmSettingsToggle input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}' +
'#removerModal .rmSettingsToggleBody{display:block;min-width:0}' +
'#removerModal .rmSettingsToggleTitle{display:block;font-size:14px;font-weight:600;line-height:1.4;color:' + tk.cBase + '}' +
'#removerModal .rmSettingsToggleTitle.is-emphasized{font-size:15px;font-weight:700}' +
'#removerModal .rmSettingsToggleHint{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}' +
'#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:72ch}' +
'#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:72ch}' +
'#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{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 .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:12px 13px;border:1px solid ' + tk.bSubS + ';border-radius:14px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box;box-shadow:0 1px 0 rgba(255,255,255,.72) inset}' +
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:22px;row-gap:8px;align-items:start;justify-content:start}' +
'#removerModal .rmTransferGrid .rmTransferFieldLabel{margin:0}' +
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}' +
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}' +
'#removerModal .rmTransferField{margin-top:10px;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 .rmTransferField:first-child{margin-top:0}' +
'#removerModal .rmTransferFieldLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}' +
'#removerModal .rmTransferFieldHint{margin:0 0 6px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}' +
'#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 .rmArticleRow{flex-wrap:wrap!important;gap:6px}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}' +
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow #rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .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(3){order:2}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:3}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(4){order:4}' +
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(5){order:5}' +
'#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.bgN + ';border-color:' + tk.bSubS + ';color:' + tk.cSubM + ';-webkit-text-fill-color:' + tk.cSubM + ';opacity:1;cursor:not-allowed}' +
'#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 .rmTransferPanel{padding:10px 11px}' +
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}' +
'#removerModal .rmTransferHintRow{grid-column:auto}' +
'#removerModal .rmSettingsToggle{padding:9px 10px}' +
'#removerModal .rmQuickPhraseChip{max-width:100%}' +
'}';
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 createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
if (opts.subtitleHtml) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' + opts.subtitleHtml + '</div>';
} else if (opts.subtitlePage) {
subtitleHtml = '<div id="removerModalSubtitle" style="margin:-4px 0 8px;font-size:12px;color:' + tk.cSubM + ';line-height:1.3;">' +
(opts.subtitleLabel || 'Текущий день') + ': <a href="' + getPageUrl(opts.subtitlePage) +
'" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + normTitle(opts.subtitlePage) + '</a></div>';
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
'<button id="removerSettingsTrigger" type="button" title="Настройки Remover" aria-label="Настройки Remover" ' +
'style="margin:0 0 0 auto;min-width:32px;height:32px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border:1px solid ' + tk.bSub + ';' +
'border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;">⚙</button>';
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;' : '';
$('#content').prepend(
'<div id="removerModal" style="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 + '">' +
'<div id="removerModalHeaderBar" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';">' +
'<h1 id="removerModalTitle" style="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;"><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 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 = '<div id="rmFooterCheckboxes" style="' + stFooterChecks + '">';
if (showSub) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmSubscribe" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setSubscribe ? 'checked' : '') + '>Подписаться на номинацию</label>';
if (showCb) cbInlineHtml += '<label style="' + stFooterCheckLabel + '"><input name="rmUAlert" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ' + (setAlert ? 'checked' : '') + '>' + notifyLabel + '</label>';
cbInlineHtml += '</div>';
}
$('#removerModalFooter').html(
'<div id="rmFooterButtons" style="' + stFooterWrap + 'justify-content:' + (cbInlineHtml ? 'space-between' : 'flex-end') + ';">' +
cbInlineHtml +
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Отмена</button>' +
'<button id="removerSubmit" style="' + stSubmit + '">' + (opts.submitText || 'ОК') + '</button>' +
'</div></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 =
'<div id="rmFooterActionButtons" style="' + stFooterActions + '">' +
'<button id="removerCancel" style="' + stCancel + '">Закрыть</button>' +
'<button id="removerReload" style="' + stReload + '">' + (opts.reloadText || 'Обновить страницу') + '</button>' +
'</div>';
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append('<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(
'<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(String(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 logPageEdit(pageName, error, opts) {
logStatus('Правка страницы «' + normTitle(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"' : '';
var html = '<div class="rmInfoBox"><p' + cls + ' style="margin:0' + (detailsText ? ' 0 6px' : '') + ';">' + mainText + '</p>';
if (detailsText) html += '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>';
return html + '</div>';
}
function buildActionsHtml(actions, inputName, listId) {
var html = '<div style="margin:0 0 8px;color:' + tk.cSubM + ';font-size:13px;">Обнаружены открытые номинации:</div>';
html += '<div' + (listId ? ' id="' + listId + '"' : '') + ' class="rmActionList">';
actions.forEach(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>'
: '';
html += '<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>';
});
return html + '</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 '<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 buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = '<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="' + commentWrapId + '" data-rm-comment-textarea="' + commentId + '" aria-expanded="false" style="' + commentBtnStyle + '">Комментарий</button>';
if (opts.showAdd) {
buttonsHtml += '<button id="rmAddArticle" type="button" title="Добавить статью" aria-label="Добавить статью" style="' + stRemoveBtn + '">+</button>';
} else {
buttonsHtml += '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить статью">−</button>';
}
return '<div class="rmMultiArticleBlock ' + RESIZE_CLASS + '" style="' + blockStyle + '">' +
'<div' + (opts.rowId ? ' id="' + opts.rowId + '"' : '') + ' class="rmArticleRow" style="' + articleRowStyle + '">' +
'<input id="' + articleId + '" type="text" placeholder="Статья" class="rmArticleInput" style="' + stInputBox + '"' + articleValue + '>' +
buttonsHtml +
'</div>' +
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: 'Комментарий только для этой статьи (необязательно)',
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}) +
'</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.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) {
var conflict;
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье «' + escapeHtml(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 pageUrl = getPageUrl(conflict.pageName);
var pageLink = '<a href="' + escapeHtml(pageUrl) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(conflict.pageName) + '</a>';
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();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
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;
$('#' + opts.containerId).append(
'<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + (w ? 'width:' + w + 'px;' : '') + '">' +
'<input type="text" class="' + (opts.inputClass || 'variantInput') + '" placeholder="' + (opts.placeholder || '') + '" style="' + stInputBox + '">' +
'<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) {
return '<div class="rmInputRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + c.firstId + '" type="text" class="' + (c.inputClass || 'variantInput') + '" placeholder="' + (c.firstPh || '') + '" style="' + stInputBox + '">' +
'<button id="' + c.addBtnId + '" type="button" style="' + stToolBtn + '">' + (c.addBtnLabel || '+ Добавить') + '</button></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 });
});
}
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 '<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 = '<div class="rmSettingsSectionHeader">' +
(title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '') +
(description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '') +
'</div>';
}
return '<div class="rmSettingsSection">' +
headerHtml +
bodyHtml +
'</div>';
}
function buildSettingsCheckboxHtml(id, text, options) {
var opts = options || {};
return '<label class="rmSettingsToggle">' +
'<input id="' + id + '" type="checkbox">' +
'<span class="rmSettingsToggleBody">' +
'<span class="rmSettingsToggleTitle' + (opts.emphasize ? ' is-emphasized' : '') + '">' + text + '</span>' +
(opts.description ? '<span class="rmSettingsToggleHint">' + opts.description + '</span>' : '') +
'</span></label>';
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return '<label class="rmSettingsCheck">' +
'<input id="' + id + '" type="checkbox">' +
'<span>' + text + '</span>' +
'</label>';
}
function buildQuickPhrasesSettingsEditorHtml() {
return '<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 '<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 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();
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();
}
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;
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);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
}
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 isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
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 '<div class="rmSettingsMenuPresetWrap">' +
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>' +
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">' +
getMenuTitlePresetOptions().map(function (option) {
return '<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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
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 (mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless') 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 = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
var menuTitleValue = data.menuTitle || '';
if (forcePresetOnly && !isMenuTitlePresetValue(menuTitleValue)) menuTitleValue = MENU_TITLE_PRESET_CACTIONS;
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#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() {
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : MENU_TITLE_PRESET_CACTIONS)
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: collectQuickPhraseValues()
})
};
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
createModal({
title: 'Настройки Remover',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalContent').html(
'<div id="rmSettingsForm" style="max-width:100%;">' +
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>' +
buildSettingsSectionHtml(
'Меню',
buildSettingsFieldHtml(
'Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(),
{ forId: 'rmSettingsMenuTitle' }
) +
buildSettingsFieldHtml(
'Визуальное оформление пунктов',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи перед текстом') +
'</div>'
),
'Настройки внешнего вида и состава меню Remover.'
) +
buildSettingsSectionHtml(
'Оформление сообщений',
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' }
),
'Настройки оформления публикуемых сообщений в номинациях.'
) +
buildSettingsSectionHtml(
'Опции по умолчанию',
'<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
'</div>',
'Регулирует изначальное состояние галочек.'
) +
buildSettingsSectionHtml(
'Отключение',
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' }
),
'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'
) +
'</div>'
);
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();
});
}
});
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;margin-right:auto;">' +
'<button id="rmSettingsResetFooter" type="button" style="' + stCancel + '">Сбросить настройки</button></div>'
);
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
$('#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();
}
// ─── Общий 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) {
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(escapeRegExp).join('|');
var RE_DATE_ANY = '(\\d{4}-\\d\\d-\\d\\d|\\d{1,2}\\s+[\\u0430-\\u044f\\u0410-\\u042f]+\\s+\\d{4})';
var tplRegex = RegExp('\\{\\{(' + tplPattern + ')\\|' + RE_DATE_ANY + '\\|?(.*?)\\}\\}', 'gi');
var tpl = tplRegex.exec(article);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var date = getDate(convertToStandardDate(tpl[2]));
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') { sectionNW = pg + ' → ' + tpl[3]; tplpar = pg + '|' + tpl[3]; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = String(tpl[3] || '').trim();
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) {
var sourceTalkText = talkText || '';
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 = article.replace(RegExp('(<noinclude>)?\\{\\{(' + tplPattern + ')\\|[\\s\\S]*?\\}\\}\\n?(<\\/noinclude>)?\\n?', 'gi'), '');
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 };
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';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
function finishConflictResolution(sourceText) {
var resolvedText;
if (conflictDecision.templateAction === 'keep') {
return { skip: true, meta: { successMessage: 'Шаблон на странице «' + normTitle(pg) + '» оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице «' + normTitle(pg) + '» перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице «' + normTitle(pg) + '».'
}
};
}
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 statusId = logStatus('Обрабатывается страница «' + normTitle(pg) + '»...', 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('Завершение по странице «' + normPg + '»', err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы «' + normPg + '».', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы «' + normPg + '».', 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 ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} 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 = collectInputValues('.rmArticleInput');
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
// Для мультиноминации msg расширяется заголовками статей
msg = buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
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,
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';
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op, event) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({ title: 'Быстрое удаление', width: 'compact' });
var html = '<select id="rmSel" style="' + stInputFull + '">';
reasons.forEach(function (r, i) { html += '<option value="' + i + '">' + r[1] + '</option>'; });
html += '</select>';
html += '<input id="fiRm" type="hidden" style="' + stInputFull + '">';
html += '<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="' + stInputFull + '">';
html += buildQuickPhrasesPanelHtml('fiRmComment');
$('#removerModalContent').html(html);
$('#rmSel').change(function () {
var paramCfg = cfg.requiredParamTemplates[reasons[this.value][0]];
var showComment = true;
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').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];
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: makeSummary('номинация категории на быстрое удаление'), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
renderModalFooter('reload');
}
});
} 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 () { finalizeSuccess(null, true); });
}
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;
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
var html = '';
// Многостраничный режим (только КУ)
if (multiMode) {
html += '<div id="rmMultiHeader" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:6px;"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="' + stInputFull + '"></div>';
html += '<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:' + multiNominationGap + ';margin-bottom:' + multiNominationGap + ';">' +
buildMultiArticleRowHtml(0, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true }) +
'</div>';
}
// Дополнительные поля (переименование, объединение, разделение)
if (nom.extraInput) html += buildMultiInputHtml(nom.extraInput);
html += '<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('rmMsg');
// Блок переноса с КБУ/КУЛ (только КУ)
if (nom.supportsTransfer) {
html += '<div id="rmTransferBox" class="' + RESIZE_CLASS + ' rmTransferPanel" style="width:100%;box-sizing:border-box;">' +
'<div class="rmTransferGrid">' +
'<div class="rmTransferFieldLabel">Режим номинации</div>' +
'<div class="rmTransferFieldLabel">Перенос со снятием шаблонов</div>' +
'<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>';
}
$('#removerModalContent').html(html);
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
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 + '\n' : '');
}
$(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) {
var articleCounter = 1;
function ensureArticleCommentTextareaResizer(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 setArticleCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureArticleCommentTextareaResizer(textareaId, wrapId);
}
function updateMultiMode() {
var hasExtra = $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (!hasExtra) $('#rmHeader').val('');
$('.rmArticleCommentToggle').toggle(hasExtra);
if (!hasExtra) {
$('.rmArticleCommentToggle').each(function () {
setArticleCommentExpanded($(this), false);
});
}
syncModalLayout();
}
$('#rmAddArticle').click(function () {
$('#rmArticlesContainer').append(buildMultiArticleRowHtml(articleCounter++));
updateMultiMode();
});
$(document).off('click.rmArticleComment').on('click.rmArticleComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setArticleCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmArticle').on('click.rmArticle', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticle0').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 && !RE_DATE_ISO.test(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();
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var html = vCfg ? buildMultiInputHtml(vCfg) : '';
html += '<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>' + buildQuickPhrasesPanelHtml('nominationReason');
$('#removerModalContent').html(html);
setupResizableModal('nominationReason');
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = $('#nominationReason').val().trim();
var pageName = normTitle(mwCfg.wgPageName);
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplateToCategory(mwCfg.wgPageName, catType, mainName, additionalNames, function (err) { next(err); });
},
nominationStep: function (done) {
createCategoryDiscussion(pageName, reason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages([pageName], {
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';
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>'
});
var html;
if (isZka) {
html = '<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="' + stInputFull + '" value="' + escapeHtml(ctx.pageLink) + '">';
} else {
html =
'<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;"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="' + stInputFull + '"></div>' +
'<div id="rmProtectFirstRow" style="' + stRow + '">' +
'<input id="rmProtectPage0" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '" value="' + escapeHtml(ctx.pageName) + '">' +
'<button id="rmProtectAddPage" type="button" style="' + stToolBtn + '">+ Страница</button></div>' +
'<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>';
}
if (!isZka) html += '<div id="rmProtectTextBlock" class="' + RESIZE_CLASS + '">';
html += '<textarea id="rmReportText" placeholder="' + (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически."></textarea>' + buildQuickPhrasesPanelHtml('rmReportText');
if (!isZka) html += '</div>';
$('#removerModalContent').html(html);
if (!isZka) {
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');
}
var pageCounter = 1;
function updateProtectMultiUi() {
$('#rmProtectHeaderWrap').toggle($('#rmProtectPagesContainer .rmProtectPageRow').length >= 1);
syncModalLayout();
}
$('#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(
'<div class="rmProtectPageRow ' + RESIZE_CLASS + '" style="' + stRow + '">' +
'<input id="' + id + '" type="text" placeholder="Страница" class="rmProtectPageInput" style="' + stInputBox + '">' +
'<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button></div>'
);
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 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) {
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: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
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) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
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) {
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 + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames) {
var cb = callback || function () {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames);
var discText = discTitle + appendNominationSignature(reason) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary('добавление обсуждения для категории [[:' + pageName + ']]'),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, 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) {
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 showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text) {
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(
'<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>'
);
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 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 = {
section: '0',
appendtext: '\n\n== ' + header + ' ==\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 = {
appendtext: '\n\n== ' + sectionTitle + ' ==\n' + pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
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 };
} catch (e) {
console.error('RemoverCore: ошибка инициализации:', e);
throw e;
}
}());
dgrvg5ebce70dss7emsyi0drfemact7
Wikipedia:NewPage1
4
174854
738042
2026-04-15T14:15:57Z
~2026-23347-42
73585
Created page with "this is a new page"
738042
wikitext
text/x-wiki
this is a new page
k3k2n6yaic8gwi9dv3xjmg5s2ji37m7
Wikipedia talk:NewPage1
5
174855
738043
2026-04-15T14:18:55Z
~2026-23347-42
73585
/* thisi s an edit */ new section showcaptcha
738043
wikitext
text/x-wiki
== thisi s an edit ==
this is a new edit [[Special:Contributions/~2026-23347-42|~2026-23347-42]] ([[User talk:~2026-23347-42|talk]]) 14:18, 15 April 2026 (UTC)
h7n4rg3u7qnhiplnzulx3cwpsmdg1d3
Event:13May new1
1728
174856
738049
2026-04-15T14:32:02Z
~2026-23347-42
73585
Created page with "new event"
738049
wikitext
text/x-wiki
new event
9jb587h0uutihhlqzhn36khatcjruut
User:Wooze/common.js
2
174857
738051
2026-04-15T14:40:29Z
Wooze
54732
Created page with "==/UserScript== (function () { if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') return; const api = new mw.Api(); const summaryText = "$USER tarafından yapılan değişiklikler geri alındı"; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getRollbackLinks() { return document.querySelectorAll('a.mw-rollback-link'); } async function rollbackAll() { const links = getRollbackLinks(); for (l..."
738051
javascript
text/javascript
==/UserScript==
(function () { if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') return;
const api = new mw.Api();
const summaryText = "$USER tarafından yapılan değişiklikler geri alındı";
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getRollbackLinks() {
return document.querySelectorAll('a.mw-rollback-link');
}
async function rollbackAll() {
const links = getRollbackLinks();
for (let link of links) {
const url = new URL(link.href);
const title = url.searchParams.get('title');
const user = url.searchParams.get('from');
try {
await api.post({
action: 'rollback',
title: title,
user: user,
summary: summaryText.replace('$USER', user),
token: mw.user.tokens.get('rollbackToken')
});
console.log(`Geri alındı: ${title}`);
await sleep(800);
} catch (e) {
console.error('Hata:', e);
}
}
alert('Tüm işlemler tamamlandı');
}
function addCheckboxes() {
const items = document.querySelectorAll('.mw-contributions-list li');
items.forEach(item => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'massRollbackCheckbox';
checkbox.style.marginRight = '5px';
item.prepend(checkbox);
});
}
async function rollbackSelected() {
const items = document.querySelectorAll('.mw-contributions-list li');
for (let item of items) {
const checkbox = item.querySelector('.massRollbackCheckbox');
if (!checkbox || !checkbox.checked) continue;
const link = item.querySelector('a.mw-rollback-link');
if (!link) continue;
const url = new URL(link.href);
const title = url.searchParams.get('title');
const user = url.searchParams.get('from');
try {
await api.post({
action: 'rollback',
title: title,
user: user,
summary: summaryText.replace('$USER', user),
token: mw.user.tokens.get('rollbackToken')
});
console.log(`Seçili geri alındı: ${title}`);
await sleep(800);
} catch (e) {
console.error('Hata:', e);
}
}
alert('Seçili işlemler tamamlandı');
}
function addButtons() {
mw.util.addPortletLink('p-tb', '#', 'Hepsini geri al', 'rollback-all', 'Tüm katkıları geri al');
mw.util.addPortletLink('p-tb', '#', 'Seçilenleri geri al', 'rollback-selected', 'Seçili katkıları geri al');
document.getElementById('rollback-all').addEventListener('click', function (e) {
e.preventDefault();
rollbackAll();
});
document.getElementById('rollback-selected').addEventListener('click', function (e) {
e.preventDefault();
rollbackSelected();
});
}
addCheckboxes();
addButtons();
})();
jgba0hgoh433x03mrstcc9pwbpd6flj
User:TurkMapper
2
174858
738068
2026-04-15T18:44:26Z
HakanIST
30391
HakanIST moved page [[User:TurkMapper]] to [[User:At1as]]: Automatically moved page while renaming the user "[[Special:CentralAuth/TurkMapper|TurkMapper]]" to "[[Special:CentralAuth/At1as|At1as]]"
738068
wikitext
text/x-wiki
#REDIRECT [[User:At1as]]
{{Redirect category shell|
{{R from move}}
}}
4no2q0rdnv101jclidqyxtgfni196x7
User:PerfektesChaos/createwithcontentmodel
2
174859
738070
2026-04-15T19:13:13Z
PerfektesChaos
18104
PerfektesChaos created the page [[User:PerfektesChaos/createwithcontentmodel]] using a non-default content model "plain text"
738070
text
text/plain
phoiac9h4m842xq45sp7s6u21eteeq1
738071
738070
2026-04-15T19:13:56Z
PerfektesChaos
18104
Test
738071
text
text/plain
Test
bop1vj5i98maix36pjrpgep1w6hnxfe
User:Perfektes Chaos/createwithcontentmodel
2
174860
738072
2026-04-15T19:15:37Z
PerfektesChaos
18104
PerfektesChaos created the page [[User:Perfektes Chaos/createwithcontentmodel]] using a non-default content model "plain text"
738072
text
text/plain
phoiac9h4m842xq45sp7s6u21eteeq1
User:Catrope/common.js
2
174861
738074
2026-04-15T21:32:42Z
Catrope
2565
Created page with "console.log('hello world');"
738074
javascript
text/javascript
console.log('hello world');
k8m0w5sqautivi2xifjhe21199xzcf1
Translations:User:Gnoeee/sandbox3/2/en
1198
174862
738117
2026-04-16T08:08:44Z
FuzzyBot
18251
Importing a new version from external source
738117
wikitext
text/x-wiki
=== OK ===
83cc2u1so84eevfdp0zu8m2g37fg9eg
Translations:User:Gnoeee/sandbox3/3/en
1198
174863
738118
2026-04-16T08:08:44Z
FuzzyBot
18251
Importing a new version from external source
738118
wikitext
text/x-wiki
=== Testing ===
This is a test
i5ssel4j9yr0nqcicp2zcxfl2c59hhl
User:Gnoeee/sandbox3/en
2
174864
738119
2026-04-16T08:08:45Z
FuzzyBot
18251
Updating to match new version of source page
738119
wikitext
text/x-wiki
Test Page
=== OK ===
=== Testing ===
This is a test
pjvuo5r45eysrm4fvq64xjzdso0sb58