Wikipedia
vlswiki
https://vls.wikipedia.org/wiki/Voorblad
MediaWiki 1.46.0-wmf.22
first-letter
Media
Specioal
Discuusje
Gebruker
Discuusje gebruker
Wikipedia
Discuusje Wikipedia
Ofbeeldienge
Discuusje ofbeeldienge
MediaWiki
Discuusje MediaWiki
Patrôon
Discuusje patrôon
Ulpe
Discuusje ulpe
Categorie
Discuusje categorie
TimedText
TimedText talk
Module
Overleg module
Event
Event talk
Wikipedia:Café
4
1439
326050
325906
2026-04-03T17:11:07Z
MediaWiki message delivery
10030
/* Action Required: Update templates/modules for electoral maps (Migrating from P1846 to P14226) */ nieuwe koutnanche
326050
wikitext
text/x-wiki
[[Categorie:Wikipedia:Café| ]]
<!--nobot--><div class=plainlinks style="text-align: center;"><big>[http://vls.wikipedia.org/w/wiki.phtml?title=Wikipedia:Café&action=edit§ion=new Over 'n twuk nieuws begunn]
[[Wikipedia:Café/Archief|'t Archief va de café bekykn]]</big>
__NEWSECTIONLINK__
</div>
[[Ofbeeldienge:Westvleteren-beer.jpg|80px|left|Westvleteren]]
Welgekommn in de '''café van de West-Vlamsche Wikipedia'''. Pakt junder ma e pinte en zet je: der es ier nôois gin belet. De café es de plekke woa da me discuteern over dingn die etwadde te moakn ein met Wikipedia of vo geweune gêestige koutnanche.<br>Te verkrygn an den bar: [[Trappist Westvleteren]], [[Bavik]], [[Brugse Tripel]], [[Brouwerij Rodenbach|Rôodnbach]], [[Ingelmunster]]s [[Kasteelbier|Kastêelbier]] en [[Hommelbier|Poperings Hommelbier]]...
<!-- uutzonderlik vo de café stoan de iw's vanboven - voor e gemakkelike archiverienge-->
<br clear="all"><!-- Alleen onder deze lijn archiveren! -->
== Thank You for Last Year – Join Wiki Loves Ramadan 2026 ==
Dear Wikimedia communities,
We hope you are doing well, and we wish you a happy New Year.
''Last year, we captured light. This year, we’ll capture legacy.''
In 2025, communities around the world shared the glow of Ramadan nights and the warmth of collective iftars. In 2026, ''Wiki Loves Ramadan'' is expanding, bringing more stories, more cultures, and deeper global connections across Wikimedia projects.
We invite you to explore the ''Wiki Loves Ramadan 2026'' [[m:Special:MyLanguage/Wiki Loves Ramadan 2026|Meta page]] to learn how you can participate and [[m:Special:MyLanguage/Wiki Loves Ramadan 2026/Participating communities|sign up]] your community.
📷 ''Photo campaign on '' [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan 2026|Wikimedia Commons]]
If you have questions about the project, please refer to the FAQs:
* [[m:Special:MyLanguage/Wiki Loves Ramadan/FAQ/|Meta-Wiki]]
* [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan/FAQ|Wikimedia Commons]]
''Early registration for updates is now open via the '''[[m:Special:RegisterForEvent/2710|Event page]]'''''
''Stay connected and receive updates:''
* [https://t.me/WikiLovesRamadan Telegram channel]
* [https://lists.wikimedia.org/postorius/lists/wikilovesramadan.lists.wikimedia.org/ Mailing list]
We look forward to collaborating with you and your community.
'''The Wiki Loves Ramadan 2026 Organizing Team''' 16 jan 2026 19:44 (UTC)
<!-- Bericht verzonden door User:ZI Jony@metawiki via de lijst op de pagina https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29879549 -->
== Feminism and Folklore 2026 starts soon ==
<div style="border:8px maroon ridge;padding:6px;">
[[File:Feminism and Folklore 2026 logo.svg|centre|550px|frameless]]
::<div lang="en" dir="ltr" class="mw-content-ltr">
<div style="text-align: center; width: 100%;">''{{int:please-translate}}''</div>
;Invitation to Organize Feminism and Folklore 2026
Dear Wiki Community,
We are pleased to invite Wikimedia communities, affiliates, and independent contributors to organize the '''[[:m:Feminism and Folklore 2026|Feminism and Folklore 2026]]''' writing competition on your local Wikipedia.
The international campaign will run from '''1 February to 31 March 2026''' and aims to improve coverage of feminism, women’s histories, gender-related topics, and folk culture across Wikipedia projects.
;About the Campaign
'''Feminism and Folklore''' is a global writing initiative that complements the '''[[:c:Commons:Wiki Loves Folklore 2026|Wiki Loves Folklore]]''' photography competition. While Wiki Loves Folklore focuses on visual documentation, this writing campaign addresses the '''gender gap on Wikipedia''' by improving encyclopedic content related to folk culture and marginalized voices.
;What Can Participants Write About?
Communities can contribute by creating, expanding, or translating articles related to:
* Folk festivals, rituals, and celebrations
* Folk dances, music, and traditional performances
* Women and queer figures in folklore
* Women in mythology and oral traditions
* Women warriors, witches, and witch-hunting narratives
* Fairy tales, folk stories, and legends
* Folk games, sports, and cultural practices
Participants may work from curated article lists or generate new article suggestions using campaign tools.
;How to Sign Up as an Organizer
Organizers are requested to complete the following steps to register their community:
# Create a local project page on your wiki [[:m:Feminism and Folklore/Sample|(see sample)]]
# Set up the campaign using the '''CampWiz''' tool
# Prepare a local article list and clearly mention:
#* Campaign timeline
#* Local and international prizes
# Request a site notice from local administrators [[:mr:Template:SN-FNF|(see sample)]]
# Add your local project page and CampWiz link to the '''[[:m:Feminism and Folklore 2026/Project Page|Meta project page]]'''
;Campaign Tools
The Wiki Loves Folklore Tech Team has introduced tools to support organizers and participants:
* '''Article List Generator by Topic''' – Helps identify articles available on English Wikipedia but missing in your local language Wikipedia. The tool allows customized filters and provides downloadable article lists in CSV and wikitable formats.
* '''CampWiz''' – Enables communities to manage writing campaigns effectively, including jury-based evaluation. This will be the third year CampWiz is officially used for Feminism and Folklore.
Both tools are now available for use in the campaign. '''[https://tools.wikilovesfolklore.org/ Click here to access the tools]'''
;Learn More & Get Support
For detailed information about rules, timelines, and prizes, please visit the
'''[[:m:Feminism and Folklore 2026|Feminism and Folklore 2026 project page]]'''.
If you have any questions or need assistance, feel free to reach out via:
* '''[[:m:Talk:Feminism and Folklore 2026/Project Page|Meta talk page]]'''
* Email us using details on the contact page.
;Join Us
We look forward to your collaboration and coordination in making Feminism and Folklore 2026 a meaningful and impactful campaign for closing gender gaps and enriching folk culture content on Wikipedia.
Thank you and best wishes,
'''[[:m:Feminism and Folklore 2026|Feminism and Folklore 2026 International Team]]'''
----
''Stay connected:''
[[File:B&W Facebook icon.png|link=https://www.facebook.com/feminismandfolklore/|30x30px]]
[[File:B&W Twitter icon.png|link=https://twitter.com/wikifolklore|30x30px]]
</div></div>
== Invitation to Host Wiki Loves Folklore 2026 in Your Country ==
<div lang="en" dir="ltr" class="mw-content-ltr">
<div style="text-align: center; width: 100%;">''{{int:please-translate}}''</div>
[[File:Wiki Loves Folklore Logo.svg|right|150px|frameless]]
Hello everyone,
We are delighted to invite Wikimedia affiliates, user groups, and community organizations worldwide to participate in '''Wiki Loves Folklore 2026''', an international initiative dedicated to documenting and celebrating folk culture across the globe.
;About Wiki Loves Folklore
'''Wiki Loves Folklore''' is an annual international photography competition hosted on Wikimedia Commons. The campaign runs from '''1 February to 31 March 2026''' and encourages photographers, cultural enthusiasts, and community members to contribute photographs that highlight:
* Folk traditions and rituals
* Cultural festivals and celebrations
* Traditional attire and crafts
* Performing arts, music, and dance
* Everyday practices rooted in folk heritage
Through this campaign, we aim to preserve and promote diverse folk cultures and make them freely accessible to the world.
[[:c:Commons:Wiki_Loves_Folklore_2026|Project page on Wikimedia Commons]]
; Host a Local Edition
As we celebrate the '''eight edition''' of Wiki Loves Folklore, we warmly invite communities to organize a local edition in their country or region. Hosting a local campaign is a great opportunity to:
* Increase visibility of your region’s folk culture
* Engage new contributors in your community
* Enrich Wikimedia Commons with high-quality cultural content
'''[[:c:Commons:Wiki_Loves_Folklore_2026/Organize|Sign up to organize]]:'''
If your team prefers to organize the competition in ''either February or March only'', please feel free to let us know.
If you are unable to organize, we encourage you to share this opportunity with other interested groups or organizations in your region.
;Get in Touch
If you have any questions, need support, or would like to explore collaboration opportunities, please feel free to contact us via:
* The project Talk pages
* Email: '''support@wikilovesfolklore.org'''
We are also happy to connect via an online meeting if your team would like to discuss planning or coordination in more detail.
Warm regards,
'''The Wiki Loves Folklore International Team'''
</div>
[[Gebruker:MediaWiki message delivery|MediaWiki message delivery]] ([[Discuusje gebruker:MediaWiki message delivery|overleg]]) 18 jan 2026 13:21 (UTC)
<!-- Bericht verzonden door User:Tiven2240@metawiki via de lijst op de pagina https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery/Wikipedia&oldid=29228188 -->
== <span lang="en" dir="ltr">Annual review of the Universal Code of Conduct and Enforcement Guidelines</span> ==
<div lang="en" dir="ltr">
<section begin="announcement-content" />
I am writing to you to let you know the annual review period for the Universal Code of Conduct and Enforcement Guidelines is open now. You can make suggestions for changes through 9 February 2026. This is the first step of several to be taken for the annual review. [[m:Special:MyLanguage/Universal Code of Conduct/Annual review/2026|Read more information and find a conversation to join on the UCoC page on Meta]].
The [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee|Universal Code of Conduct Coordinating Committee]] (U4C) is a global group dedicated to providing an equitable and consistent implementation of the UCoC. This annual review was planned and implemented by the U4C. For more information and the responsibilities of the U4C, [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee/Charter|you may review the U4C Charter]].
Please share this information with other members in your community wherever else might be appropriate.
-- In cooperation with the U4C, [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|talk]])<section end="announcement-content" />
</div>
19 jan 2026 21:02 (UTC)
<!-- Bericht verzonden door User:Keegan (WMF)@metawiki via de lijst op de pagina https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=29905753 -->
== Join the sixth Ukraine’s Cultural Diplomacy Month on Wikipedia! ==
<div lang="en" dir="ltr">
[[File:Ukraine’s Cultural Diplomacy Month on Wikipedia 2026.png|right|250px|thumb|link=https://meta.wikimedia.org/wiki/Ukraine%27s_Cultural_Diplomacy_Month_2026|Join our campaign!]]
{{int:please-translate}}
Dear Wikipedians!
[[:m:Special:MyLanguage/Wikimedia Ukraine|Wikimedia Ukraine]], in cooperation with the [[:en:Ministry of Foreign Affairs of Ukraine|MFA of Ukraine]] and [[:en:Ukrainian Institute|Ukrainian Institute]], has launched the sixth edition of writing challenge "'''[[:m:Special:MyLanguage/Ukraine's Cultural Diplomacy Month 2026|Ukraine's Cultural Diplomacy Month]]'''", which lasts from '''1st April''' until '''30th April 2026'''.
The initiative aims to promote knowledge about Ukrainian culture abroad by creating and improving Wikipedia articles in multiple languages. This year marks the sixth edition of the campaign, which will focus on contemporary culture, making today’s artistic voices and practices more visible to international audiences.
🧩'''How to participate?'''
Choose an article from the suggested list → Write an article in your language, or improve an existing one according to the rules → Add your contribution to the contest page and calculate your points → Win prizes and receive a certificate of participation → Become a promoter of truthful knowledge about Ukraine.
🧩'''[[m:Special:MyLanguage/Ukraine's Cultural Diplomacy Month 2026|Check our main page for more information]]'''.
'''If you are interested in coordinating long-term community engagement for the campaign and becoming a local ambassador, we would love to hear from you! Please let us know your interest.'''
If not, then we encourage you to translate the [[m:Special:MyLanguage/Ukraine's Cultural Diplomacy Month 2026|landing page of the contest]] and [https://meta.wikimedia.org/wiki/Special:MessageGroupStats?group=Centralnotice-tgroup-UCDM2026banner&messages=&language=en&x=D banner] into your own language.
Also, we set up a [[:m:CentralNotice/Request/Ukraine's Cultural Diplomacy Month 2026|banner]] to notify users of the possibility to participate in this challenge!
[[:m:User:OlesiaLukaniuk (WMUA)|OlesiaLukaniuk (WMUA)]] ([[:m:User talk:OlesiaLukaniuk (WMUA)|talk]]) 04:35, 1 April 2026 (UTC)
</div>
<!-- Bericht verzonden door User:OlesiaLukaniuk (WMUA)@metawiki via de lijst op de pagina https://meta.wikimedia.org/w/index.php?title=User:OlesiaLukaniuk_(WMUA)/list_of_wikis&oldid=28552112 -->
== Action Required: Update templates/modules for electoral maps (Migrating from P1846 to P14226) ==
Hello everyone,
This is a notice regarding an ongoing data migration on Wikidata that may affect your election-related templates and Lua modules (such as <code>Module:Itemgroup/list</code>).
'''The Change:'''<br />
Currently, many templates pull electoral maps from Wikidata using the property [[:d:Property:P1846|P1846]], combined with the qualifier [[:d:Property:P180|P180]]: [[:d:Q19571328|Q19571328]].
We are migrating this data (across roughly 4,000 items) to a newly created, dedicated property: '''[[:d:Property:P14226|P14226]]'''.
'''What You Need To Do:'''<br />
To ensure your templates and infoboxes do not break or lose their maps, please update your local code to fetch data from [[:d:Property:P14226|P14226]] instead of the old [[:d:Property:P1846|P1846]] + [[:d:Property:P180|P180]] structure. A [[m:Wikidata/Property Migration: P1846 to P14226/List|list of pages]] was generated using Wikimedia Global Search.
'''Deadline:'''<br />
We are temporarily retaining the old data on [[:d:Property:P1846|P1846]] to allow for a smooth transition. However, to complete the data cleanup on Wikidata, the old [[:d:Property:P1846|P1846]] statements will be removed after '''May 1, 2026'''. Please update your modules and templates before this date to prevent any disruption to your wiki's election articles.
Let us know if you have any questions or need assistance with the query logic. Thank you for your help! [[User:ZI Jony|ZI Jony]] using [[Gebruker:MediaWiki message delivery|MediaWiki message delivery]] ([[Discuusje gebruker:MediaWiki message delivery|overleg]]) 3 apr 2026 17:11 (UTC)
<!-- Bericht verzonden door User:ZI Jony@metawiki via de lijst op de pagina https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29941252 -->
59skg2byv87cpkpy2pgcfw8bp393vx6
MediaWiki:Common.css
8
1534
326046
292347
2026-04-03T15:54:48Z
Kannotlogin
29153
326046
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
21rgmz9zj44trzeqv5yjrsz5w6674at
326267
326046
2026-04-04T11:23:08Z
Kannotlogin
29153
326267
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
/* Icoontjes vo de "In andere projectn" zy-balke en Wikidata */
.wb-otherproject-commons a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/16px-Commons-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-foundation a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-mediawiki a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/16px-MediaWiki-2020-icon.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-meta a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Wikimedia_Community_Logo.svg/16px-Wikimedia_Community_Logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-outreach a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-sources a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/16px-Wikisource-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-species a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/16px-Wikispecies-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-wikidata a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-wikifunctions a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/16px-Wikifunctions-logo.svg.png') no-repeat left center; padding-left: 20px; }
.wb-otherproject-wikimania a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Wikimania_logo.svg/16px-Wikimania_logo.svg.png') no-repeat left center; padding-left: 20px; }
/* Icoontje vo de 'Wikidata-item' link onder ulpmiddels */
#t-wikibase a { background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat left center; padding-left: 20px; }
e21s4e8u8q7q1mtnlmf30h672re9pua
326268
326267
2026-04-04T11:24:44Z
Kannotlogin
29153
326268
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
/* Icoontjes vo de "In andere projectn" zy-balke en Wikidata */
li.wb-otherproject-commons a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/16px-Commons-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-foundation a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-mediawiki a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/16px-MediaWiki-2020-icon.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-meta a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Wikimedia_Community_Logo.svg/16px-Wikimedia_Community_Logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-outreach a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-sources a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/16px-Wikisource-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-species a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/16px-Wikispecies-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-wikidata a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-wikifunctions a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/16px-Wikifunctions-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
li.wb-otherproject-wikimania a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Wikimania_logo.svg/16px-Wikimania_logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
/* Icoontje vo de 'Wikidata-item' link onder ulpmiddels */
li#t-wikibase a { background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') !important; background-repeat: no-repeat !important; background-position: left center !important; padding-left: 24px !important; }
42x37dwt7k3su310p5bmq7lird8cgba
326269
326268
2026-04-04T11:26:54Z
Kannotlogin
29153
326269
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
/* Verberg het standaard grijze Vector 2022-icoontje voor deze specifieke links */
.wb-otherproject-commons .vector-icon, .wb-otherproject-foundation .vector-icon,
.wb-otherproject-mediawiki .vector-icon, .wb-otherproject-meta .vector-icon,
.wb-otherproject-outreach .vector-icon, .wb-otherproject-sources .vector-icon,
.wb-otherproject-species .vector-icon, .wb-otherproject-wikidata .vector-icon,
.wb-otherproject-wikifunctions .vector-icon, .wb-otherproject-wikimania .vector-icon,
#t-wikibase .vector-icon {
display: none !important;
}
/* Injecteer de originele gekleurde logo's net vóór de tekst */
.wb-otherproject-commons a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/16px-Commons-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-foundation a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-mediawiki a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/16px-MediaWiki-2020-icon.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-meta a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Wikimedia_Community_Logo.svg/16px-Wikimedia_Community_Logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-outreach a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-sources a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/16px-Wikisource-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-species a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/16px-Wikispecies-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikidata a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikifunctions a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/16px-Wikifunctions-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikimania a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Wikimania_logo.svg/16px-Wikimania_logo.svg.png') no-repeat center; background-size: contain; }
/* Icoontje vo de 'Wikidata-item' link onder ulpmiddels */
#t-wikibase a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
rd8mu33g0ieekufe4wmcxhpnjk5jqei
326270
326269
2026-04-04T11:28:36Z
Kannotlogin
29153
326270
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
/* Verberg het standaard grijze Vector 2022-icoontje voor deze specifieke links */
.wb-otherproject-commons .vector-icon, .wb-otherproject-foundation .vector-icon,
.wb-otherproject-mediawiki .vector-icon, .wb-otherproject-meta .vector-icon,
.wb-otherproject-outreach .vector-icon, .wb-otherproject-sources .vector-icon,
.wb-otherproject-species .vector-icon, .wb-otherproject-wikidata .vector-icon,
.wb-otherproject-wikifunctions .vector-icon, .wb-otherproject-wikimania .vector-icon,
#t-wikibase .vector-icon {
display: none !important;
}
/* Injecteer de originele gekleurde logo's net vóór de tekst */
.wb-otherproject-commons a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/16px-Commons-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-foundation a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-mediawiki a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/16px-MediaWiki-2020-icon.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-meta a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Wikimedia_Community_Logo.svg/16px-Wikimedia_Community_Logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-outreach a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-sources a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/16px-Wikisource-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-species a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/16px-Wikispecies-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikidata a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikifunctions a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/16px-Wikifunctions-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikimania a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/Wikimania.svg/16px-Wikimania.svg.png') no-repeat center; background-size: contain; }
/* Icoontje vo de 'Wikidata-item' link onder ulpmiddels */
#t-wikibase a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
ti60mbzp28vud5zicjyuuy23ur9cqzj
326271
326270
2026-04-04T11:30:44Z
Kannotlogin
29153
326271
css
text/css
/*
== Plainlinksneverexpand ==
Onderstaande is uit [[:en:MediaWiki:Common.css]] gekopieëerd.
Dit wordt o.a. gebruikt in [[Sjabloon:ref]].
''Add formatting to make sure that "external references" from [[Template:Ref]] do not get URL expansion, not even when printed. The mechanism up to MediaWiki 1.4 was that the HTML code contained a SPAN following the anchor A; this SPAN had the class "urlexpansion", which was not displayed on screen, but was shown when the medium was "print". The rules below ensure (a) that there is no extra padding to the right of the anchor (displayed as "[<number>]"), (b) that there is no "external link arrow" for the link, and (c) that this SPAN of class "urlexpansion" is never shown.''
<pre><nowiki>*/
.plainlinksneverexpand {
background: none ! important;
padding: 0 !important;
}
.fundraiser-box {display:none;}
.fundraiser-quote-box {display:none;}
.fundraiser-blog {display:none;}
.siteNoticeSmall {display:none;}
.plainlinksneverexpand .urlexpansion {
display : none ! important;
}
/* Make sure that ext links displayed within "plainlinksneverexpand" don't get
the arrow...
*/
.plainlinksneverexpand a {
background: none !important;
padding: 0 !important;
}
/* With MediaWiki 1.5, the mechanism has changed: instead of a SPAN of class "urlexpansion"
following the anchor A, the anchor itself now has class "external autonumber" and the
expansion is inserted when printing (see the common printing style sheet at
http://en.wikipedia.org/w/skins/common/commonPrint.css) using the ":after" pseudo-
element of CSS. We have to switch this off for links due to Template:Ref!
*/
.plainlinksneverexpand a.external.autonumber:after {
display: none !important;
}
/* Remove padding from external links displayed without icon */
#bodyContent .plainlinks a {
padding: 0 !important;
}
/*</pre>
== Tabelfuncties ==
=== Prettytable ===
Maakt een tabel met grijze achtergrond en lijnen. Voor los gebruik en in [[Sjabloon:Prettytable]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre>*/
.prettytable, .prettytable table
{
border: 1px solid #aaa;
border-collapse: collapse;
font-size: 95%;
margin: 1em 0;
background: #f9f9f9;
}
.prettytable table
{
margin: 0;
}
.prettytable td, table.prettytable th
{
border: 1px solid #aaa;
padding: 4px;
}
.prettytable th
{
background-color: #ddd;
}
/*</pre>
=== Overige functies ===
Kopcellen links uitlijnen.
<pre>*/
.thleft th, th.thleft
{
text-align: left;
}
/*</pre>
Ter vervanging van <code>valign=top</code>. (gebruik <code><nowiki>{| class="vatop"</nowiki></code>)
<pre>*/
.vatop tr, tr.vatop
{
vertical-align: top;
}
/*</pre>
== In- en uitklapfunctie ==
Wordt o.a. gebruikt in [[Sjabloon:Toggletext]] en [[Sjabloon:Navigatiezonder2]].
<pre>*/
div.UitklapFrame {
clear: both;
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrameNoClear {
margin: 0;
padding: 0;
border: 1px solid #aaa;
border-collapse: collapse;
background-color:#f9f9f9;
font-size: 95%;
}
div.UitklapFrame div.UitklapHead {
padding: 0 .5em;
text-align: center;
font-weight: bold;
background-color: #ddd;
}
div.UitklapFrame div.UitklapContent {
padding: 5px;
}
div.UitklapEind {
clear: both;
}
div.UitklapEindNoClear {
}
a.UitklapToggle {
font-size: x-small;
float:right;
padding: 0 .5em;
}
/* </pre>
== Kleine aanpassingen ==
Redirects cursief en groen in [[Special:Allpages]] en [[Special:Prefixindex]]. Eventueel overschrijven in bijvoorbeeld [[MediaWiki:Monobook.css|Monobook.css]], [[MediaWiki:Cologneblue.css|Cologneblue.css]].
<pre> */
.allpagesredirect a {
font-style: italic;
color: green;
}
.allpagesredirect a:visited {
color: #008000;
}
/* Makes redirects appear in italics in categories and on [[Special:Allpages]] */
.redirect-in-category, .allpagesredirect {
font-style: italic;
}
/* hiddenstructure wordt gebruikt in o.a. Sjabloon:Infobox_luchthaven maar werd alleen maar in de monobook skin gedefinieerd. Gekopieerd uit http://nl.wikipedia.org/w/skins/MonoBook/main.css */
.hiddenStructure {
display: none;
speak: none;
}
/* box op je volglijst met links naar andere volglijsten standaard niet afbebeeld */
/* om het te tonen verander je het in "display:block" in je eigen css */
#volglijstkader {
display: none;
}
/*</pre>
Optie "Markeer je eigen bewerkingen als gecontroleerd" verbergen:
<pre> */
input#autopatrol { display: none; }
/* </pre>
==CommonsTicker==
<pre> */
/************************/
/* CommonsTicker styles */
/************************/
/* Link */
.tickerDiffLink { } /* diff links in ticker */
.tickerMiscLink { } /* misc links in ticker */
.tickerUsage { font-size:80%; } /* ticker usage list */
/* per-type styles */
.tickerEntry_deleted { } /* entry for image deletion */
.tickerEntry_restored { } /* entry for restored image */
.tickerEntry_replaced { } /* entry for image replacement */
.tickerEntry_tagged { } /* entry for adding/removing problem tags */
.tickerEntry_redir { } /* entry for critical redirection (fot tag redirects) */
.tickerEntry_recat { } /* entry for critical re-categorization (for tag categories) */
.tickerEntry_notify { } /* entry for global notifications */
.tickerEntry_changed { } /* entry for generic change */
/* per-status styles */
.tickerStatus_done { text-decoration:line-through; } /* strike through when entry has been handeled */
/* per-action styles */
.tickerAction_deleted { background:#F88; } /* action marker for image deletion */
.tickerAction_restored { background:#8F8; } /* action marker for restored image */
.tickerAction_replaced { background:#FED; } /* action marker for image replacement */
.tickerAction_deletedRev { background:#FDD; } /* action marker for revision deletion */
.tickerAction_replacedOwn { background:none; } /* action marker for image replacement by uploader */
.tickerAction_addedBad { background:#FDD; } /* action marker for adding problem markers */
.tickerAction_removedBad { background:#DFD; } /* action marker for removing problem markers */
.tickerAction_addedGood { background:#DFD; } /* action marker for adding license markgers (for tag categories) */
.tickerAction_removedGood { background:#FDD; } /* action marker for removing license markers (for tag categories) */
/* minor entry styles */
.tickerMinorEntry { color:#666; } /* minor entry */
.tickerMinorEntry a,
.tickerMinorEntry a:link,
.tickerMinorEntry a:visited { color:#669; }
#bodyContent .tickerMinorEntry a.extiw,
#bodyContent .tickerMinorEntry a.extiw:link,
#bodyContent .tickerMinorEntry a.extiw:visited { color:#669; }
.tickerTemplateEntry { font-weight: bold; } /* entry applies to a template used by multiple images */
.tickerSubEntry { } /* sub-entry for multi-image entry */
/*</nowiki></pre>*/
/* == Status Bevestigde Gebrukers == */
/* Standaard weergave (niet-bevestigd) */
.vls-autoconfirmed { display: none !important; }
.vls-unconfirmed { display: inline !important; }
/* Verberg het standaard grijze Vector 2022-icoontje voor deze specifieke links */
.wb-otherproject-commons .vector-icon, .wb-otherproject-foundation .vector-icon,
.wb-otherproject-mediawiki .vector-icon, .wb-otherproject-meta .vector-icon,
.wb-otherproject-outreach .vector-icon, .wb-otherproject-sources .vector-icon,
.wb-otherproject-species .vector-icon, .wb-otherproject-wikidata .vector-icon,
.wb-otherproject-wikifunctions .vector-icon, .wb-otherproject-wikimania .vector-icon,
#t-wikibase .vector-icon {
display: none !important;
}
/* Injecteer de originele gekleurde logo's net vóór de tekst */
.wb-otherproject-commons a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/16px-Commons-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-foundation a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-mediawiki a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/16px-MediaWiki-2020-icon.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-meta a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/a/af/Wikimedia_Community_Logo.svg') no-repeat center; background-size: contain; }
.wb-otherproject-outreach a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/16px-Wikimedia-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-sources a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/16px-Wikisource-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-species a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/16px-Wikispecies-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikidata a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikifunctions a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/16px-Wikifunctions-logo.svg.png') no-repeat center; background-size: contain; }
.wb-otherproject-wikimania a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/c/c3/Wikimania_logo.svg') no-repeat center; background-size: contain; }
/* Icoontje vo de 'Wikidata-item' link onder ulpmiddels */
#t-wikibase a::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: text-bottom; background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/16px-Wikidata-logo.svg.png') no-repeat center; background-size: contain; }
33j3nh63eyrlr356l31s3kstouvpag9
Trento
0
24383
326052
298519
2026-04-03T18:37:20Z
Unikalinho
14338
326052
wikitext
text/x-wiki
{{dbintro}}
* [[Trentino|Provincia autonoma di Trento]], autonome provinsje in Nôord-[[Itoalië]]
* [[Trento (stad)]], stad in de provinsje Trentino
{{db}}
2rqf04bl2hyi4nfipyxul7q6tavjzfn
Huus Capet-Artois
0
24464
326036
298763
2026-04-03T13:13:47Z
Kontributor 2K
26728
Woapn @common file
326036
wikitext
text/x-wiki
[[File:Blason comtes fr d'Artois 1.svg|thumb|<center>'t Woapn van Artois]]
Et '''Huus Capet-Artois''' (1237-1472) was e joungern tak van et [[Huus Capet]], gesticht deur [[Robert I van Artois]], e zeune van keunienk [[Louis VIII van Vrankryk]] en Blanche van Castilië. In 1237 krêeg Robert I et [[Groafschap Artois]] van zyn voader in apanage.
==Groavn van et Huus Capet-Artois==
*1237-1249: [[Robert I van Artois]]
*1250-1302: [[Robert II van Artois]]
*1302-1329: Mahaut van Artois (betwist deur Robert III van Artois)
In 1285 trouwde Mahaut van Artois met Otto IV van Bourgondië. Under dochter Johanna II van Bourgondië erfde 't groafschap Bourgondië van heur voader en 't groafschap Artois van heur moeder.
[[Categorie:Groaf van Artois]]
4qllc29wiwddvtg6qcfsn3263puz6wa
Wikipedia:Botn
4
29021
326037
326023
2026-04-03T14:07:15Z
Kannotlogin
29153
Doorverwijzing naar [[Wikipedia:Botn/Archief]] verwijderd
326037
wikitext
text/x-wiki
[[Ofbeeldienge:Enon robot.jpg|thumb|right|220px|Nen bot int echt.]]
Nen '''robotgebruker''' of geweune '''bot''' is e computerprogramma da zelve, (semi-)automatisch, bewerkiengn uutvoert ip Wikipedia. Ne bot wordt bestuurd deur e geweune menschelikke gebruker (den eignoare) die em zegt wa dat 'n moe doene. Ze wordn surtout gebruukt vo repititieve werkskes lijk 't verbetern van spelfoutn, 't toevoegn van categorieën of 't anpassn van kapotte links.
Deur bots een officiële '''botstatus''' te geevn, wordn nunder massale bewerkiengn verborgen ip de lyste van [[Specioal:RecenteWijzigingen|recente veranderiengn]]. Zo bluuft de wiki overzichtelik vo de geweune menschn, moar kunn'n de bots toch nunder werk doen.
== Regels vo bots ip de VLS wiki ==
Om ier e bot te meugn droaien, moe je je an de volgende regels oudn (gebaseerd ip 't [[:m:Bot policy|globaal botbeleid van Wikimedia]]):
* '''Aparte account:''' E bot moe droaien ip een aparte account, en 't woord "Bot" moe in de noame stoan (bv. ''JanBot'').
* '''Gebrukerspagina:''' Ip de gebrukerspagina van de bot moe dudelik stoan wie den eignoare is en wa de bot zjuuste doet.
* '''Gin controversiële bewerkiengn:''' Bots meugen gin massale anpassiengn doen woar da discussie over ku bestoan, zounder da 't êest overlegd is met de gemêenschap.
* '''Snelied:''' Bots zounder botstatus meugen maximoal 1 bewerkienge per minute doen. Bots mét botstatus meugen rapper goan, moar meugen de servers noois overbelastn.
* '''Verantwoordelikied:''' Den eignoare is OSSAN verantwoordelik vo wa zyn bot uutspookt. As e bot de wiki upfokt of foutn makt, wordt ie direct geblokkeerd.
== Botstatus anvroagn ==
Der zyn twêe maniern om een officiële botstatus te krygn:
# '''Globale bots:''' Deze wiki lat globale bots toe. Da zyn bots die al ip vele andere wiki's actief zyn en nunder status via de Stewards ip Meta-Wiki gekreegn èn.
# '''Lokale bots:''' As je e nieuwe bot èt specifiek vo de VLS wiki, moe je de botstatus anvroagn in de [[{{ns:project}}:Café|Café]]. As der achter e poar doagn gin bezwoar is van d'andere gebrukers, kan e lokale [[{{ns:project}}:Burocraatn|Burocraat]] (of e Steward) de botstatus toekenn'n.
== Lyste van bots ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve bots ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Specioal:Gebrukers|group=bot}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Bot-lyste</span>]</div>
== Archief ==
Oude bot-anvroagn uut 't verleden zyn ier te viendn: '''[[{{ns:project}}:Botn/Archief|Archief van bot-anvroagn]]'''.
[[Categorie:Wikipedia]]
51mv08w9qd2nk3z5onmns4aph890ere
326038
326037
2026-04-03T14:12:11Z
Kannotlogin
29153
326038
wikitext
text/x-wiki
[[Ofbeeldienge:Enon robot.jpg|thumb|right|220px|Nen bot int echt.]]
Nen '''robotgebruker''' of geweune '''bot''' is e computerprogramma da zelve, (semi-)automatisch, bewerkiengn uutvoert ip Wikipedia. Ne bot wordt bestuurd deur e geweune menschelikke gebruker (den eignoare) die em zegt wa dat 'n moe doene. Ze wordn surtout gebruukt vo repititieve werkskes lijk 't verbetern van spelfoutn, 't toevoegn van categorieën of 't anpassn van kapotte links.
Deur bots een officiële '''botstatus''' te geevn, wordn nunder massale bewerkiengn verborgen ip de lyste van [[Specioal:RecenteWijzigingen|recente veranderiengn]]. Zo bluuft de wiki overzichtelik vo de geweune menschn, moar kunn'n de bots toch nunder werk doen.
== Regels vo bots ip de VLS wiki ==
Om ier e bot te meugn droaien, moe je je an de volgende regels oudn (gebaseerd ip 't [[:m:Bot policy|globaal botbeleid van Wikimedia]]):
* '''Aparte account:''' E bot moe droaien ip een aparte account, en 't woord "Bot" moe in de noame stoan (bv. ''JanBot'').
* '''Gebrukerspagina:''' Ip de gebrukerspagina van de bot moe dudelik stoan wie den eignoare is en wa de bot zjuuste doet.
* '''Gin controversiële bewerkiengn:''' Bots meugen gin massale anpassiengn doen woar da discussie over ku bestoan, zounder da 't êest overlegd is met de gemêenschap.
* '''Snelied:''' Bots zounder botstatus meugen maximoal 1 bewerkienge per minute doen. Bots mét botstatus meugen rapper goan, moar meugen de servers noois overbelastn.
* '''Verantwoordelikied:''' Den eignoare is OSSAN verantwoordelik vo wa zyn bot uutspookt. As e bot de wiki upfokt of foutn makt, wordt ie direct geblokkeerd.
=== Massoal artikels anmoakn ===
In 't verleen is gebleekn da 't massoal en automatisch anmoakn van nieuwe artikels (bv. over olle gemêentn in e bepoald land) vo vele upfok kan zurgn. As je e bot wilt gebruukn om massoal artikels t'importeern of an te moakn, moe je dit êest dudelik meldn en bespreekn in de [[{{ns:project}}:Café|Café]].
== Gereedschap en Gebruuk ==
Je moe zelve gin informaticus zyn om e bot te droaien. Der bestoan teegnwoordig programma's en tools da je ku gebruukn:
* [[Wikipedia:AutoWikiBrowser|AutoWikiBrowser (AWB)]]: E programma da je ku downloadn om stief zêre (semi-)automatisch anpassiengn te doen.
* '''Pywikibot''': E bibliotheek in Python die vele gebruukt wordt vo 't zwoardere bot-werk.
== Botstatus anvroagn of ofpakkn ==
Der zyn twêe maniern om een officiële botstatus te krygn:
# '''Globale bots:''' Deze wiki lat globale bots toe. Da zyn bots die al ip vele andere wiki's actief zyn en nunder status via de Stewards ip Meta-Wiki gekreegn èn.
# '''Lokale bots:''' As je e nieuwe bot èt specifiek vo de VLS wiki, moe je de botstatus anvroagn in de [[{{ns:project}}:Café|Café]]. As der achter e poar doagn gin bezwoar is van d'andere gebrukers, kan e lokale [[{{ns:project}}:Burocraatn|Burocraat]] (of e Steward) de botstatus toekenn'n.
'''Innoame botstatus:'''
De botstatus kan were ofgepakt wordn (door e Burocraat of Steward) as: den eignoare 't zelve vroagt, de bot de regels brikt, of as de bot ol joarenlangn nimeer actief is gewist ip de wiki.
== Systeembots ==
Nevest de geweune bots zyn der ouk systeembots die angetuurd wordn deur de MediaWiki-software zelve (lik de ''MediaWiki message delivery''). Die zurgn datter achter de schermn olles bluuft droaien. Oa je doa probleemn mee èt, moe je nie ip de VLS wiki zyn, moa moet er e bugreport by Wikimedia ingediend wordn.
== Lyste van bots ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve bots ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Specioal:Gebrukers|group=bot}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Bot-lyste</span>]</div>
== Archief ==
Oude bot-anvroagn uut 't verleden zyn ier te viendn: '''[[{{ns:project}}:Botn/Archief|Archief van bot-anvroagn]]'''.
[[Categorie:Wikipedia]]
kxnvce88z5072tr58giuw6msaae494n
326041
326038
2026-04-03T14:37:53Z
Kannotlogin
29153
326041
wikitext
text/x-wiki
[[Ofbeeldienge:Enon robot.jpg|thumb|right|220px|Nen bot int echt.]]
Nen '''robotgebruker''' of geweune '''bot''' is e computerprogramma da zelve, (semi-)automatisch, bewerkiengn uutvoert ip Wikipedia. Ne bot wordt bestuurd deur e geweune menschelikke gebruker (den eignoare) die em zegt wa dat 'n moe doene. Ze wordn surtout gebruukt vo repititieve werkskes lijk 't verbetern van spelfoutn, 't toevoegn van categorieën of 't anpassn van kapotte links.
Deur bots een officiële '''botstatus''' te geevn, wordn nunder massale bewerkiengn verborgen ip de lyste van [[Specioal:RecenteWijzigingen|recente veranderiengn]]. Zo bluuft de wiki overzichtelik vo de geweune menschn, moar kunn'n de bots toch nunder werk doen.
== Regels vo bots ip de VLS wiki ==
Om ier e bot te meugn droaien, moe je je an de volgende regels oudn (gebaseerd ip 't [[:m:Bot policy|globaal botbeleid van Wikimedia]]):
* '''Aparte account:''' E bot moe droaien ip een aparte account, en 't woord "Bot" moe in de noame stoan (bv. ''JanBot'').
* '''Gebrukerspagina:''' Ip de gebrukerspagina van de bot moe dudelik stoan wie den eignoare is en wa de bot zjuuste doet.
* '''Gin controversiële bewerkiengn:''' Bots meugen gin massale anpassiengn doen woar da discussie over ku bestoan, zounder da 't êest overlegd is met de gemêenschap.
* '''Snelied:''' Bots zounder botstatus meugen maximoal 1 bewerkienge per minute doen. Bots mét botstatus meugen rapper goan, moar meugen de servers noois overbelastn.
* '''Verantwoordelikied:''' Den eignoare is OSSAN verantwoordelik vo wa zyn bot uutspookt. As e bot de wiki upfokt of foutn makt, wordt ie direct geblokkeerd.
=== Massoal artikels anmoakn ===
In 't verleen is gebleekn da 't massoal en automatisch anmoakn van nieuwe artikels (bv. over olle gemêentn in e bepoald land) vo vele upfok kan zurgn. As je e bot wilt gebruukn om massoal artikels t'importeern of an te moakn, moe je dit êest dudelik meldn en bespreekn in de [[{{ns:project}}:Café|Café]].
== Gereedschap en Gebruuk ==
Je moe zelve gin informaticus zyn om e bot te droaien. Der bestoan teegnwoordig programma's en tools da je ku gebruukn:
* [[Wikipedia:AutoWikiBrowser|AutoWikiBrowser (AWB)]]: E programma da je ku downloadn om stief zêre (semi-)automatisch anpassiengn te doen.
* '''Pywikibot''': E bibliotheek in Python die vele gebruukt wordt vo 't zwoardere bot-werk.
== Botstatus anvroagn of ofpakkn ==
Der zyn twêe maniern om een officiële botstatus te krygn:
# '''Globale bots:''' Deze wiki lat globale bots toe. Da zyn bots die al ip vele andere wiki's actief zyn en nunder status via de Stewards ip Meta-Wiki gekreegn èn.
# '''Lokale bots:''' As je e nieuwe bot èt specifiek vo de VLS wiki, moe je de botstatus anvroagn in de [[{{ns:project}}:Café|Café]]. As der achter e poar doagn gin bezwoar is van d'andere gebrukers, kan e lokale [[{{ns:project}}:Burocraatn|Burocraat]] (of e Steward) de botstatus toekenn'n.
'''Innoame botstatus:'''
De botstatus kan were ofgepakt wordn (door e Burocraat of Steward) as: den eignoare 't zelve vroagt, de bot de regels brikt, of as de bot ol joarenlangn nimeer actief is gewist ip de wiki.
== Systeembots ==
Nevest de geweune bots zyn der ouk systeembots die angetuurd wordn deur de MediaWiki-software zelve (lik de ''MediaWiki message delivery''). Die zurgn datter achter de schermn olles bluuft droaien. Oa je doa probleemn mee èt, moe je nie ip de VLS wiki zyn, moa moet er e bugreport by Wikimedia ingediend wordn.
== Lyste van bots ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve bots ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=bot}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Bot-lyste</span>]</div>
== Archief ==
Oude bot-anvroagn uut 't verleden zyn ier te viendn: '''[[{{ns:project}}:Botn/Archief|Archief van bot-anvroagn]]'''.
[[Categorie:Wikipedia]]
l59zk0lfrirsnn11tdg1zh7ns04h3oc
Wikipedia:Burocraatn
4
29023
326044
326027
2026-04-03T15:52:13Z
Kannotlogin
29153
Doorverwijzing naar [[Wikipedia:Burocraatn/Archief]] verwijderd
326044
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia bureaucrat.svg|thumb|right|150px|'t Officiële symbool vo burocraatn.]]
Nen '''burocraat''' is ne [[{{ns:project}}:Bêerders|bêerder]] ip Wikipedia die e poar extra, stief belangryke technische rechtn eit. 't Zyn ervoarn vrywilligers die 't uuterste vertrouwn van de gemêenschap èn gekreegn om aal d'andere gebrukersrechtn te beheern.
== Wa da e burocraat kan doene ==
Nevest de geweune bêerdersknoppn, kan e burocraat de volgende specioale rechtn toekenn'n (en soms weer ofpakkn):
* '''Bêerders & Burocraatn:''' Ze kunn'n andere gebrukers promoveern tot bêerder of zels tot burocraat. '''Let ip:''' e burocraat ku deze rechtn zelve ''nie'' ofpakkn as ze ki gegeevn zyn. Dat is een toake vo de globale [[{{ns:project}}:Stewards|Stewards]].
* '''Botstatus:''' Ze kunn'n de officiële botstatus toekenn'n of ofpakkn van [[{{ns:project}}:Botn|robotgebrukers]].
* '''Interfacemoderatoern:''' Ze kunn'n de rechtn toekenn'n of ofpakkn van de gebrukers die de [[{{ns:project}}:Interfacemoderatoern|technische site-code (CSS/JS)]] bewerkn.
== Regels en ofsproakn ==
Omdat een burocraat de sleutels van de wiki in handn eit, zyn der twêe grôte regels:
# '''Noois ip eign initiatief:''' E burocraat mag noois zomaar emand promoveern of rechtn toekenn'n. Der moe ossan êest ne dudelikken consensus (akkôord) zyn van de gemêenschap. Da gebeurt mêestol deur een aanvroag en stemmienge in de [[{{ns:project}}:Café|Café]].
# '''Neutroal ôordeel:''' Van e burocraat wordt verwacht da j' goed ku luustern noar de gemêenschap en een eerlik, neutroal ôordeel ku velln over de uutslag van die stemmiengn.
== Burocraat wordn ==
De lat vo burocraat te wordn ligt stief oge. Je moe mêestol al een ende meedroaien as goeie bêerder, en 't akkôord van de gemêenschap moe nog dudelikker en brêder zyn dan by e geweune bêerders-stemmienge.
== Lyste van burocraatn ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve burocraatn ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=bureaucrat&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Burocraatn-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Stewards|Stewards]]
[[Categorie:Wikipedia]]
m6hlyv46t5xfe23a27ksclv19nzdrfd
Wikipedia:Bêerders
4
29024
326042
326029
2026-04-03T15:02:57Z
Kannotlogin
29153
Doorverwijzing naar [[Wikipedia:Bêerders/Archief]] verwijderd
326042
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Administrator.svg|thumb|right|150px|'t Officiële symbool vo bêerders ip Wikipedia.]]
Nen '''Bêerder''', ôok wel ne '''sysop''' (system operator) genoemd, is e gebruker ip Wikipedia die e poar extra technische rechtn eit om de encyclopedie t'ounderoudn en te beschermn.
Bêerders zyn geweune vrywilligers die 't vertrouwn gekreegn èn van de gemêenschap. Ze zyn nie de boas over andere gebrukers en èn gin extra zeggenschap over den inoud van artikels, moar ze zurgn dervoorn da olles goed bluuft droaien.
== Wa da e bêerder kan doene ==
Om 't ounderoud makkelikker te moakn, èn bêerders e poar extra "knoppn":
* '''Verwydern:''' Bloadn of ofbeeldiengn verwydern (of verwyderde bloadn werekêern).
* '''Blokkeern:''' Gebrukers of IP-adressn die ipzettelik an 't môoschn zyn (vandalisme), tydelik of vour ossan blokkeern.
* '''Beveilign:''' Bloadn beveilign zodanig da ze (tydelik) nie mêer kunn'n wordn angebast.
* '''Systeemtekstn:''' De tekstn van de interface (MediaWiki-noamruumte) anpassn en ertoaln.
== Regels vo bêerders ==
Omdat de knoppn van e bêerder vele schoade kunn'n moakn as ze verkêerd wordn gebruukt, zyn der strakke regels:
# '''Vôorbeeldfunctie:''' Van e bêerder wordt verwacht da j' em ossan beleefd en neutroal gedraagt. Bêerder zyn is e verantwoordelikied, gin statussymbool.
# '''Gin eign ruzies (Involved):''' E bêerder mag zyn knoppn NOOIS gebruukn vo 't winn'n van een eign discussie. As je zelve betrokkn zyt in e ruze over een artikel, moe je de beslissienge overloatn an een andere, neutroale bêerder.
# '''Gin acties forceren:''' As een andere bêerder jouw actie oungedoan makt, meug je da nie zomaar were inplekkn zounder êest t'overleggn ("wheel warring" is verboodn).
== Bêerder wordn ==
A je al een ende meedroait, vele goeie bydroagn eit geleverd en 't vertrouwn eit van d'andere gebrukers, ku je je kandidoat stelln vo bêerder.
Je doe da deur e verzoek te platsn in de [[{{ns:project}}:Café|Café]]. As de gemêenschap via e stemmienge of overleg tôo (akkôord) is, zal e [[{{ns:project}}:Burocraatn|Burocraat]] je de bêerdersknoppn geevn.
== Lyste van bêerders ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve bêerders ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=sysop&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Bêerders-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Burocraatn|Burocraatn]]
* [[{{ns:project}}:Interfacemoderatoern|Interfacemoderatoern]]
[[Categorie:Wikipedia]]
i32x3hh088m2vge6q27mvhvdxv9y0i1
Wikipedia:Burocrat
4
29025
326039
2026-04-03T14:27:48Z
Kannotlogin
29153
deurverwiezienge noa [[Wikipedia:Burocraatn/Archief]]
326039
wikitext
text/x-wiki
#DOORVERWIJZING [[Wikipedia:Burocraatn/Archief]]
62h56y9hhzslwgvjz8nznpilaw8dutr
Wikipedia:AutoWikiBrowser
4
29026
326040
2026-04-03T14:36:32Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Awbscreenshot.PNG|thumb|right|320px|Nen blik ip d'interface van AutoWikiBrowser (AWB).]] '''AutoWikiBrowser''' (oftekort '''AWB''') is e (semi-)automatisch computerprogramma vo Wikipedia. 't Is gemakt om stief zêre en makkelik vele repititieve bewerkiengn te doen, lijk 't anpassn van dubbele redirects, spelfoutn verbetern of 't massoal toevoegn van categorieën an artikels. AWB toont iddere anpassienge êest an de gebruker. Je moe dus zelve ip 'Save' (Ipsloan)…
326040
wikitext
text/x-wiki
[[Ofbeeldienge:Awbscreenshot.PNG|thumb|right|320px|Nen blik ip d'interface van AutoWikiBrowser (AWB).]]
'''AutoWikiBrowser''' (oftekort '''AWB''') is e (semi-)automatisch computerprogramma vo Wikipedia. 't Is gemakt om stief zêre en makkelik vele repititieve bewerkiengn te doen, lijk 't anpassn van dubbele redirects, spelfoutn verbetern of 't massoal toevoegn van categorieën an artikels.
AWB toont iddere anpassienge êest an de gebruker. Je moe dus zelve ip 'Save' (Ipsloan) of 'Skip' (Oversloan) drukn.
== Regels vo 't gebruuk ==
Omdat AWB zuk e krachtig programma is woarmee da je in e poar minuutn ounderden artikels ku verandern, zyn der strakke regels an verboundn:
# '''Je bent OSSAN verantwoordelik vo iddere bewerkienge.''' Kyk olles goed noa vôor da je ipsloat. Offer gin kwaliteit ip vo snelied.
# '''Gin controversiële bewerkiengn:''' Gebruuk 't programma nie vo massale anpassiengn woar dat er gin dudelikke goedkeurienge of overleg over is met de rest van de gemêenschap.
# '''Gin nutteloze bewerkiengn:''' Gebruuk AWB nie vo anpassiengn die gin dudelik effect èn ip wa da de lezer ziet (lijk 't toevoegn van e nutteloze spaoce).
== Toestemmienge vo de VLS wiki ==
Om AWB te kunn'n gebruukn ip deze wiki, moe je account goedgekeurd zyn.
* [[{{ns:project}}:Bêerders|Bêerders]] meugen 't programma direct gebruukn zounder te vroagn.
* Geweune gebrukers moetn toestemmienge anvroagn by e Bêerder in de [[{{ns:project}}:Café|Café]]. As 't goedgekeurd is, wordt je noame toegevoegd an de [[{{ns:project}}:AutoWikiBrowser/CheckPageJSON|interne lyste (CheckPage)]], en ku je inloggn in 't programma.
== Wa ku je dermee doen? ==
AWB is surtout bekend omdat 't g'automatiseerde lystn kan moakn van brol da moe veranderd wordn. Je ku in 't programma lystn inloadn uut:
* Een specifieke categorie
* Olle artikels die linkn noar e bepoalde pagina ("Wa linkt ier naartoe")
* Joen eign volglyste of gebrukersbydroagn
* E tekstbestand of een specifieke zoekipdracht
== Installoatie en Download ==
't Programma is in d'êeste plekke gemakt vo '''Windows''' (en makt gebruuk van 't .NET Framework). Ip Linux of Mac moe je 't droaien via een emulator lijk Wine of Parallels. Je ku AWB gratis en veylig downloadn via 't officiële SourceForge-project.
<div class="plainlinks" style="display:inline-block; background-color:#28a745; border:1px solid #1e7e34; border-radius:4px; padding:12px 24px; font-size:15px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.15);">[https://sourceforge.net/projects/autowikibrowser/files/autowikibrowser/ <span style="color:#ffffff; text-decoration:none;">Download AutoWikiBrowser</span>]</div>
<br><br>
[[Categorie:Wikipedia]]
lbmhwhikkj5lg062oqpi2sni2ul8xdt
Wikipedia:Interfacemoderatoern
4
29027
326043
2026-04-03T15:27:41Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikipedia Interface administrator.svg|thumb|right|150px|'t Officiële symbool vo interfacemoderatoern.]] Nen '''interfacemoderatoer''' is e gebruker die de technische code ([[Cascading Style Sheets|CSS]], [[JavaScript]] en [[JSON]]) van de wiki mag anpassn. Die code bepoalt oe da de wiki deruut ziet en oe da 't werkt vo aal de lezers en gebrukers ip nunder computer of gsm. Omdat die code direct uutgevoerd wordt in de browser van de menschn, is toegang hiertoe st…
326043
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Interface administrator.svg|thumb|right|150px|'t Officiële symbool vo interfacemoderatoern.]]
Nen '''interfacemoderatoer''' is e gebruker die de technische code ([[Cascading Style Sheets|CSS]], [[JavaScript]] en [[JSON]]) van de wiki mag anpassn. Die code bepoalt oe da de wiki deruut ziet en oe da 't werkt vo aal de lezers en gebrukers ip nunder computer of gsm.
Omdat die code direct uutgevoerd wordt in de browser van de menschn, is toegang hiertoe stief gevoarlik in de handn van e slichte of onervoarn gebruker. Doarom is dit een aparte, zwoar beveiligde rolle ip Wikipedia.
== Veiligied en Regels ==
# '''Twee-factor-authenticatie (2FA):''' 't Is verplicht deur Wikimedia da interfacemoderatoern 2FA gebruukn. Zounder da, kunn'n ze de code zels nie anpassn.
# '''Kloek wachtwoord:''' Ze moetn under account extra goed beveilign teegn overnoame (hacking).
# '''Technische kennisse:''' Een interfacemoderatoer moe verstand èn van CSS en JavaScript, zodanig da ze de wiki nie per ongeluk kapotmoakn of trage moakn by 't bewerkn.
== Wa da een interfacemoderatoer kan doene ==
In teegnstellienge tot geweune [[{{ns:project}}:Bêerders|bêerders]], die wel tekstn kunn'n anpassn moar gin achterliggende code, kan een interfacemoderatoer:
* Site-brede CSS- en JavaScript-bloadn bewerkn (in de ''MediaWiki''-noamruumte).
* De persoonlijke CSS- en JS-bloadn van andere gebrukers anpassn (byvôorbeeld om slichte code te stoppn of vo t'elpn by e probleem ip nunder account).
* Gadgets (handige extra tools die je ku anzettn in je voorkeurn) moakn en uutbreidn.
* Systeemberichtn en de gebruikersinterface (lay-out en menu's) van de site verandern.
== Interfacemoderatoer wordn ==
't Is e rolle die ip de mêeste wiki's ollêne gegeevn wordt an gebrukers die aal bêerder zyn én bewyzn dan ze goed kunn'n codern. A je denkt da je die technische kennisse eit en de wiki wilt elpn ounderoudn, ku je e verzoek platsn in de [[{{ns:project}}:Café|Café]]. E [[{{ns:project}}:Burocraatn|Burocraat]] ku je de rechtn toekenn'n as de gemêenschap tôo is.
== Lyste van interfacemoderatoern ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve interfacemoderatoern ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=interface-admin&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Interfacemoderatoern-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Burocraatn|Burocraatn]]
[[Categorie:Wikipedia]]
o3dwsb3trrqyzqx1lcsgsqlkdd4edea
MediaWiki:Group-autoconfirmed.css
8
29028
326045
2026-04-03T15:53:03Z
Kannotlogin
29153
nieuw blad: /* CSS die hier wordt geplaatst heeft alleen invloed op automatisch bevestigde gebruikers */ .vls-autoconfirmed { display: inline !important; } .vls-unconfirmed { display: none !important; }
326045
css
text/css
/* CSS die hier wordt geplaatst heeft alleen invloed op automatisch bevestigde gebruikers */
.vls-autoconfirmed { display: inline !important; }
.vls-unconfirmed { display: none !important; }
os0gj53s0npqim68spxbemiqya9ezdj
Wikipedia:Bevestigde gebrukers
4
29029
326047
2026-04-03T16:22:22Z
Kannotlogin
29153
nieuw blad: <div style="float: right; width: 250px; border: 1px solid var(--border-color-muted, #dadde3); background-color: var(--background-color-neutral-subtle, #f8f9fa); font-size: 14px; text-align: center; padding: 12px; margin: 0 0 15px 15px;"> <span class="vls-unconfirmed">Je zyt nie ingelogd of joen account is te nieuw, dus je zyt '''<span style="color:var(--color-destructive, #bf3c2c);">nie automatisch bevestigd.</span>'''</span><span class="vls-autoconfirmed" style="display:none;">…
326047
wikitext
text/x-wiki
<div style="float: right; width: 250px; border: 1px solid var(--border-color-muted, #dadde3); background-color: var(--background-color-neutral-subtle, #f8f9fa); font-size: 14px; text-align: center; padding: 12px; margin: 0 0 15px 15px;">
<span class="vls-unconfirmed">Je zyt nie ingelogd of joen account is te nieuw, dus je zyt '''<span style="color:var(--color-destructive, #bf3c2c);">nie automatisch bevestigd.</span>'''</span><span class="vls-autoconfirmed" style="display:none;">Joen account '''<span style="color:var(--color-success, #177860);">is automatisch bevestigd</span>'''.</span>
</div>
Een '''bevestigde gebruker''' (in 't Iengels: ''confirmed'' of ''autoconfirmed user'') is e geregistreerde gebruker ip Wikipedia die beweezn eit dat 'n gin spambot of rappe vandaal is. Zodra da j' deze status eit, kryg je e poar extra basisrechtn ip de wiki.
Der zyn twêe maniern om deze status te krygn: automatisch of handmoatig.
== Automatisch bevestigd (Autoconfirmed) ==
De mêeste menschn krygn deze status voun eign, zounder dat er emand moe tussnkommn. De MediaWiki-software controleert iddere account ip twêe dinge:
# Je account moe minstens '''4 doagn''' oud zyn.
# J' eit minstens '''10 bewerkiengn''' (edits) gemakt.
Zodra da je an die twêe vôorwoardn voldôet, wordt je account ip de achterground gepromoveerd noar "automatisch bevestigd".
== Handmoatig bevestigd (Confirmed) ==
Soms eit etwien die extra rechtn direct nôdig, byvôorbeeld binst een ountmoetienge of schryfsessie woar da nieuwe menschn direct fotto's willn uploadn. In da geval ku ne [[{{ns:project}}:Bêerders|Bêerder]] (of nen [[{{ns:project}}:Organisatoern van evenementn|organisatoer]]) de account ''handmoatig'' bevestign, zodoanig da j' gin 4 doagn moe wachtn. Da noemn ze de "Confirmed" groep.
== Wa meug je mêer as bevestigde gebruker? ==
Nieuwe accounts zyn e bitje beperkt om misbruuk teegn te goan. Vanaf da je (automatisch of handmoatig) bevestigd zyt, ku je:
* '''Half-beveiligde bloadn bewerkn:''' Bloadn die vele Gevandaliseerd wordn, zyn soms beveiligd zodoanig da nieuwe of anonieme gebrukers ze nie kunn'n anpassn. Bevestigde gebrukers meugen da wel.
* '''Bloadn hernoemn:''' Je ku den titel van een artikel verandern en 't verplatsn.
* '''Bestoandn uploadn:''' Je ku fotto's en ofbeeldiengn uploadn noa de wiki.
* '''Gin CAPTCHA mêer:''' Je moe nimeer van die ambetante vôorbêeld-woordjes overtikkn as je een externe link toevoegt an een artikel.
== Lyste van handmoatig bevestigde gebrukers ==
Omdat bykan iddereen achter 4 doagn automatisch bevestigd wordt, is der gin lyste van de "autoconfirmed" gebrukers (da zoudn der duuzenden zyn).
Druk ip de knoppe ierounder vo de live lyste te bekykn van de gebrukers die uutzounderlik ''handmoatig'' bevestigd zyn deur e bêerder.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=confirmed&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Confirmed-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Organisatoern van evenementn|Organisatoern van evenementn]]
[[Categorie:Wikipedia]]
dq5pksa8ebkeamocti995tgpf5f4jga
326128
326047
2026-04-04T09:22:51Z
Kannotlogin
29153
326128
wikitext
text/x-wiki
<div style="float: right; width: 250px; border: 1px solid var(--border-color-muted, #dadde3); background-color: var(--background-color-neutral-subtle, #f8f9fa); color: var(--color-base, #202122); font-size: 14px; text-align: center; padding: 12px; margin: 0 0 15px 15px;">
<span class="vls-unconfirmed">Je zyt nie ingelogd of joen account is te nieuw, dus je zyt '''<span style="color:var(--color-destructive, #bf3c2c);">nie automatisch bevestigd.</span>'''</span><span class="vls-autoconfirmed" style="display:none;">Joen account '''<span style="color:var(--color-success, #177860);">is automatisch bevestigd</span>'''.</span>
</div>
Een '''bevestigde gebruker''' (in 't Iengels: ''confirmed'' of ''autoconfirmed user'') is e geregistreerde gebruker ip Wikipedia die beweezn eit dat 'n gin spambot of rappe vandaal is. Zodra da j' deze status eit, kryg je e poar extra basisrechtn ip de wiki.
Der zyn twêe maniern om deze status te krygn: automatisch of handmoatig.
== Automatisch bevestigd (Autoconfirmed) ==
De mêeste menschn krygn deze status voun eign, zounder dat er emand moe tussnkommn. De MediaWiki-software controleert iddere account ip twêe dinge:
# Je account moe minstens '''4 doagn''' oud zyn.
# J' eit minstens '''10 bewerkiengn''' (edits) gemakt.
Zodra da je an die twêe vôorwoardn voldôet, wordt je account ip de achterground gepromoveerd noar "automatisch bevestigd".
== Handmoatig bevestigd (Confirmed) ==
Soms eit etwien die extra rechtn direct nôdig, byvôorbeeld binst een ountmoetienge of schryfsessie woar da nieuwe menschn direct fotto's willn uploadn. In da geval ku ne [[{{ns:project}}:Bêerders|Bêerder]] (of nen [[{{ns:project}}:Organisatoern van evenementn|organisatoer]]) de account ''handmoatig'' bevestign, zodoanig da j' gin 4 doagn moe wachtn. Da noemn ze de "Confirmed" groep.
== Wa meug je mêer as bevestigde gebruker? ==
Nieuwe accounts zyn e bitje beperkt om misbruuk teegn te goan. Vanaf da je (automatisch of handmoatig) bevestigd zyt, ku je:
* '''Half-beveiligde bloadn bewerkn:''' Bloadn die vele Gevandaliseerd wordn, zyn soms beveiligd zodoanig da nieuwe of anonieme gebrukers ze nie kunn'n anpassn. Bevestigde gebrukers meugen da wel.
* '''Bloadn hernoemn:''' Je ku den titel van een artikel verandern en 't verplatsn.
* '''Bestoandn uploadn:''' Je ku fotto's en ofbeeldiengn uploadn noa de wiki.
* '''Gin CAPTCHA mêer:''' Je moe nimeer van die ambetante vôorbêeld-woordjes overtikkn as je een externe link toevoegt an een artikel.
== Lyste van handmoatig bevestigde gebrukers ==
Omdat bykan iddereen achter 4 doagn automatisch bevestigd wordt, is der gin lyste van de "autoconfirmed" gebrukers (da zoudn der duuzenden zyn).
Druk ip de knoppe ierounder vo de live lyste te bekykn van de gebrukers die uutzounderlik ''handmoatig'' bevestigd zyn deur e bêerder.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=confirmed&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Confirmed-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Organisatoern van evenementn|Organisatoern van evenementn]]
[[Categorie:Wikipedia]]
fe2zkluw6poovzhcrit7yl6ktdlgqq4
326129
326128
2026-04-04T09:24:00Z
Kannotlogin
29153
326129
wikitext
text/x-wiki
<div style="float: right; width: 250px; border: 1px solid var(--border-color-muted, #dadde3); background-color: var(--background-color-neutral-subtle, #f8f9fa); color: var(--color-base, #202122); font-size: 14px; text-align: center; padding: 12px; margin: 0 0 15px 15px;">
<span class="vls-unconfirmed">Je zyt nie ingelogd of joen account is te nieuw, dus je zyt '''<span style="color:var(--color-destructive, #bf3c2c);">nie automatisch bevestigd.</span>'''</span><span class="vls-autoconfirmed" style="display:none;">Joen account '''<span style="color:var(--color-success, #177860);">is automatisch bevestigd</span>'''.</span>
</div>
Een '''bevestigde gebruker''' (in 't Iengels: ''confirmed'' of ''autoconfirmed user'') is e geregistreerde gebruker ip Wikipedia die beweezn eit dat 'n gin spambot of rappe vandaal is. Zodra da j' deze status eit, kryg je e poar extra basisrechtn ip de wiki.
Der zyn twêe maniern om deze status te krygn: automatisch of handmoatig.
== Automatisch bevestigd (Autoconfirmed) ==
De mêeste menschn krygn deze status voun eign, zounder dat er emand moe tussnkommn. De MediaWiki-software controleert iddere account ip twêe dinge:
# Je account moe minstens '''4 doagn''' oud zyn.
# J' eit minstens '''10 bewerkiengn''' (edits) gemakt.
Zodra da je an die twêe vôorwoardn voldôet, wordt je account ip de achterground gepromoveerd noar "automatisch bevestigd".
== Handmoatig bevestigd (Confirmed) ==
Soms eit etwien die extra rechtn direct nôdig, byvôorbeeld binst een ountmoetienge of schryfsessie woar da nieuwe menschn direct fotto's willn uploadn. In da geval ku ne [[{{ns:project}}:Bêerders|Bêerder]] (of nen [[{{ns:project}}:Evenementorganisatoren|organisatoer]]) de account ''handmoatig'' bevestign, zodoanig da j' gin 4 doagn moe wachtn. Da noemn ze de "Confirmed" groep.
== Wa meug je mêer as bevestigde gebruker? ==
Nieuwe accounts zyn e bitje beperkt om misbruuk teegn te goan. Vanaf da je (automatisch of handmoatig) bevestigd zyt, ku je:
* '''Half-beveiligde bloadn bewerkn:''' Bloadn die vele Gevandaliseerd wordn, zyn soms beveiligd zodoanig da nieuwe of anonieme gebrukers ze nie kunn'n anpassn. Bevestigde gebrukers meugen da wel.
* '''Bloadn hernoemn:''' Je ku den titel van een artikel verandern en 't verplatsn.
* '''Bestoandn uploadn:''' Je ku fotto's en ofbeeldiengn uploadn noa de wiki.
* '''Gin CAPTCHA mêer:''' Je moe nimeer van die ambetante vôorbêeld-woordjes overtikkn as je een externe link toevoegt an een artikel.
== Lyste van handmoatig bevestigde gebrukers ==
Omdat bykan iddereen achter 4 doagn automatisch bevestigd wordt, is der gin lyste van de "autoconfirmed" gebrukers (da zoudn der duuzenden zyn).
Druk ip de knoppe ierounder vo de live lyste te bekykn van de gebrukers die uutzounderlik ''handmoatig'' bevestigd zyn deur e bêerder.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=confirmed&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Confirmed-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Evenementorganisatoren|Organisatoern van evenementn]]
[[Categorie:Wikipedia]]
e4p4mdgtmct0aaunaksp8rjo35myn9o
Wikipedia:Toezichtouwers
4
29030
326048
2026-04-03T16:41:08Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Oversight logo.png|thumb|right|150px|'t Officiële symbool vo toezichtouwers.]] Nen '''toezichtouwer''' (of in 't Iengels: ''oversighter'') is e gebruker met 't machtigste verwyder-recht ip Wikipedia. Ze kunn'n bewerkiengn, logboek-acties en zels gebrukersnoamn zodoanig verdukn, da zels geweune [[{{ns:project}}:Bêerders|bêerders]] ze nimeer kunn'n bekykn. Dit noemn ze ''suppression'' of ''oversight''. Omdat dit recht in goat teegn 't open karakter van Wikipedi…
326048
wikitext
text/x-wiki
[[Ofbeeldienge:Oversight logo.png|thumb|right|150px|'t Officiële symbool vo toezichtouwers.]]
Nen '''toezichtouwer''' (of in 't Iengels: ''oversighter'') is e gebruker met 't machtigste verwyder-recht ip Wikipedia. Ze kunn'n bewerkiengn, logboek-acties en zels gebrukersnoamn zodoanig verdukn, da zels geweune [[{{ns:project}}:Bêerders|bêerders]] ze nimeer kunn'n bekykn. Dit noemn ze ''suppression'' of ''oversight''.
Omdat dit recht in goat teegn 't open karakter van Wikipedia, wordt 't uuterst zelden en ollêne in nôodgevalln gebruukt.
== Wannêer wordt 't gebruukt? ==
Toezichtouwers meugen nunder knoppn ollêne in de volgende, zwoare gevalln gebruukn:
# '''Privé-gegeevns (Doxing):''' 't Verdukn van nie-publieke persôonlikke informoatie, lijk e-mailadressn, telefoongetaln, thuusadressn of echte noamn van menschn die anoniem willn bluuvn.
# '''Laster en Smaad:''' 't Verwydern van uuterst kwetsende, bedreigende of lasterlikke informoatie over leevende persôonn.
# '''Zwoare auteursrechtenschendiengn:''' Ollêne ip anroadn van de advocoatn van de Wikimedia Foundation.
== De situoatie ip de VLS wiki ==
De West-Vloamsche Wikipedia eit, net lijk de mêeste kleinere wiki's, '''gin lokale toezichtouwers'''.
As ter ier zwoare privé-gegeevns ip de wiki verschynn, wordt deze toake overgepakt deur de globale [[{{ns:project}}:Stewards|Stewards]]. Zynder èn oversighter-rechtn ip aal de wiki's in de weireld.
== Hoe e toezichtouwer contacteern? ==
<div style="border: 2px solid #cc0000; background-color: #ffcccc; padding: 12px; margin: 15px 0; font-size: 14px;">
'''WAARSCHUWING:''' Zet e verzoek om privé-gegeevns te verwydern NOOIS openboar ip de wiki of in de Café! As je ier openboar noar verwyst, trekt da ollêne moar mêer andacht van vandoaln.
</div>
Oa j' de ulpe van e toezichtouwer nôdig eit vo 't verdukn van privé-gegeevns of zwoare laster, moe je e '''beslootn e-mail''' stuurn noa de centrale toezichtouwers van Wikimedia: <code>stewards-oversight@wikimedia.org</code>. Voeg een interne link (URL) toe noar de bewerkienge die moe verduukt wordn.
== Lyste van toezichtouwers ==
Ip deze wiki is 't getal lokale toezichtouwers dus ossan nul, moa je ku de (lege) lyste ierounder bekykn vo de zekeried.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=suppress&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Toezichtouwers-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Stewards|Stewards]]
[[Categorie:Wikipedia]]
esw0yzpniyucdx05d1oreznu6hqm402
Wikipedia:Stewards
4
29031
326049
2026-04-03T16:59:30Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Emblem-person-blue.svg|thumb|right|150px|'t Officiële symbool vo Stewards ip Wikimedia.]] Ne '''Steward''' is e vrywilliger met de ollerôogste technische rechtn binn'n de hêle [[Wikimedia Foundation|Wikimedia-weireld]]. Z' èn toegang toet aal de (mêer of 800!) wiki's, en z' zyn d' ênigste die uuterst gevoelige acties meugen en kunn'n uutvoern. == De rol van e Steward ip de VLS wiki == Omdat oze wiki redelik klêne is, èn we lokaal gin {{ns:project}}:Toe…
326049
wikitext
text/x-wiki
[[Ofbeeldienge:Emblem-person-blue.svg|thumb|right|150px|'t Officiële symbool vo Stewards ip Wikimedia.]]
Ne '''Steward''' is e vrywilliger met de ollerôogste technische rechtn binn'n de hêle [[Wikimedia Foundation|Wikimedia-weireld]]. Z' èn toegang toet aal de (mêer of 800!) wiki's, en z' zyn d' ênigste die uuterst gevoelige acties meugen en kunn'n uutvoern.
== De rol van e Steward ip de VLS wiki ==
Omdat oze wiki redelik klêne is, èn we lokaal gin [[{{ns:project}}:Toezichtouwers|Toezichtouwers]] of Controlegebrukers, en is d' actievied van oze [[{{ns:project}}:Burocraatn|Burocraatn]] soms beperkt. Doarom èn we de Stewards soms nôdig vo 't zwoardere werk.
Stewards kommn ier ollêne as 't écht nôdig is of as we under roepn vo:
* '''Rechtn ofpakkn:''' 't Ontneemn van bêerders- of burocraatnrechtn (lokoale burocraatn kunn'n die ollêne toekenn'n, moa nie ofpakkn).
* '''Globale blocks:''' 't Globaal blokkeern van vandoaln of accounts die ip mêerdere wiki's tegelyk an 't môoschn zyn.
* '''Privé-gegeevns verdukn:''' 't Gebruukn van de [[{{ns:project}}:Toezichtouwers|oversight-knoppn]] vo gevoelige informoatie (lijk adressn of e-mails) vôor ossan te verdukn.
== Oe kom je in contact me Stewards? ==
Stewards opereern vanof 't centrale project, '''[[m:Main Page|Meta-Wiki]]'''. As je e Steward nôdig eit vo de VLS wiki, moe je doaroa zyn:
* '''Vo geweune verzoekn:''' (Lijk 't ofpakkn van rechtn of globale blocks) ku je terechte ip de [[:m:Special:MyLanguage/Steward requests|Steward Requests]]-pagina ip Meta.
* '''Vo dringende, privé-nôodgevalln:''' (Lijk laster of privacy-schendiengn) moe je een e-mail stuurn noa <code>stewards-oversight@wikimedia.org</code> zodoanig datter gin andacht ip de wiki zelve getrokkn wordt.
* '''Live chat:''' A j' emand uuterst dringend nôdig eit, ku je nunder viendn ip IRC in 't kanoal <code>#wikimedia-stewards</code>.
== Lyste van Stewards ==
Omdat Stewards globaal verkoozn wordn (en nie ip de VLS wiki zelve), stoat deze lyste nie ier, moa centraal.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[[m:Special:MyLanguage/Stewards#List_of_current_stewards| <span style="color:#ffffff; text-decoration:none;">Bekyk de globale Stewards-lyste ip Meta-Wiki</span>]]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
* [[{{ns:project}}:Burocraatn|Burocraatn]]
[[Categorie:Wikipedia]]
69qw37a5t99nm9kq3jkdrpmcxb1tghr
Wikipedia:Accountanmoakers
4
29032
326051
2026-04-03T18:36:29Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikipedia Accountcreators v2.svg|thumb|right|150px|'t Officiële symbool vo accountanmoakers.]] Nen '''accountanmoaker''' (in 't Iengels: ''account creator'') is e gebruker met e specifieke set van technische rechtn vo massoal nieuwe gebrukersaccounts an te moakn vour andere menschn. Ip grôte wiki's lijk d' Iengelsche Wikipedia zyn der specioale teams die accounts moakn vo menschn die da zelve nie kunn'n (byvôorbeeld omdat nunder IP-adres van 't werk of de sch…
326051
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Accountcreators v2.svg|thumb|right|150px|'t Officiële symbool vo accountanmoakers.]]
Nen '''accountanmoaker''' (in 't Iengels: ''account creator'') is e gebruker met e specifieke set van technische rechtn vo massoal nieuwe gebrukersaccounts an te moakn vour andere menschn.
Ip grôte wiki's lijk d' Iengelsche Wikipedia zyn der specioale teams die accounts moakn vo menschn die da zelve nie kunn'n (byvôorbeeld omdat nunder IP-adres van 't werk of de schole geblokkeerd is deur e vandaal). De accountanmoakers èn de uutzounderlikke rechtn om da te meugen oplossn.
== Wa da nen accountanmoaker kan doene ==
De MediaWiki-software eit e poar zwoare beveiligiengn ip 't anmoakn van nieuwe accounts. Een accountanmoaker kan die beveiligiengn bypassn:
* '''Gin limiet:''' Normoal gezien ku je vanuut één IP-adres moa maximum 6 accounts per dag anmoakn. Een accountanmoaker eit gin limiet en kan der zoevele anmoakn lijk of dat 'n wilt.
* '''Teegn-spoofing (Anti-spoof):''' Ze kunn'n accounts moakn met e noame die stief ard trekt ip de noame van een andre gebruker (wa da normoal geblokkeerd wordt om verwarrienge teegn te goan).
* '''Blacklist:''' Ze kunn'n in uutzounderlikke gevalln accounts moakn die eigentlik ip de ''zwarte lyste'' (blacklist) stoan.
== De situoatie ip de VLS wiki ==
Omdat de West-Vloamsche Wikipedia e bitje klêner is en gin duuzenden nieuwe anmeldingn per dag eit, èn we ier lokaal gin accountanmoakers.
Bovendien èn oze [[{{ns:project}}:Bêerders|Bêerders]] aal de rechtn van een accountanmoaker automatisch inbegreepn in nunder eign knoppnpakket. As ter dus ooit massoal accounts moetn gemakt wordn (byvôorbeeld voor een evenement of e schole), kan e bêerder da perfect zelve doene.
== Lyste van accountanmoakers ==
Druk ip de knoppe ierounder vo de live, automatisch bygewerkte lyste van aal actieve accountanmoakers ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=accountcreator&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Accountanmoakers-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
a5pw5a8oti8sfbus6bjjh6hajw0mjus
Wikipedia:Importeurs
4
29033
326053
2026-04-03T19:00:04Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Oxygen480-actions-document-import.svg|thumb|right|120px|'t Symbool vo 't importeern van bestoandn.]] Nen '''importeur''' is e gebruker met e stief specioal en uuterst beperkt recht: ze kunn'n hêle bloadn, inclusief de complete bewerkingsgeschiedenisse, in één kêe ip de wiki zettn deur e computerbestand (een XML-bestand) t' uploadn. == Wa da nen importeur kan doene == Normoal gezien ku je een artikel ollêne overzettn deur de tekst te kopiëren en te plakkn.…
326053
wikitext
text/x-wiki
[[Ofbeeldienge:Oxygen480-actions-document-import.svg|thumb|right|120px|'t Symbool vo 't importeern van bestoandn.]]
Nen '''importeur''' is e gebruker met e stief specioal en uuterst beperkt recht: ze kunn'n hêle bloadn, inclusief de complete bewerkingsgeschiedenisse, in één kêe ip de wiki zettn deur e computerbestand (een XML-bestand) t' uploadn.
== Wa da nen importeur kan doene ==
Normoal gezien ku je een artikel ollêne overzettn deur de tekst te kopiëren en te plakkn. Moar ton verlies je wel de geschiedenisse van wie dat 't ôorsprounkelik geschreevn eit.
Een importeur kan via [[Specioal:Importeren]] e bestand uut e willekeurige andere wiki iploadn. De MediaWiki-software plat ton 't artikel én aal d' oude bewerkiengn perfect in oze database. Z' èn ouk 't recht om de geschiedenisse van twêe bloadn in makkoar te smytn (samenvoegn).
== Gevoarlik werk ==
Omdat e gebruker met dit recht rechtstreeks data (XML-bestoandn) vanof zyn eign computer in de database van Wikipedia kan pompn, is dit uuterst gevoarlik. E slicht of verkêerd bestand kan de wiki zwoar beschoadigen of in de war briengn. Doarom wordt dit recht bykan noois gegeevn.
== De situoatie ip de VLS wiki ==
Ip de West-Vloamsche Wikipedia èn we lokaal '''gin importeurs'''. As ter écht etwien via XML moe g'ïmporteerd wordn, moe dit idder geval zorgvuldig overlegd wordn, en wordt de toake uutgevoerd deur de globale [[{{ns:project}}:Stewards|Stewards]].
== Lyste van importeurs ==
Druk ip de knoppe ierounder vo de live (lege) lyste van actieve importeurs ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=import&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Importeurs-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Transwiki-importeurs|Transwiki-importeurs]]
* [[{{ns:project}}:Stewards|Stewards]]
[[Categorie:Wikipedia]]
kaf42zcm8354t56xl6hycpjlxnbbjmo
Wikipedia:Transwiki-importeurs
4
29034
326054
2026-04-03T21:26:05Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikimedia logo family complete.svg|thumb|right|150px|Transwiki-importeurs verplatsn bloadn binn'n de Wikimedia-familie.]] Nen '''transwiki-importeur''' is e gebruker die bloadn rechtstreeks kan kopiëren van d' êne officiële Wikimedia-wiki noa d' andere. Ze pakkn doaby de hêle geschiedenisse van 't artikel mee, zodoanig da je ku bluuvn zien wie da 't ôorsprounkelik geschreevn eit. == 't Verschil met e geweunen importeur == Nen transwiki-importeur eit e veyli…
326054
wikitext
text/x-wiki
[[Ofbeeldienge:Wikimedia logo family complete.svg|thumb|right|150px|Transwiki-importeurs verplatsn bloadn binn'n de Wikimedia-familie.]]
Nen '''transwiki-importeur''' is e gebruker die bloadn rechtstreeks kan kopiëren van d' êne officiële Wikimedia-wiki noa d' andere. Ze pakkn doaby de hêle geschiedenisse van 't artikel mee, zodoanig da je ku bluuvn zien wie da 't ôorsprounkelik geschreevn eit.
== 't Verschil met e geweunen importeur ==
Nen transwiki-importeur eit e veyligere versie van 't import-recht. In teegnstellienge toet geweune [[{{ns:project}}:Importeurs|importeurs]], kunn'n zynder '''gin''' eign bestoandn (XML) uploadn vanof nunder computer.
Zynder kunn'n via [[Specioal:Importeren]] ollêne informoatie binn'ntrekkn vanof specifieke, goedgekeurde zusterprojectn (lijk de Nederlandstalige of Iengelsche Wikipedia). Omdat de data van een andere betrouwboare Wikimedia-server komt, is 't risico ip schoade an de wiki vele klêner.
== De situoatie ip de VLS wiki ==
Zels ol is 't veyliger dan de geweune XML-import, toch èn we ip de West-Vloamsche Wikipedia normoal gezien '''gin lokale transwiki-importeurs'''.
As ter e grôot artikel uut 't Nederlands of Iengels inclusief geschiedenisse moe overgezet wordn noa 't VLS, wordn doavoorn normoal gezien globale [[{{ns:project}}:Stewards|Stewards]] ingeschakeld, die deze rechtn standaard èn. Oze eign [[{{ns:project}}:Bêerders|bêerders]] kunn'n natuurlik wel geweune bloadn anmoakn en vertoaln zounder de complete import-knoppn.
== Lyste van transwiki-importeurs ==
Druk ip de knoppe ierounder vo de live (lege) lyste van actieve transwiki-importeurs ip deze wiki te bekykn.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=transwiki&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Transwiki-importeurs-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Importeurs|Importeurs]]
* [[{{ns:project}}:Stewards|Stewards]]
[[Categorie:Wikipedia]]
87pwfelwrjdual71zmvd6lscr3h1iiw
Wikipedia:Uutzounderiengn van IP-blokkoades
4
29035
326055
2026-04-03T21:26:05Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikipedia IP Block Exempt.svg|thumb|right|120px|'t Symbool vo d' uutzounderiengn ip IP-blokkoades.]] '''Uutzounderiengn van IP-blokkoades''' (of in 't Iengels: ''IP block exempt'') is e specioale gebrukersgroep vo menschn die under ip e netwerk beviendn da geblokkeerd is deur Wikipedia, moa die zelve tutebêle gin regels èn gebrookn. == Wa is 't probleem zjuuste? == Omdat er vele spam en vandalisme vanof publieke netwerkn komt (lijk scholen, bibliotheekn, bedry…
326055
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia IP Block Exempt.svg|thumb|right|120px|'t Symbool vo d' uutzounderiengn ip IP-blokkoades.]]
'''Uutzounderiengn van IP-blokkoades''' (of in 't Iengels: ''IP block exempt'') is e specioale gebrukersgroep vo menschn die under ip e netwerk beviendn da geblokkeerd is deur Wikipedia, moa die zelve tutebêle gin regels èn gebrookn.
== Wa is 't probleem zjuuste? ==
Omdat er vele spam en vandalisme vanof publieke netwerkn komt (lijk scholen, bibliotheekn, bedryvn of via [[Wikipedia:Open proxy's|VPN's / open proxy's]]), zyn deze IP-adressn vo de veyligheid dikwils volledig geblokkeerd. Da wil zeggn da ''niemand'' vanof dat IP-adres noa Wikipedia kan schryvn.
Da's noga lastig as j' een eerlikke, geregistreerde gebruker zyt die toevallig ip ze werk of ip schole wilt bewerkn.
== De iplossienge (IP-exempt) ==
Om dit ip te lossn, ku j' in deze gebrukersgroep gezet wordn. As je dit recht eit (en je zyt ingelogd ip je account), negeert de wiki de IP-blokkoade van joen netwerk en ku je geweune vôort bewerkn lijk normoal. Zels via een anoniem Tor-netwerk!
== Hoe ku je da recht anvroagn? ==
Dit recht ku je nie zomaar krygn. Je moe êest bewyzn da j' een betrouwboare gebruker zyt.
# '''Lokale blokkoade:''' Ku j' ip de VLS wiki nie bewerkn deur een IP-blokkoade? Vroag ton (via een andre locatie of computer) de ''IP block exempt'' an by e [[{{ns:project}}:Bêerders|Bêerder]] via de [[{{ns:project}}:Café|Café]] of richt e berichtjen an nunder discuusjeblad.
# '''Globale blokkoade:''' Soms wordt een IP-adres globaal (ip alle Wikipedia's tegelyk) geblokkeerd. In da geval è j' e "Globale IP-exempt" nôdig. Dit ku je ollêne anvroagn by de [[{{ns:project}}:Stewards|Stewards]] ip [[:m:Steward requests/Global permissions#Requests for global IP block exemption|deze pagina ip Meta-Wiki]].
''Let ip:'' [[{{ns:project}}:Bêerders|Bêerders]] zelve zyn automatisch al uutgezounderd van IP-blokkoades.
== Lyste van uutzounderiengn ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers die teegnwoordig een uutzounderienge èn ip de VLS wiki.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=ipblock-exempt&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live IP-Exempt-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
kkb7reqmu3wix599mojul4g3imxe3f4
Wikipedia:Controlegebrukers
4
29036
326056
2026-04-03T21:26:06Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikipedia Checkuser.svg|thumb|right|150px|'t Officiële symbool vo controlegebrukers.]] Nen '''controlegebruker''' (in 't Iengels: ''CheckUser'') is e gebruker die met e specioale tool de ounderliggende [[IP-adres]]sn van andre geregistreerde gebrukers kan bekykn in de database. Deze tool wordt uutsluutend gebruukt vo 't ipspôorn van '''sokpoppn''' (menschn die stiekem mêerdere accounts teglyk gebruukn om discussies of stemmiengn te bedriegn) en vo 't teegnho…
326056
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Checkuser.svg|thumb|right|150px|'t Officiële symbool vo controlegebrukers.]]
Nen '''controlegebruker''' (in 't Iengels: ''CheckUser'') is e gebruker die met e specioale tool de ounderliggende [[IP-adres]]sn van andre geregistreerde gebrukers kan bekykn in de database.
Deze tool wordt uutsluutend gebruukt vo 't ipspôorn van '''sokpoppn''' (menschn die stiekem mêerdere accounts teglyk gebruukn om discussies of stemmiengn te bedriegn) en vo 't teegnhoan van zwoar, structureel vandalisme.
== Strakke privacyregels ==
Omdat e controlegebruker ip 't randje van de persôonlikke levenssfêre werkt, is dit recht stief zwoar bewaakt.
# '''Geheymoudienge:''' Controlegebrukers moetn mêerderjoarig zyn en een officieel privacy-contract (NDA) getêekend èn by de Wikimedia Foundation vôor da ze de tool meugen gebruukn.
# '''Gin visexpedities (fishing):''' De tool mag noois zomaar gebruukt wordn om uut nieuwsgierigied te "kykn" wie dat er achter een account zit. Der moe êest dudelik en antoonboar bewys zyn van bedrog in de bewerkiengn.
# '''Discretie:''' De resultoatn wordn mêestol anzienlik verduukt meegedeeld (byvôorbeeld: "Account A en B zyn effectief dezelfste persôon", zounder 't effectieve IP-adres te toonn an 't publiek).
== De situoatie ip de VLS wiki ==
Omdat oze wiki klêne is en gin onofhankelikke Arbitragecommissie eit, èn we lokaal '''gin controlegebrukers'''.
As ter ip de West-Vloamsche Wikipedia e zwoar vermoedn is da twien de boel an 't ipfokkn is me mêerdere accounts, wordt deze toake uutgevoerd deur de globale [[{{ns:project}}:Stewards|Stewards]].
== Hoe een ounderzoek anvroagn? ==
As j' e gefundeerd vermoedn eit van e sokpoppe ip oze wiki, ku j' e verzoek indienen by de Stewards ip Meta-Wiki via de pagina: '''[[:m:Steward requests/Checkuser|Steward requests/Checkuser]]'''. Zurg wel da je goeie bewyzn eit uut nunder bewerkiengsgeschiedenisse (lijk dezelfste spelfoutn of typische maniern van werkn), want zounder goeie reedn wordn der gin IP-adressn nagekeekn.
== Lyste van controlegebrukers ==
Ip deze wiki is 't getal lokale controlegebrukers dus ossan nul, moa je ku de (lege) lyste ierounder bekykn vo de zekeried.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=checkuser&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Controlegebrukers-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Toezichtouwers|Toezichtouwers]]
* [[{{ns:project}}:Stewards|Stewards]]
[[Categorie:Wikipedia]]
d0x5rjf8ba6ao1mjkwtugn37wjmrjyx
Wikipedia:Inzoage IP-adressn tydelikke accounts
4
29037
326057
2026-04-03T21:26:08Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:TAIV Wikipedia.svg|thumb|right|150px|'t Officiële symbool vo d' inzoage in IP-adressn.]] '''Inzoage in IP-adressn van tydelikke accounts''' (in 't Iengels: ''Temporary account IP viewer'') is e specifieke gebrukersgroep vo menschn die de ounderliggende [[IP-adres]]sn van tydelikke accounts meugen bekykn in de software. == Wa zyn tydelikke accounts? == Vroeger koest iddereen de IP-adressn zien van menschn die anoniem (zounder in te loggn) noa Wikipedia schreevn.…
326057
wikitext
text/x-wiki
[[Ofbeeldienge:TAIV Wikipedia.svg|thumb|right|150px|'t Officiële symbool vo d' inzoage in IP-adressn.]]
'''Inzoage in IP-adressn van tydelikke accounts''' (in 't Iengels: ''Temporary account IP viewer'') is e specifieke gebrukersgroep vo menschn die de ounderliggende [[IP-adres]]sn van tydelikke accounts meugen bekykn in de software.
== Wa zyn tydelikke accounts? ==
Vroeger koest iddereen de IP-adressn zien van menschn die anoniem (zounder in te loggn) noa Wikipedia schreevn. Vo de privacy te beschermn, eit Wikimedia da veranderd. Menschn die nu anoniem bewerkn, krygn automatisch e "tydelik account" (byvôorbeeld ''~2026-12345''). Nunder echte IP-adres wordt verduukt vo 't grôte publiek.
Omda we toch vandoaln en sokpoppn moetn kunn'n teegnhoan, is deze specioale gebrukersgroep gemakt om die verduukte IP-adressn toch te kunn'n inzien.
== Strakke privacyregels ==
Omdat 't over persôonlikke gegeevns goat, is 't gebruuk van deze tool zwoar gereglementeerd deur de Wikimedia Foundation:
# '''Ollêne vo misbruuk:''' Je mag de tool ollêne gebruukn vo vandalisme, spam of sokpoppn ip te spôorn. 't Is strang verboodn om uut nieuwsgierigied IP-adressn te bekykn of om 't te gebruukn in e geweune discussie over den inoud van een artikel.
# '''Noois openboar moakn:''' 't Is verboodn om een IP-adres van e tydelik account openboar te platsn ip de wiki.
# '''Logboek:''' Iddere kêe da je een IP-adres bekykt met de tool, wordt da in e beslootn logboek geregistreerd vo controle.
== De situoatie ip de VLS wiki ==
[[{{ns:project}}:Bêerders|Bêerders]], [[{{ns:project}}:Burocraatn|Burocraatn]] en [[{{ns:project}}:Toezichtouwers|Toezichtouwers]] krygn dit recht automatisch, moa ze moetn 't wel êest zelve anzettn in nunder vôorkeurn (ounder 't tabblad "Gebrukersprofiel").
'''Ku je dit recht zelve anvroagn?'''
Joak, ouk geweune gebrukers kunn'n dit recht anvroagn in de [[{{ns:project}}:Café|Café]], moa je moe wel an strakke vôorwoardn voldôen:
* Je account moe minstens 6 moandn oud zyn.
* Je moe minstens 300 goeie bewerkiengn èn ip deze wiki.
* Je moe dudelik kunn'n bewyzn da je styf actief zyt in 't bestrydn van vandalisme.
== Lyste van inzoage-gebrukers ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn explecieten toegang èn (bêerders zyn ier normoal gezien nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=temporary-account-viewer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Inzoage-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
l16vq78y9hzbbd4ncx5ppdroc4eqvpq
326132
326057
2026-04-04T09:26:14Z
Kannotlogin
29153
Kannotlogin heeft pagina [[Wikipedia:Inzage IP-adressen tijdelijke accounts]] hernoemd naar [[Wikipedia:Inzoage IP-adressn tydelikke accounts]]
326057
wikitext
text/x-wiki
[[Ofbeeldienge:TAIV Wikipedia.svg|thumb|right|150px|'t Officiële symbool vo d' inzoage in IP-adressn.]]
'''Inzoage in IP-adressn van tydelikke accounts''' (in 't Iengels: ''Temporary account IP viewer'') is e specifieke gebrukersgroep vo menschn die de ounderliggende [[IP-adres]]sn van tydelikke accounts meugen bekykn in de software.
== Wa zyn tydelikke accounts? ==
Vroeger koest iddereen de IP-adressn zien van menschn die anoniem (zounder in te loggn) noa Wikipedia schreevn. Vo de privacy te beschermn, eit Wikimedia da veranderd. Menschn die nu anoniem bewerkn, krygn automatisch e "tydelik account" (byvôorbeeld ''~2026-12345''). Nunder echte IP-adres wordt verduukt vo 't grôte publiek.
Omda we toch vandoaln en sokpoppn moetn kunn'n teegnhoan, is deze specioale gebrukersgroep gemakt om die verduukte IP-adressn toch te kunn'n inzien.
== Strakke privacyregels ==
Omdat 't over persôonlikke gegeevns goat, is 't gebruuk van deze tool zwoar gereglementeerd deur de Wikimedia Foundation:
# '''Ollêne vo misbruuk:''' Je mag de tool ollêne gebruukn vo vandalisme, spam of sokpoppn ip te spôorn. 't Is strang verboodn om uut nieuwsgierigied IP-adressn te bekykn of om 't te gebruukn in e geweune discussie over den inoud van een artikel.
# '''Noois openboar moakn:''' 't Is verboodn om een IP-adres van e tydelik account openboar te platsn ip de wiki.
# '''Logboek:''' Iddere kêe da je een IP-adres bekykt met de tool, wordt da in e beslootn logboek geregistreerd vo controle.
== De situoatie ip de VLS wiki ==
[[{{ns:project}}:Bêerders|Bêerders]], [[{{ns:project}}:Burocraatn|Burocraatn]] en [[{{ns:project}}:Toezichtouwers|Toezichtouwers]] krygn dit recht automatisch, moa ze moetn 't wel êest zelve anzettn in nunder vôorkeurn (ounder 't tabblad "Gebrukersprofiel").
'''Ku je dit recht zelve anvroagn?'''
Joak, ouk geweune gebrukers kunn'n dit recht anvroagn in de [[{{ns:project}}:Café|Café]], moa je moe wel an strakke vôorwoardn voldôen:
* Je account moe minstens 6 moandn oud zyn.
* Je moe minstens 300 goeie bewerkiengn èn ip deze wiki.
* Je moe dudelik kunn'n bewyzn da je styf actief zyt in 't bestrydn van vandalisme.
== Lyste van inzoage-gebrukers ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn explecieten toegang èn (bêerders zyn ier normoal gezien nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=temporary-account-viewer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Inzoage-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
l16vq78y9hzbbd4ncx5ppdroc4eqvpq
Wikipedia:Evenementcoördinatoorn
4
29038
326058
2026-04-03T21:26:09Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Wikipedia Event coordinator.svg|thumb|right|150px|'t Officiële symbool vo organisatoern van evenementn.]] Nen '''organisatoer van evenementn''' (in 't Iengels: ''event coordinator'') is e gebruker die specioale rechtn eit vo 't organiseern van Wikipedia-byêenkomstn, lijk e schryfsessie (edit-a-thon), e workshop of een ountmoetienge in 't echt. == Wa da nen organisatoer kan doene == As je met e grôte groep menschn tôpe komt vo Wikipedia te lêern bewerkn, loo…
326058
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Event coordinator.svg|thumb|right|150px|'t Officiële symbool vo organisatoern van evenementn.]]
Nen '''organisatoer van evenementn''' (in 't Iengels: ''event coordinator'') is e gebruker die specioale rechtn eit vo 't organiseern van Wikipedia-byêenkomstn, lijk e schryfsessie (edit-a-thon), e workshop of een ountmoetienge in 't echt.
== Wa da nen organisatoer kan doene ==
As je met e grôte groep menschn tôpe komt vo Wikipedia te lêern bewerkn, loop je dikwils teegn technische limietn en beveiligiengn an. Een organisatoer kan die limietn tydelik uutzettn om 't evenement vlot te loatn verlôopn:
* '''Massoal accounts anmoakn:''' Normoal ku je moa 6 accounts per dag moakn vanof êen IP-adres (byvôorbeeld via de wifi van e schole of e bibliotheeke). Een organisatoer eit die limiet nie en kan vo de hêle zoale accounts moakn.
* '''Tydelikke bevestigienge:''' Ze kunn'n nieuwe gebrukers direct de status van "[[Bevestigde gebrukers|bevestigde gebruker]]" geevn. Doadeure meugen die nieuwe menschn binst 't evenement direct an de slag met olles, zounder de normoale 4 doagn te moetn wachtn.
* '''Evenement-systeem:''' Ze kunn'n de specioale ''Event:'' noamruumte (Event Center) gebruukn vo fiches an te moakn woar da menschn nunder kunn'n inschryvn.
== De situoatie ip de VLS wiki ==
Omdat de West-Vloamsche Wikipedia moa zeldn grôte fysieke evenementn of massale schryfsessies organiseert, èn we lokaal gin aparte organisatoern in deze groep.
Oze eign [[{{ns:project}}:Bêerders|Bêerders]] èn gelukkig ouk aal deze rechtn automatisch in nunder knoppnpakket zitten. As ter dus een evenement g'organiseerd wordt in West-Vloandern, kan e bêerder da perfect zelve in goeie boann leydn.
== Lyste van organisatoern ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn specifieken titel èn (bêerders zyn ier nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=event-organizer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Organisatoern-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
lhos1rcyx8t7yu3n6holu6gwspia8bg
326060
326058
2026-04-03T21:26:54Z
Kannotlogin
29153
326060
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Event coordinator.svg|thumb|right|150px|'t Officiële symbool vo organisatoern van evenementn.]]
Nen '''organisatoer van evenementn''' (in 't Iengels: ''event coordinator'') is e gebruker die specioale rechtn eit vo 't organiseern van Wikipedia-byêenkomstn, lijk e schryfsessie (edit-a-thon), e workshop of een ountmoetienge in 't echt.
== Wa da nen organisatoer kan doene ==
As je met e grôte groep menschn tôpe komt vo Wikipedia te lêern bewerkn, loop je dikwils teegn technische limietn en beveiligiengn an. Een organisatoer kan die limietn tydelik uutzettn om 't evenement vlot te loatn verlôopn:
* '''Massoal accounts anmoakn:''' Normoal ku je moa 6 accounts per dag moakn vanof êen IP-adres (byvôorbeeld via de wifi van e schole of e bibliotheeke). Een organisatoer eit die limiet nie en kan vo de hêle zoale accounts moakn.
* '''Tydelikke bevestigienge:''' Ze kunn'n nieuwe gebrukers direct de status van "[[Wikipedia:Bevestigde gebrukers|bevestigde gebruker]]" geevn. Doadeure meugen die nieuwe menschn binst 't evenement direct an de slag met olles, zounder de normoale 4 doagn te moetn wachtn.
* '''Evenement-systeem:''' Ze kunn'n de specioale ''Event:'' noamruumte (Event Center) gebruukn vo fiches an te moakn woar da menschn nunder kunn'n inschryvn.
== De situoatie ip de VLS wiki ==
Omdat de West-Vloamsche Wikipedia moa zeldn grôte fysieke evenementn of massale schryfsessies organiseert, èn we lokaal gin aparte organisatoern in deze groep.
Oze eign [[{{ns:project}}:Bêerders|Bêerders]] èn gelukkig ouk aal deze rechtn automatisch in nunder knoppnpakket zitten. As ter dus een evenement g'organiseerd wordt in West-Vloandern, kan e bêerder da perfect zelve in goeie boann leydn.
== Lyste van organisatoern ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn specifieken titel èn (bêerders zyn ier nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=event-organizer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Organisatoern-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
8nqibm07a9psxmiy0cte7dupqn8pqzm
326138
326060
2026-04-04T09:33:36Z
Kannotlogin
29153
Kannotlogin heeft de pagina [[Wikipedia:Evenementorganisatoren]] hernoemd naar [[Wikipedia:Evenementcoördinatoorn]] zonder een doorverwijzing achter te laten
326060
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Event coordinator.svg|thumb|right|150px|'t Officiële symbool vo organisatoern van evenementn.]]
Nen '''organisatoer van evenementn''' (in 't Iengels: ''event coordinator'') is e gebruker die specioale rechtn eit vo 't organiseern van Wikipedia-byêenkomstn, lijk e schryfsessie (edit-a-thon), e workshop of een ountmoetienge in 't echt.
== Wa da nen organisatoer kan doene ==
As je met e grôte groep menschn tôpe komt vo Wikipedia te lêern bewerkn, loop je dikwils teegn technische limietn en beveiligiengn an. Een organisatoer kan die limietn tydelik uutzettn om 't evenement vlot te loatn verlôopn:
* '''Massoal accounts anmoakn:''' Normoal ku je moa 6 accounts per dag moakn vanof êen IP-adres (byvôorbeeld via de wifi van e schole of e bibliotheeke). Een organisatoer eit die limiet nie en kan vo de hêle zoale accounts moakn.
* '''Tydelikke bevestigienge:''' Ze kunn'n nieuwe gebrukers direct de status van "[[Wikipedia:Bevestigde gebrukers|bevestigde gebruker]]" geevn. Doadeure meugen die nieuwe menschn binst 't evenement direct an de slag met olles, zounder de normoale 4 doagn te moetn wachtn.
* '''Evenement-systeem:''' Ze kunn'n de specioale ''Event:'' noamruumte (Event Center) gebruukn vo fiches an te moakn woar da menschn nunder kunn'n inschryvn.
== De situoatie ip de VLS wiki ==
Omdat de West-Vloamsche Wikipedia moa zeldn grôte fysieke evenementn of massale schryfsessies organiseert, èn we lokaal gin aparte organisatoern in deze groep.
Oze eign [[{{ns:project}}:Bêerders|Bêerders]] èn gelukkig ouk aal deze rechtn automatisch in nunder knoppnpakket zitten. As ter dus een evenement g'organiseerd wordt in West-Vloandern, kan e bêerder da perfect zelve in goeie boann leydn.
== Lyste van organisatoern ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn specifieken titel èn (bêerders zyn ier nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=event-organizer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Organisatoern-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
8nqibm07a9psxmiy0cte7dupqn8pqzm
326139
326138
2026-04-04T09:34:04Z
Kannotlogin
29153
326139
wikitext
text/x-wiki
[[Ofbeeldienge:Wikipedia Event coordinator.svg|thumb|right|150px|'t Officiële symbool vo organisatoern van evenementn.]]
Nen '''organisatoer van evenementn''' of '''Evenementcoördinatoorn''' (in 't Iengels: ''event coordinator'') is e gebruker die specioale rechtn eit vo 't organiseern van Wikipedia-byêenkomstn, lijk e schryfsessie (edit-a-thon), e workshop of een ountmoetienge in 't echt.
== Wa da nen organisatoer kan doene ==
As je met e grôte groep menschn tôpe komt vo Wikipedia te lêern bewerkn, loop je dikwils teegn technische limietn en beveiligiengn an. Een organisatoer kan die limietn tydelik uutzettn om 't evenement vlot te loatn verlôopn:
* '''Massoal accounts anmoakn:''' Normoal ku je moa 6 accounts per dag moakn vanof êen IP-adres (byvôorbeeld via de wifi van e schole of e bibliotheeke). Een organisatoer eit die limiet nie en kan vo de hêle zoale accounts moakn.
* '''Tydelikke bevestigienge:''' Ze kunn'n nieuwe gebrukers direct de status van "[[Wikipedia:Bevestigde gebrukers|bevestigde gebruker]]" geevn. Doadeure meugen die nieuwe menschn binst 't evenement direct an de slag met olles, zounder de normoale 4 doagn te moetn wachtn.
* '''Evenement-systeem:''' Ze kunn'n de specioale ''Event:'' noamruumte (Event Center) gebruukn vo fiches an te moakn woar da menschn nunder kunn'n inschryvn.
== De situoatie ip de VLS wiki ==
Omdat de West-Vloamsche Wikipedia moa zeldn grôte fysieke evenementn of massale schryfsessies organiseert, èn we lokaal gin aparte organisatoern in deze groep.
Oze eign [[{{ns:project}}:Bêerders|Bêerders]] èn gelukkig ouk aal deze rechtn automatisch in nunder knoppnpakket zitten. As ter dus een evenement g'organiseerd wordt in West-Vloandern, kan e bêerder da perfect zelve in goeie boann leydn.
== Lyste van organisatoern ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip de VLS wiki die deezn specifieken titel èn (bêerders zyn ier nie in ipgenomen, omda ze 't standaard al èn).
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=event-organizer&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live Organisatoern-lyste</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Bêerders|Bêerders]]
[[Categorie:Wikipedia]]
rzu480b9f03qh3tsjdm63zsszwrozql
Wikipedia:Geblokkeerd voorn IP-info
4
29039
326059
2026-04-03T21:26:11Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Stop hand.svg|thumb|right|120px|'t Symbool vo geblokkeerde toegang toet IP-info.]] '''Geblokkeerd voorn IP-info''' (in 't Iengels: ''no-ipinfo'') is e specioale straf-groep ip Wikipedia vo gebrukers wien nunder toegang toet de IP-info tool volledig is ofgepakt. == Wa is de IP-info tool? == De IP-info tool is e functie in de MediaWiki-software woarmee da bepoalde gebrukers mêer informoatie kunn'n ipvragen over een [[IP-adres]] (byvôorbeeld uut wukken streke dat…
326059
wikitext
text/x-wiki
[[Ofbeeldienge:Stop hand.svg|thumb|right|120px|'t Symbool vo geblokkeerde toegang toet IP-info.]]
'''Geblokkeerd voorn IP-info''' (in 't Iengels: ''no-ipinfo'') is e specioale straf-groep ip Wikipedia vo gebrukers wien nunder toegang toet de IP-info tool volledig is ofgepakt.
== Wa is de IP-info tool? ==
De IP-info tool is e functie in de MediaWiki-software woarmee da bepoalde gebrukers mêer informoatie kunn'n ipvragen over een [[IP-adres]] (byvôorbeeld uut wukken streke dat 't komt of van wukke internetprovider 't is). Da's stief andig vo vandalisme teegn te goan, moa 't is ouk e grôot privacy-risico.
== Waarom wordt etwien in deze groep gezet? ==
Omdat 't over persôonlikke informoatie goat, eit de Wikimedia Foundation uuterst strakke privacyregels ipgesteld. As e gebruker misbruuk makt van de IP-info tool (byvôorbeeld deur informoatie openboar te moakn of te spioneren zounder goeie reedn), wordt zyn toegang onmiddellik en volledig afgeslootn.
De gebruker wordt ton in deze "no-ipinfo" groep gezet, wa da lokaal ip de wiki zorgt dat de tool nimeer werkt vo dien account.
== Wie deelt deze straffe uut? ==
Omdat de privacyregels globaal zyn vo hêel Wikimedia, kunn'n oze lokale [[{{ns:project}}:Bêerders|Bêerders]] of [[{{ns:project}}:Burocraatn|Burocraatn]] deze straffe nie zelve uutdêeln of ongedoan moakn.
Ollêne de globale [[{{ns:project}}:Stewards|Stewards]] en vaste medewerkers van de Wikimedia Foundation èn 't recht om etwien in of uut deze geblokkeerde groep te platsn.
== Lyste van geblokkeerde gebrukers ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip deze wiki die nunder toegang toet de IP-info tool èn kwytgespeeld.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=no-ipinfo&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live lyste van Geblokkeerdn</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Stewards|Stewards]]
* [[{{ns:project}}:Inzoage IP-adressn tydelikke accounts|Inzoage IP-adressn tydelikke accounts]]
[[Categorie:Wikipedia]]
rw4yjtv35hom3nz29fk52xcejvufstv
326130
326059
2026-04-04T09:25:34Z
Kannotlogin
29153
/* Zie ôok */
326130
wikitext
text/x-wiki
[[Ofbeeldienge:Stop hand.svg|thumb|right|120px|'t Symbool vo geblokkeerde toegang toet IP-info.]]
'''Geblokkeerd voorn IP-info''' (in 't Iengels: ''no-ipinfo'') is e specioale straf-groep ip Wikipedia vo gebrukers wien nunder toegang toet de IP-info tool volledig is ofgepakt.
== Wa is de IP-info tool? ==
De IP-info tool is e functie in de MediaWiki-software woarmee da bepoalde gebrukers mêer informoatie kunn'n ipvragen over een [[IP-adres]] (byvôorbeeld uut wukken streke dat 't komt of van wukke internetprovider 't is). Da's stief andig vo vandalisme teegn te goan, moa 't is ouk e grôot privacy-risico.
== Waarom wordt etwien in deze groep gezet? ==
Omdat 't over persôonlikke informoatie goat, eit de Wikimedia Foundation uuterst strakke privacyregels ipgesteld. As e gebruker misbruuk makt van de IP-info tool (byvôorbeeld deur informoatie openboar te moakn of te spioneren zounder goeie reedn), wordt zyn toegang onmiddellik en volledig afgeslootn.
De gebruker wordt ton in deze "no-ipinfo" groep gezet, wa da lokaal ip de wiki zorgt dat de tool nimeer werkt vo dien account.
== Wie deelt deze straffe uut? ==
Omdat de privacyregels globaal zyn vo hêel Wikimedia, kunn'n oze lokale [[{{ns:project}}:Bêerders|Bêerders]] of [[{{ns:project}}:Burocraatn|Burocraatn]] deze straffe nie zelve uutdêeln of ongedoan moakn.
Ollêne de globale [[{{ns:project}}:Stewards|Stewards]] en vaste medewerkers van de Wikimedia Foundation èn 't recht om etwien in of uut deze geblokkeerde groep te platsn.
== Lyste van geblokkeerde gebrukers ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip deze wiki die nunder toegang toet de IP-info tool èn kwytgespeeld.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=no-ipinfo&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live lyste van Geblokkeerdn</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Stewards|Stewards]]
* [[{{ns:project}}:Inzage IP-adressen tijdelijke accounts|Inzoage IP-adressn tydelikke accounts]]
[[Categorie:Wikipedia]]
n9t8q8ils2eicjff6rci54hot1tagy3
326131
326130
2026-04-04T09:26:01Z
Kannotlogin
29153
veranderienge 326130 van [[Special:Contributions/Kannotlogin|Kannotlogin]] were weggedoan
326131
wikitext
text/x-wiki
[[Ofbeeldienge:Stop hand.svg|thumb|right|120px|'t Symbool vo geblokkeerde toegang toet IP-info.]]
'''Geblokkeerd voorn IP-info''' (in 't Iengels: ''no-ipinfo'') is e specioale straf-groep ip Wikipedia vo gebrukers wien nunder toegang toet de IP-info tool volledig is ofgepakt.
== Wa is de IP-info tool? ==
De IP-info tool is e functie in de MediaWiki-software woarmee da bepoalde gebrukers mêer informoatie kunn'n ipvragen over een [[IP-adres]] (byvôorbeeld uut wukken streke dat 't komt of van wukke internetprovider 't is). Da's stief andig vo vandalisme teegn te goan, moa 't is ouk e grôot privacy-risico.
== Waarom wordt etwien in deze groep gezet? ==
Omdat 't over persôonlikke informoatie goat, eit de Wikimedia Foundation uuterst strakke privacyregels ipgesteld. As e gebruker misbruuk makt van de IP-info tool (byvôorbeeld deur informoatie openboar te moakn of te spioneren zounder goeie reedn), wordt zyn toegang onmiddellik en volledig afgeslootn.
De gebruker wordt ton in deze "no-ipinfo" groep gezet, wa da lokaal ip de wiki zorgt dat de tool nimeer werkt vo dien account.
== Wie deelt deze straffe uut? ==
Omdat de privacyregels globaal zyn vo hêel Wikimedia, kunn'n oze lokale [[{{ns:project}}:Bêerders|Bêerders]] of [[{{ns:project}}:Burocraatn|Burocraatn]] deze straffe nie zelve uutdêeln of ongedoan moakn.
Ollêne de globale [[{{ns:project}}:Stewards|Stewards]] en vaste medewerkers van de Wikimedia Foundation èn 't recht om etwien in of uut deze geblokkeerde groep te platsn.
== Lyste van geblokkeerde gebrukers ==
Druk ip de knoppe ierounder vo de live lyste te bekykn van aal de gebrukers ip deze wiki die nunder toegang toet de IP-info tool èn kwytgespeeld.
<div class="plainlinks" style="display:inline-block; background-color:#3366cc; border:1px solid #2a4b8d; border-radius:4px; padding:10px 20px; font-size:14px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1);">[{{fullurl:Special:ListUsers|group=no-ipinfo&username=}} <span style="color:#ffffff; text-decoration:none;">Bekyk de live lyste van Geblokkeerdn</span>]</div>
== Zie ôok ==
* [[{{ns:project}}:Stewards|Stewards]]
* [[{{ns:project}}:Inzoage IP-adressn tydelikke accounts|Inzoage IP-adressn tydelikke accounts]]
[[Categorie:Wikipedia]]
rw4yjtv35hom3nz29fk52xcejvufstv
MediaWiki:Grouppage-oversight
8
29043
326066
2026-04-03T21:45:25Z
Kannotlogin
29153
nieuw blad: {{ns:project}}:Toezichtouwers
326066
wikitext
text/x-wiki
{{ns:project}}:Toezichtouwers
igyok1ne7jxa90xq1392qjg6z4fsryn
MediaWiki:Group-oversight
8
29044
326067
2026-04-03T21:47:50Z
Kannotlogin
29153
nieuw blad: Toezichtouwers
326067
wikitext
text/x-wiki
Toezichtouwers
8n3vurqpkq17zdagy0hduoz3v1y9i0n
MediaWiki:Group-suppress
8
29045
326068
2026-04-03T21:48:20Z
Kannotlogin
29153
nieuw blad: Toezichtouwers
326068
wikitext
text/x-wiki
Toezichtouwers
8n3vurqpkq17zdagy0hduoz3v1y9i0n
MediaWiki:Gadgets-definition
8
29046
326069
2026-04-03T21:50:04Z
Kannotlogin
29153
nieuw blad: == Browsn == * Navigation_popups[ResourceLoader|dependencies=mediawiki.api,mediawiki.user,mediawiki.util,user.options,mediawiki.jqueryMsg|type=general]|popups.js|navpop.css * ReferenceTooltips[ResourceLoader|default|skins=vector,vector-2022,monobook|type=general|dependencies=mediawiki.cookie,jquery.client]|ReferenceTooltips.js|ReferenceTooltips.css * ImageAnnotator[ResourceLoader]|ImageAnnotator.js * exlinks[ResourceLoader|dependencies=mediawiki.util]|exlinks.js == Gebrukersinf…
326069
wikitext
text/x-wiki
== Browsn ==
* Navigation_popups[ResourceLoader|dependencies=mediawiki.api,mediawiki.user,mediawiki.util,user.options,mediawiki.jqueryMsg|type=general]|popups.js|navpop.css
* ReferenceTooltips[ResourceLoader|default|skins=vector,vector-2022,monobook|type=general|dependencies=mediawiki.cookie,jquery.client]|ReferenceTooltips.js|ReferenceTooltips.css
* ImageAnnotator[ResourceLoader]|ImageAnnotator.js
* exlinks[ResourceLoader|dependencies=mediawiki.util]|exlinks.js
== Gebrukersinfo ==
* markblocked[ResourceLoader|dependencies=mediawiki.util,mediawiki.page.ready,mediawiki.Title]|markblocked.js
* Gebruikersinfo[ResourceLoader|dependencies=mediawiki.util]|Gebruikersinfo.js
* XTools-ArticleInfo[ResourceLoader]|XTools-ArticleInfo.js
== Bewerkn ==
* HotCat[ResourceLoader|rights=edit,minoredit]|HotCat.js
* charinsert[ResourceLoader|default|actions=edit]|charinsert.js
* charinsert-core[ResourceLoader|hidden|dependencies=jquery.textSelection,user,mediawiki.storage]|charinsert-styles.css|charinsert-core.js
* wikEdDiff[ResourceLoader]|wikEdDiff.js
* ProveIt[ResourceLoader|namespaces=0,2,10]|ProveIt.js
== Interface ==
* UTCLiveClock[ResourceLoader|supportsUrlLoad|type=general|dependencies=mediawiki.util,mediawiki.api]|UTCLiveClock.js|UTCLiveClock.css
* purgetab[ResourceLoader|dependencies=mediawiki.util,mediawiki.api]|purgetab.js
* edittop[ResourceLoader|dependencies=user.options,mediawiki.util|type=general]|edittop.js|edittop.css
* dropdown-menus[ResourceLoader|dependencies=mediawiki.api,mediawiki.util,mediawiki.Title,user.options]|MoreMenu.vls.js|dropdown-menus.js
== Ounderoud en Beheer ==
* Twinkle[ResourceLoader|dependencies=ext.gadget.morebits,ext.gadget.select2,mediawiki.api,mediawiki.language|type=general|rights=autoconfirmed]|Twinkle.js|Twinkle.css|twinklearv.js|twinkleunlink.js|twinklewarn.js|twinkleblock.js|friendlywelcome.js|twinklespeedy.js|twinklexfd.js|twinkleprotect.js|friendlytag.js|twinklediff.js|twinklefluff.js|twinklebatchdelete.js|twinklebatchprotect.js|twinklebatchundelete.js|twinkleconfig.js
* morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title,jquery.ui|hidden]|morebits.js|morebits.css
* select2[ResourceLoader|hidden]|select2.min.js|select2.min.css
* RTRC[ResourceLoader]|RTRC.js
* CleanDeleteReasons[ResourceLoader|rights=delete|default]|CleanDeleteReasons.js
jwgjqwnwl13j2ctq2m9xa8wzwy9istx
326101
326069
2026-04-04T09:01:01Z
Kannotlogin
29153
326101
wikitext
text/x-wiki
== Browsn ==
* Navigation_popups[ResourceLoader|dependencies=mediawiki.api,mediawiki.user,mediawiki.util,user.options,mediawiki.jqueryMsg|type=general]|popups.js|navpop.css
* ReferenceTooltips[ResourceLoader|default|skins=vector,vector-2022,monobook|type=general|dependencies=mediawiki.cookie,jquery.client]|ReferenceTooltips.js|ReferenceTooltips.css
* ImageAnnotator[ResourceLoader]|ImageAnnotator.js
* exlinks[ResourceLoader|dependencies=mediawiki.util]|exlinks.js
== Gebrukersinfo ==
* markblocked[ResourceLoader|dependencies=mediawiki.util,mediawiki.page.ready,mediawiki.Title]|markblocked.js
* Gebruikersinfo[ResourceLoader|dependencies=mediawiki.util]|Gebruikersinfo.js
* XTools-ArticleInfo[ResourceLoader]|XTools-ArticleInfo.js
== Bewerkn ==
* HotCat[ResourceLoader|rights=edit,minoredit]|HotCat.js
* charinsert[ResourceLoader|default|actions=edit]|charinsert.js
* charinsert-core[ResourceLoader|hidden|dependencies=jquery.textSelection,user,mediawiki.storage]|charinsert-styles.css|charinsert-core.js
* wikEdDiff[ResourceLoader]|wikEdDiff.js
* ProveIt[ResourceLoader|namespaces=0,2,10]|ProveIt.js
== Interface ==
* UTCLiveClock[ResourceLoader|supportsUrlLoad|type=general|dependencies=mediawiki.util,mediawiki.api]|UTCLiveClock.js|UTCLiveClock.css
* purgetab[ResourceLoader|dependencies=mediawiki.util,mediawiki.api]|purgetab.js
* edittop[ResourceLoader|dependencies=user.options,mediawiki.util|type=general]|edittop.js|edittop.css
* dropdown-menus[ResourceLoader|dependencies=mediawiki.api,mediawiki.util,mediawiki.Title,user.options]|MoreMenu.vls.js|dropdown-menus.js
== Ounderoud en Beheer ==
* Twinkle[ResourceLoader|dependencies=ext.gadget.morebits,ext.gadget.select2,mediawiki.api,mediawiki.language|type=general|rights=autoconfirmed]|Twinkle.js|Twinkle.css|twinklearv.js|twinkleunlink.js|twinklewarn.js|twinkleblock.js|friendlywelcome.js|twinklespeedy.js|twinklexfd.js|twinkleprotect.js|friendlytag.js|twinklediff.js|twinklefluff.js|twinklebatchdelete.js|twinklebatchprotect.js|twinklebatchundelete.js|twinkleconfig.js
* morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title,jquery.ui|hidden]|morebits.js|morebits.css
* select2[ResourceLoader|hidden]|select2.min.js|select2.min.css
* RTRC[ResourceLoader]|RTRC.js
mlhaosrestiq19c6tgr1ok73gd7pu7a
326122
326101
2026-04-04T09:13:36Z
Kannotlogin
29153
326122
wikitext
text/x-wiki
<noinclude>'''Wyzigiengn an deze pagina kunn'n e grôte impact èn ip d' ervoarienge van de gebrukers. Test olles goed uut.'''
Links om de gadget-beschryviengn te bekykn en te beheern stoan ip [[Specioal:Gadgets]].
</noinclude>
== Browsn ==
* Navigation_popups [ResourceLoader | dependencies=mediawiki.api, mediawiki.user, mediawiki.util, user.options, mediawiki.jqueryMsg | type=general] | popups.js | navpop.css
* ReferenceTooltips [ResourceLoader | default | skins=vector, vector-2022, monobook | type=general | dependencies=mediawiki.cookie, jquery.client] | ReferenceTooltips.js | ReferenceTooltips.css
* ImageAnnotator [ResourceLoader] | ImageAnnotator.js
* exlinks [ResourceLoader | dependencies=mediawiki.util] | exlinks.js
== Gebrukersinfo ==
* markblocked [ResourceLoader | dependencies=mediawiki.util, mediawiki.page.ready, mediawiki.Title] | markblocked.js
* Gebruikersinfo [ResourceLoader | dependencies=mediawiki.util] | Gebruikersinfo.js
* XTools-ArticleInfo [ResourceLoader] | XTools-ArticleInfo.js
== Bewerkn ==
* HotCat [ResourceLoader | rights=edit, minoredit] | HotCat.js
* charinsert [ResourceLoader | default | actions=edit] | charinsert.js
* charinsert-core [ResourceLoader | hidden | dependencies=jquery.textSelection, user, mediawiki.storage] | charinsert-styles.css | charinsert-core.js
* wikEdDiff [ResourceLoader] | wikEdDiff.js
* ProveIt [ResourceLoader | namespaces=0, 2, 10] | ProveIt.js
== Interface ==
* UTCLiveClock [ResourceLoader | supportsUrlLoad | type=general | dependencies=mediawiki.util, mediawiki.api] | UTCLiveClock.js | UTCLiveClock.css
* purgetab [ResourceLoader | dependencies=mediawiki.util, mediawiki.api] | purgetab.js
* edittop [ResourceLoader | dependencies=user.options, mediawiki.util | type=general] | edittop.js | edittop.css
* dropdown-menus [ResourceLoader | dependencies=mediawiki.api, mediawiki.util, mediawiki.Title, user.options] | MoreMenu.vls.js | dropdown-menus.js
== Ounderoud en Beheer ==
* Twinkle [ResourceLoader | dependencies=ext.gadget.morebits, ext.gadget.select2, mediawiki.api, mediawiki.language | type=general | rights=autoconfirmed] | Twinkle.js | Twinkle.css | twinklearv.js | twinkleunlink.js | twinklewarn.js | twinkleblock.js | friendlywelcome.js | twinklespeedy.js | twinklexfd.js | twinkleprotect.js | friendlytag.js | twinklediff.js | twinklefluff.js | twinklebatchdelete.js | twinklebatchprotect.js | twinklebatchundelete.js | twinkleconfig.js
* morebits [ResourceLoader | dependencies=mediawiki.user, mediawiki.util, mediawiki.Title, jquery.ui | hidden] | morebits.js | morebits.css
* select2 [ResourceLoader | hidden] | select2.min.js | select2.min.css
* RTRC [ResourceLoader] | RTRC.js
nqudqz01eetwec0wo3kpglx5ucz9hlc
MediaWiki:Gadget-popups.js
8
29047
326070
2026-04-04T08:50:28Z
Kannotlogin
29153
nieuw blad: // STARTFILE: main.js // ********************************************************************** // ** ** // ** changes to this file affect many users. ** // ** please discuss on the talk page before editing ** // ** ** // ********************************************************************** // **…
326070
javascript
text/javascript
// STARTFILE: main.js
// **********************************************************************
// ** **
// ** changes to this file affect many users. **
// ** please discuss on the talk page before editing **
// ** **
// **********************************************************************
// ** **
// ** if you do edit this file, be sure that your editor recognizes it **
// ** as utf8, or the weird and wonderful characters in the namespaces **
// ** below will be completely broken. You can check with the show **
// ** changes button before submitting the edit. **
// ** test: مدیا מיוחד Мэдыя **
// ** **
// **********************************************************************
/* eslint-env browser */
/* global $, jQuery, mw, window */
// Fix later
/* global log, errlog, popupStrings, wikEdUseWikEd, WikEdUpdateFrame */
$(() => {
//////////////////////////////////////////////////
// Globals
//
// Trying to shove as many of these as possible into the pg (popup globals) object
const pg = {
api: {}, // MediaWiki API requests
re: {}, // regexps
ns: {}, // namespaces
string: {}, // translatable strings
wiki: {}, // local site info
user: {}, // current user info
misc: {}, // YUCK PHOOEY
option: {}, // options, see newOption etc
optionDefault: {}, // default option values
flag: {}, // misc flags
cache: {}, // page and image cache
structures: {}, // navlink structures
timer: {}, // all sorts of timers (too damn many)
counter: {}, // .. and all sorts of counters
current: {}, // state info
fn: {}, // functions
endoflist: null,
};
/* Bail if the gadget/script is being loaded twice */
/* An element with id "pg" would add a window.pg property, ignore such property */
if (window.pg && !(window.pg instanceof HTMLElement)) {
return;
}
/* Export to global context */
window.pg = pg;
/// Local Variables: ///
/// mode:c ///
/// End: ///
// ENDFILE: main.js
// STARTFILE: actions.js
function setupTooltips(container, remove, force, popData) {
log('setupTooltips, container=' + container + ', remove=' + remove);
if (!container) {
// the main initial call
if (
getValueOf('popupOnEditSelection') &&
document &&
document.editform &&
document.editform.wpTextbox1
) {
document.editform.wpTextbox1.onmouseup = doSelectionPopup;
}
// article/content is a structure-dependent thing
container = defaultPopupsContainer();
}
if (!remove && !force && container.ranSetupTooltipsAlready) {
return;
}
container.ranSetupTooltipsAlready = !remove;
let anchors;
anchors = container.getElementsByTagName('A');
setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);
}
function defaultPopupsContainer() {
if (getValueOf('popupOnlyArticleLinks')) {
return (
document.querySelector('.skin-vector-2022 .vector-body') ||
document.getElementById('mw_content') ||
document.getElementById('content') ||
document.getElementById('article') ||
document
);
}
return document;
}
function setupTooltipsLoop(anchors, begin, howmany, sleep, remove, popData) {
log(simplePrintf('setupTooltipsLoop(%s,%s,%s,%s,%s)', arguments));
const finish = begin + howmany;
const loopend = Math.min(finish, anchors.length);
let j = loopend - begin;
log(
'setupTooltips: anchors.length=' +
anchors.length +
', begin=' +
begin +
', howmany=' +
howmany +
', loopend=' +
loopend +
', remove=' +
remove
);
const doTooltip = remove ? removeTooltip : addTooltip;
// try a faster (?) loop construct
if (j > 0) {
do {
const a = anchors[loopend - j];
if (typeof a === 'undefined' || !a || !a.href) {
log('got null anchor at index ' + loopend - j);
continue;
}
doTooltip(a, popData);
} while (--j);
}
if (finish < anchors.length) {
setTimeout(() => {
setupTooltipsLoop(anchors, finish, howmany, sleep, remove, popData);
}, sleep);
} else {
if (!remove && !getValueOf('popupTocLinks')) {
rmTocTooltips();
}
pg.flag.finishedLoading = true;
}
}
// eliminate popups from the TOC
// This also kills any onclick stuff that used to be going on in the toc
function rmTocTooltips() {
const toc = document.getElementById('toc');
if (toc) {
const tocLinks = toc.getElementsByTagName('A');
const tocLen = tocLinks.length;
for (let j = 0; j < tocLen; ++j) {
removeTooltip(tocLinks[j], true);
}
}
}
function addTooltip(a, popData) {
if (!isPopupLink(a)) {
return;
}
a.onmouseover = mouseOverWikiLink;
a.onmouseout = mouseOutWikiLink;
a.onmousedown = killPopup;
a.hasPopup = true;
a.popData = popData;
}
function removeTooltip(a) {
if (!a.hasPopup) {
return;
}
a.onmouseover = null;
a.onmouseout = null;
if (a.originalTitle) {
a.title = a.originalTitle;
}
a.hasPopup = false;
}
function removeTitle(a) {
if (!a.originalTitle) {
a.originalTitle = a.title;
}
a.title = '';
}
function restoreTitle(a) {
if (a.title || !a.originalTitle) {
return;
}
a.title = a.originalTitle;
}
function registerHooks(np) {
const popupMaxWidth = getValueOf('popupMaxWidth');
if (typeof popupMaxWidth === 'number') {
const setMaxWidth = function () {
np.mainDiv.style.maxWidth = popupMaxWidth + 'px';
np.maxWidth = popupMaxWidth;
};
np.addHook(setMaxWidth, 'unhide', 'before');
}
np.addHook(addPopupShortcuts, 'unhide', 'after');
np.addHook(rmPopupShortcuts, 'hide', 'before');
}
function removeModifierKeyHandler(a) {
//remove listeners for modifier key if any that were added in mouseOverWikiLink
document.removeEventListener('keydown', a.modifierKeyHandler, false);
document.removeEventListener('keyup', a.modifierKeyHandler, false);
}
function mouseOverWikiLink(evt) {
if (!evt && window.event) {
evt = window.event;
}
// if the modifier is needed, listen for it,
// we will remove the listener when we mouseout of this link or kill popup.
if (getValueOf('popupModifier')) {
// if popupModifierAction = enable, we should popup when the modifier is pressed
// if popupModifierAction = disable, we should popup unless the modifier is pressed
const action = getValueOf('popupModifierAction');
const key = action == 'disable' ? 'keyup' : 'keydown';
const a = this;
a.modifierKeyHandler = function (evt) {
mouseOverWikiLink2(a, evt);
};
document.addEventListener(key, a.modifierKeyHandler, false);
}
return mouseOverWikiLink2(this, evt);
}
/**
* Gets the references list item that the provided footnote link targets. This
* is typically a li element within the ol.references element inside the reflist.
*
* @param {Element} a - A footnote link.
* @return {Element|boolean} The targeted element, or false if one can't be found.
*/
function footnoteTarget(a) {
const aTitle = Title.fromAnchor(a);
// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly
const anch = aTitle.anchor;
if (!/^(cite_note-|_note-|endnote)/.test(anch)) {
return false;
}
const lTitle = Title.fromURL(location.href);
if (lTitle.toString(true) !== aTitle.toString(true)) {
return false;
}
let el = document.getElementById(anch);
while (el && typeof el.nodeName === 'string') {
const nt = el.nodeName.toLowerCase();
if (nt === 'li') {
return el;
} else if (nt === 'body') {
return false;
} else if (el.parentNode) {
el = el.parentNode;
} else {
return false;
}
}
return false;
}
function footnotePreview(x, navpop) {
setPopupHTML('<hr />' + x.innerHTML, 'popupPreview', navpop.idNumber);
}
function modifierPressed(evt) {
const mod = getValueOf('popupModifier');
if (!mod) {
return false;
}
if (!evt && window.event) {
evt = window.event;
}
return evt && mod && evt[mod.toLowerCase() + 'Key'];
}
// Checks if the correct modifier pressed/unpressed if needed
function isCorrectModifier(a, evt) {
if (!getValueOf('popupModifier')) {
return true;
}
// if popupModifierAction = enable, we should popup when the modifier is pressed
// if popupModifierAction = disable, we should popup unless the modifier is pressed
const action = getValueOf('popupModifierAction');
return (
(action == 'enable' && modifierPressed(evt)) || (action == 'disable' && !modifierPressed(evt))
);
}
function mouseOverWikiLink2(a, evt) {
if (!isCorrectModifier(a, evt)) {
return;
}
if (getValueOf('removeTitles')) {
removeTitle(a);
}
if (a == pg.current.link && a.navpopup && a.navpopup.isVisible()) {
return;
}
pg.current.link = a;
if (getValueOf('simplePopups') && !pg.option.popupStructure) {
// reset *default value* of popupStructure
setDefault('popupStructure', 'original');
}
const article = new Title().fromAnchor(a);
// set global variable (ugh) to hold article (wikipage)
pg.current.article = article;
if (!a.navpopup) {
a.navpopup = newNavpopup(a, article);
pg.current.linksHash[a.href] = a.navpopup;
pg.current.links.push(a);
}
if (a.navpopup.pending === null || a.navpopup.pending !== 0) {
// either fresh popups or those with unfinshed business are redone from scratch
simplePopupContent(a, article);
}
a.navpopup.showSoonIfStable(a.navpopup.delay);
clearInterval(pg.timer.checkPopupPosition);
pg.timer.checkPopupPosition = setInterval(checkPopupPosition, 600);
if (getValueOf('simplePopups')) {
if (getValueOf('popupPreviewButton') && !a.simpleNoMore) {
const d = document.createElement('div');
d.className = 'popupPreviewButtonDiv';
const s = document.createElement('span');
d.appendChild(s);
s.className = 'popupPreviewButton';
s['on' + getValueOf('popupPreviewButtonEvent')] = function () {
a.simpleNoMore = true;
d.style.display = 'none';
nonsimplePopupContent(a, article);
};
s.innerHTML = popupString('show preview');
setPopupHTML(d, 'popupPreview', a.navpopup.idNumber);
}
}
if (a.navpopup.pending !== 0) {
nonsimplePopupContent(a, article);
}
}
// simplePopupContent: the content that do not require additional download
// (it is shown even when simplePopups is true)
function simplePopupContent(a, article) {
/* FIXME hack */ a.navpopup.hasPopupMenu = false;
a.navpopup.setInnerHTML(popupHTML(a));
fillEmptySpans({ navpopup: a.navpopup });
if (getValueOf('popupDraggable')) {
let dragHandle = getValueOf('popupDragHandle') || null;
if (dragHandle && dragHandle != 'all') {
dragHandle += a.navpopup.idNumber;
}
setTimeout(() => {
a.navpopup.makeDraggable(dragHandle);
}, 150);
}
if (getValueOf('popupRedlinkRemoval') && a.className == 'new') {
setPopupHTML('<br>' + popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);
}
}
function debugData(navpopup) {
if (getValueOf('popupDebugging') && navpopup.idNumber) {
setPopupHTML(
'idNumber=' + navpopup.idNumber + ', pending=' + navpopup.pending,
'popupError',
navpopup.idNumber
);
}
}
function newNavpopup(a, article) {
const navpopup = new Navpopup();
navpopup.fuzz = 5;
navpopup.delay = getValueOf('popupDelay') * 1000;
// increment global counter now
navpopup.idNumber = ++pg.idNumber;
navpopup.parentAnchor = a;
navpopup.parentPopup = a.popData && a.popData.owner;
navpopup.article = article;
registerHooks(navpopup);
return navpopup;
}
// Should we show nonsimple context?
// If simplePopups is set to true, then we do not show nonsimple context,
// but if a bottom "show preview" was clicked we do show nonsimple context
function shouldShowNonSimple(a) {
return !getValueOf('simplePopups') || a.simpleNoMore;
}
// Should we show nonsimple context govern by the option (e.g. popupUserInfo)?
// If the user explicitly asked for nonsimple context by setting the option to true,
// then we show it even in nonsimple mode.
function shouldShow(a, option) {
if (shouldShowNonSimple(a)) {
return getValueOf(option);
} else {
return typeof window[option] != 'undefined' && window[option];
}
}
function nonsimplePopupContent(a, article) {
let diff = null,
history = null;
const params = parseParams(a.href);
const oldid = typeof params.oldid == 'undefined' ? null : params.oldid;
if (shouldShow(a, 'popupPreviewDiffs')) {
diff = params.diff;
}
if (shouldShow(a, 'popupPreviewHistory')) {
history = params.action == 'history';
}
a.navpopup.pending = 0;
const referenceElement = footnoteTarget(a);
if (referenceElement) {
footnotePreview(referenceElement, a.navpopup);
} else if (diff || diff === 0) {
loadDiff(article, oldid, diff, a.navpopup);
} else if (history) {
loadAPIPreview('history', article, a.navpopup);
} else if (shouldShowNonSimple(a) && pg.re.contribs.test(a.href)) {
loadAPIPreview('contribs', article, a.navpopup);
} else if (shouldShowNonSimple(a) && pg.re.backlinks.test(a.href)) {
loadAPIPreview('backlinks', article, a.navpopup);
} else if (
// FIXME should be able to get all preview combinations with options
article.namespaceId() == pg.nsImageId &&
(shouldShow(a, 'imagePopupsForImages') || !anchorContainsImage(a))
) {
loadAPIPreview('imagepagepreview', article, a.navpopup);
loadImage(article, a.navpopup);
} else {
if (article.namespaceId() == pg.nsCategoryId && shouldShow(a, 'popupCategoryMembers')) {
loadAPIPreview('category', article, a.navpopup);
} else if (
(article.namespaceId() == pg.nsUserId || article.namespaceId() == pg.nsUsertalkId) &&
shouldShow(a, 'popupUserInfo')
) {
loadAPIPreview('userinfo', article, a.navpopup);
}
if (shouldShowNonSimple(a)) {
startArticlePreview(article, oldid, a.navpopup);
}
}
}
function pendingNavpopTask(navpop) {
if (navpop && navpop.pending === null) {
navpop.pending = 0;
}
++navpop.pending;
debugData(navpop);
}
function completedNavpopTask(navpop) {
if (navpop && navpop.pending) {
--navpop.pending;
}
debugData(navpop);
}
function startArticlePreview(article, oldid, navpop) {
navpop.redir = 0;
loadPreview(article, oldid, navpop);
}
function loadPreview(article, oldid, navpop) {
if (!navpop.redir) {
navpop.originalArticle = article;
}
article.oldid = oldid;
loadAPIPreview('revision', article, navpop);
}
function loadPreviewFromRedir(redirMatch, navpop) {
// redirMatch is a regex match
const target = new Title().fromWikiText(redirMatch[2]);
// overwrite (or add) anchor from original target
// mediawiki does overwrite; eg [[User:Lupin/foo3#Done]]
if (navpop.article.anchor) {
target.anchor = navpop.article.anchor;
}
navpop.redir++;
navpop.redirTarget = target;
const warnRedir = redirLink(target, navpop.article);
setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);
navpop.article = target;
fillEmptySpans({ redir: true, redirTarget: target, navpopup: navpop });
return loadPreview(target, null, navpop);
}
function insertPreview(download) {
if (!download.owner) {
return;
}
const redirMatch = pg.re.redirect.exec(download.data);
if (download.owner.redir === 0 && redirMatch) {
loadPreviewFromRedir(redirMatch, download.owner);
return;
}
if (download.owner.visible || !getValueOf('popupLazyPreviews')) {
insertPreviewNow(download);
} else {
const id = download.owner.redir ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';
download.owner.addHook(
() => {
insertPreviewNow(download);
return true;
},
'unhide',
'after',
id
);
}
}
function insertPreviewNow(download) {
if (!download.owner) {
return;
}
const wikiText = download.data;
const navpop = download.owner;
const art = navpop.redirTarget || navpop.originalArticle;
makeFixDabs(wikiText, navpop);
if (getValueOf('popupSummaryData')) {
getPageInfo(wikiText, download);
setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);
}
let imagePage = '';
if (art.namespaceId() == pg.nsImageId) {
imagePage = art.toString();
} else {
imagePage = getValidImageFromWikiText(wikiText);
}
if (imagePage) {
loadImage(Title.fromWikiText(imagePage), navpop);
}
if (getValueOf('popupPreviews')) {
insertArticlePreview(download, art, navpop);
}
}
function insertArticlePreview(download, art, navpop) {
if (download && typeof download.data == typeof '') {
if (art.namespaceId() == pg.nsTemplateId && getValueOf('popupPreviewRawTemplates')) {
// FIXME compare/consolidate with diff escaping code for wikitext
const h =
'<hr /><span style="font-family: monospace;">' +
download.data.entify().split('\\n').join('<br />\\n') +
'</span>';
setPopupHTML(h, 'popupPreview', navpop.idNumber);
} else {
const p = prepPreviewmaker(download.data, art, navpop);
p.showPreview();
}
}
}
function prepPreviewmaker(data, article, navpop) {
// deal with tricksy anchors
const d = anchorize(data, article.anchorString());
const urlBase = joinPath([pg.wiki.articlebase, article.urlString()]);
const p = new Previewmaker(d, urlBase, navpop);
return p;
}
// Try to imitate the way mediawiki generates HTML anchors from section titles
function anchorize(d, anch) {
if (!anch) {
return d;
}
const anchRe = RegExp(
'(?:=+\\s*' +
literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') +
'\\s*=+|\\{\\{\\s*' +
getValueOf('popupAnchorRegexp') +
'\\s*(?:\\|[^|}]*)*?\\s*' +
literalizeRegex(anch) +
'\\s*(?:\\|[^}]*)?}})'
);
const match = d.match(anchRe);
if (match && match.length > 0 && match[0]) {
return d.substring(d.indexOf(match[0]));
}
// now try to deal with == foo [[bar|baz]] boom == -> #foo_baz_boom
const lines = d.split('\n');
for (let i = 0; i < lines.length; ++i) {
lines[i] = lines[i]
.replace(/[[]{2}([^|\]]*?[|])?(.*?)[\]]{2}/g, '$2')
.replace(/'''([^'])/g, '$1')
.replace(/''([^'])/g, '$1');
if (lines[i].match(anchRe)) {
return d.split('\n').slice(i).join('\n').replace(/^[^=]*/, '');
}
}
return d;
}
function killPopup() {
removeModifierKeyHandler(this);
if (getValueOf('popupShortcutKeys')) {
rmPopupShortcuts();
}
if (!pg) {
return;
}
if (pg.current.link && pg.current.link.navpopup) {
pg.current.link.navpopup.banish();
}
pg.current.link = null;
abortAllDownloads();
if (pg.timer.checkPopupPosition) {
clearInterval(pg.timer.checkPopupPosition);
pg.timer.checkPopupPosition = null;
}
return true; // preserve default action
}
// ENDFILE: actions.js
// STARTFILE: domdrag.js
/**
* @file
* The {@link Drag} object, which enables objects to be dragged around.
*
* <pre>
* *************************************************
* dom-drag.js
* 09.25.2001
* www.youngpup.net
* **************************************************
* 10.28.2001 - fixed minor bug where events
* sometimes fired off the handle, not the root.
* *************************************************
* Pared down, some hooks added by [[User:Lupin]]
*
* Copyright Aaron Boodman.
* Saying stupid things daily since March 2001.
* </pre>
*/
/**
* Creates a new Drag object. This is used to make various DOM elements draggable.
*
* @constructor
*/
function Drag() {
/**
* Condition to determine whether or not to drag. This function should take one parameter,
* an Event. To disable this, set it to <code>null</code>.
*
* @type {Function}
*/
this.startCondition = null;
/**
* Hook to be run when the drag finishes. This is passed the final coordinates of the
* dragged object (two integers, x and y). To disables this, set it to <code>null</code>.
*
* @type {Function}
*/
this.endHook = null;
}
/**
* Gets an event in a cross-browser manner.
*
* @param {Event} e
* @private
*/
Drag.prototype.fixE = function (e) {
if (typeof e == 'undefined') {
e = window.event;
}
if (typeof e.layerX == 'undefined') {
e.layerX = e.offsetX;
}
if (typeof e.layerY == 'undefined') {
e.layerY = e.offsetY;
}
return e;
};
/**
* Initialises the Drag instance by telling it which object you want to be draggable, and what
* you want to drag it by.
*
* @param {HTMLElement} o The "handle" by which <code>oRoot</code> is dragged.
* @param {HTMLElement} oRoot The object which moves when <code>o</code> is dragged, or <code>o</code> if omitted.
*/
Drag.prototype.init = function (o, oRoot) {
const dragObj = this;
this.obj = o;
o.onmousedown = function (e) {
dragObj.start.apply(dragObj, [e]);
};
o.dragging = false;
o.popups_draggable = true;
o.hmode = true;
o.vmode = true;
o.root = oRoot || o;
if (isNaN(parseInt(o.root.style.left, 10))) {
o.root.style.left = '0px';
}
if (isNaN(parseInt(o.root.style.top, 10))) {
o.root.style.top = '0px';
}
o.root.onthisStart = function () {};
o.root.onthisEnd = function () {};
o.root.onthis = function () {};
};
/**
* Starts the drag.
*
* @private
* @param {Event} e
*/
Drag.prototype.start = function (e) {
const o = this.obj; // = this;
e = this.fixE(e);
if (this.startCondition && !this.startCondition(e)) {
return;
}
const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
const x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
o.root.onthisStart(x, y);
o.lastMouseX = e.clientX;
o.lastMouseY = e.clientY;
const dragObj = this;
o.onmousemoveDefault = document.onmousemove;
o.dragging = true;
document.onmousemove = function (e) {
dragObj.drag.apply(dragObj, [e]);
};
document.onmouseup = function (e) {
dragObj.end.apply(dragObj, [e]);
};
return false;
};
/**
* Does the drag.
*
* @param {Event} e
* @private
*/
Drag.prototype.drag = function (e) {
e = this.fixE(e);
const o = this.obj;
const ey = e.clientY;
const ex = e.clientX;
const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
const x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
let nx, ny;
nx = x + (ex - o.lastMouseX) * (o.hmode ? 1 : -1);
ny = y + (ey - o.lastMouseY) * (o.vmode ? 1 : -1);
this.obj.root.style[o.hmode ? 'left' : 'right'] = nx + 'px';
this.obj.root.style[o.vmode ? 'top' : 'bottom'] = ny + 'px';
this.obj.lastMouseX = ex;
this.obj.lastMouseY = ey;
this.obj.root.onthis(nx, ny);
return false;
};
/**
* Ends the drag.
*
* @private
*/
Drag.prototype.end = function () {
document.onmousemove = this.obj.onmousemoveDefault;
document.onmouseup = null;
this.obj.dragging = false;
if (this.endHook) {
this.endHook(
parseInt(this.obj.root.style[this.obj.hmode ? 'left' : 'right'], 10),
parseInt(this.obj.root.style[this.obj.vmode ? 'top' : 'bottom'], 10)
);
}
};
// ENDFILE: domdrag.js
// STARTFILE: structures.js
pg.structures.original = {};
pg.structures.original.popupLayout = function () {
return [
'popupError',
'popupImage',
'popupTopLinks',
'popupTitle',
'popupUserData',
'popupData',
'popupOtherLinks',
'popupRedir',
[
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
],
'popupMiscTools',
['popupRedlink'],
'popupPrePreviewSep',
'popupPreview',
'popupSecondPreview',
'popupPreviewMore',
'popupPostPreview',
'popupFixDab',
];
};
pg.structures.original.popupRedirSpans = function () {
return [
'popupRedir',
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
];
};
pg.structures.original.popupTitle = function (x) {
log('defaultstructure.popupTitle');
if (!getValueOf('popupNavLinks')) {
return navlinkStringToHTML('<b><<mainlink>></b>', x.article, x.params);
}
return '';
};
pg.structures.original.popupTopLinks = function (x) {
log('defaultstructure.popupTopLinks');
if (getValueOf('popupNavLinks')) {
return navLinksHTML(x.article, x.hint, x.params);
}
return '';
};
pg.structures.original.popupImage = function (x) {
log('original.popupImage, x.article=' + x.article + ', x.navpop.idNumber=' + x.navpop.idNumber);
return imageHTML(x.article, x.navpop.idNumber);
};
pg.structures.original.popupRedirTitle = pg.structures.original.popupTitle;
pg.structures.original.popupRedirTopLinks = pg.structures.original.popupTopLinks;
function copyStructure(oldStructure, newStructure) {
pg.structures[newStructure] = {};
for (const prop in pg.structures[oldStructure]) {
pg.structures[newStructure][prop] = pg.structures[oldStructure][prop];
}
}
copyStructure('original', 'nostalgia');
pg.structures.nostalgia.popupTopLinks = function (x) {
let str = '';
str += '<b><<mainlink|shortcut= >></b>';
// user links
// contribs - log - count - email - block
// count only if applicable; block only if popupAdminLinks
str += 'if(user){<br><<contribs|shortcut=c>>';
str += 'if(wikimedia){*<<count|shortcut=#>>}';
str += 'if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>}}';
// editing links
// talkpage -> edit|new - history - un|watch - article|edit
// other page -> edit - history - un|watch - talk|edit|new
const editstr = '<<edit|shortcut=e>>';
const editOldidStr =
'if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' +
editstr +
'}';
const historystr = '<<history|shortcut=h>>';
const watchstr = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
str +=
'<br>if(talk){' +
editOldidStr +
'|<<new|shortcut=+>>' +
'*' +
historystr +
'*' +
watchstr +
'*' +
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
'}else{' + // not a talk page
editOldidStr +
'*' +
historystr +
'*' +
watchstr +
'*' +
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
// misc links
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>';
str += 'if(admin){<br>}else{*}<<move|shortcut=m>>';
// admin links
str +=
'if(admin){*<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*' +
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>}';
return navlinkStringToHTML(str, x.article, x.params);
};
pg.structures.nostalgia.popupRedirTopLinks = pg.structures.nostalgia.popupTopLinks;
/** -- fancy -- */
copyStructure('original', 'fancy');
pg.structures.fancy.popupTitle = function (x) {
return navlinkStringToHTML('<font size=+0><<mainlink>></font>', x.article, x.params);
};
pg.structures.fancy.popupTopLinks = function (x) {
const hist =
'<<history|shortcut=h|hist>>|<<lastEdit|shortcut=/|last>>|<<editors|shortcut=E|eds>>';
const watch = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
const move = '<<move|shortcut=m|move>>';
return navlinkStringToHTML(
'if(talk){' +
'<<edit|shortcut=e>>|<<new|shortcut=+|+>>*' +
hist +
'*' +
'<<article|shortcut=a>>|<<editArticle|edit>>' +
'*' +
watch +
'*' +
move +
'}else{<<edit|shortcut=e>>*' +
hist +
'*<<talk|shortcut=t|>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>' +
'*' +
watch +
'*' +
move +
'}<br>',
x.article,
x.params
);
};
pg.structures.fancy.popupOtherLinks = function (x) {
const admin =
'<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*<<undelete|undeleteShort>>|<<delete|shortcut=d|del>>';
let user = '<<contribs|shortcut=c>>if(wikimedia){|<<count|shortcut=#|#>>}';
user +=
'if(ipuser){|<<arin>>}else{*<<email|shortcut=E|' +
popupString('email') +
'>>}if(admin){*<<block|shortcut=b>>}';
const normal = '<<whatLinksHere|shortcut=l|links here>>*<<relatedChanges|shortcut=r|related>>';
return navlinkStringToHTML(
'<br>if(user){' + user + '*}if(admin){' + admin + 'if(user){<br>}else{*}}' + normal,
x.article,
x.params
);
};
pg.structures.fancy.popupRedirTitle = pg.structures.fancy.popupTitle;
pg.structures.fancy.popupRedirTopLinks = pg.structures.fancy.popupTopLinks;
pg.structures.fancy.popupRedirOtherLinks = pg.structures.fancy.popupOtherLinks;
/** -- fancy2 -- */
// hack for [[User:MacGyverMagic]]
copyStructure('fancy', 'fancy2');
pg.structures.fancy2.popupTopLinks = function (x) {
// hack out the <br> at the end and put one at the beginning
return '<br>' + pg.structures.fancy.popupTopLinks(x).replace(/<br>$/i, '');
};
pg.structures.fancy2.popupLayout = function () {
// move toplinks to after the title
return [
'popupError',
'popupImage',
'popupTitle',
'popupUserData',
'popupData',
'popupTopLinks',
'popupOtherLinks',
'popupRedir',
[
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
],
'popupMiscTools',
['popupRedlink'],
'popupPrePreviewSep',
'popupPreview',
'popupSecondPreview',
'popupPreviewMore',
'popupPostPreview',
'popupFixDab',
];
};
/** -- menus -- */
copyStructure('original', 'menus');
pg.structures.menus.popupLayout = function () {
return [
'popupError',
'popupImage',
'popupTopLinks',
'popupTitle',
'popupOtherLinks',
'popupRedir',
[
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
],
'popupUserData',
'popupData',
'popupMiscTools',
['popupRedlink'],
'popupPrePreviewSep',
'popupPreview',
'popupSecondPreview',
'popupPreviewMore',
'popupPostPreview',
'popupFixDab',
];
};
pg.structures.menus.popupTopLinks = function (x, shorter) {
// FIXME maybe this stuff should be cached
const s = [];
const dropclass = 'popup_drop';
const enddiv = '</div>';
let hist = '<<history|shortcut=h>>';
if (!shorter) {
hist = '<menurow>' + hist + '|<<historyfeed|rss>>|<<editors|shortcut=E>></menurow>';
}
const lastedit = '<<lastEdit|shortcut=/|show last edit>>';
const thank = 'if(diff){<<thank|send thanks>>}';
const jsHistory = '<<lastContrib|last set of edits>><<sinceMe|changes since mine>>';
const linkshere = '<<whatLinksHere|shortcut=l|what links here>>';
const related = '<<relatedChanges|shortcut=r|related changes>>';
const search =
'<menurow><<search|shortcut=s>>if(wikimedia){|<<globalsearch|shortcut=g|global>>}' +
'|<<google|shortcut=G|web>></menurow>';
const watch = '<menurow><<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>></menurow>';
const protect =
'<menurow><<unprotect|unprotectShort>>|' +
'<<protect|shortcut=p>>|<<protectlog|log>></menurow>';
const del =
'<menurow><<undelete|undeleteShort>>|<<delete|shortcut=d>>|<<deletelog|log>></menurow>';
const move = '<<move|shortcut=m|move page>>';
const nullPurge = '<menurow><<nullEdit|shortcut=n|null edit>>|<<purge|shortcut=P>></menurow>';
const viewOptions = '<menurow><<view|shortcut=v>>|<<render|shortcut=S>>|<<raw>></menurow>';
const editRow =
'if(oldid){' +
'<menurow><<edit|shortcut=e>>|<<editOld|shortcut=e|this revision>></menurow>' +
'<menurow><<revert|shortcut=v>>|<<undo>></menurow>' +
'}else{<<edit|shortcut=e>>}';
const markPatrolled = 'if(rcid){<<markpatrolled|mark patrolled>>}';
const newTopic = 'if(talk){<<new|shortcut=+|new topic>>}';
const protectDelete = 'if(admin){' + protect + del + '}';
if (getValueOf('popupActionsMenu')) {
s.push('<<mainlink>>*' + menuTitle(dropclass, 'actions'));
} else {
s.push('<div class="' + dropclass + '"><<mainlink>>');
}
s.push('<menu>');
s.push(editRow + markPatrolled + newTopic + hist + lastedit + thank);
if (!shorter) {
s.push(jsHistory);
}
s.push(move + linkshere + related);
if (!shorter) {
s.push(nullPurge + search);
}
if (!shorter) {
s.push(viewOptions);
}
s.push('<hr />' + watch + protectDelete);
s.push(
'<hr />' +
'if(talk){<<article|shortcut=a|view article>><<editArticle|edit article>>}' +
'else{<<talk|shortcut=t|talk page>><<editTalk|edit talk>>' +
'<<newTalk|shortcut=+|new topic>>}</menu>' +
enddiv
);
// user menu starts here
const email = '<<email|shortcut=E|email user>>';
const contribs =
'if(wikimedia){<menurow>}<<contribs|shortcut=c|contributions>>if(wikimedia){</menurow>}' +
'if(admin){<menurow><<deletedContribs>></menurow>}';
s.push('if(user){*' + menuTitle(dropclass, 'user'));
s.push('<menu>');
s.push('<menurow><<userPage|shortcut=u|user page>>|<<userSpace|space>></menurow>');
s.push(
'<<userTalk|shortcut=t|user talk>><<editUserTalk|edit user talk>>' +
'<<newUserTalk|shortcut=+|leave comment>>'
);
if (!shorter) {
s.push('if(ipuser){<<arin>>}else{' + email + '}');
} else {
s.push('if(ipuser){}else{' + email + '}');
}
s.push('<hr />' + contribs + '<<userlog|shortcut=L|user log>>');
s.push('if(wikimedia){<<count|shortcut=#|edit counter>>}');
s.push(
'if(admin){<menurow><<unblock|unblockShort>>|<<block|shortcut=b|block user>></menurow>}'
);
s.push('<<blocklog|shortcut=B|block log>>');
s.push('</menu>' + enddiv + '}');
// popups menu starts here
if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) {
x.navpop.hasPopupMenu = true;
s.push('*' + menuTitle(dropclass, 'popupsMenu') + '<menu>');
s.push('<<togglePreviews|toggle previews>>');
s.push('<<purgePopups|reset>>');
s.push('<<disablePopups|disable>>');
s.push('</menu>' + enddiv);
}
return navlinkStringToHTML(s.join(''), x.article, x.params);
};
function menuTitle(dropclass, s) {
const text = popupString(s); // i18n
const len = text.length;
return '<div class="' + dropclass + '" style="--navpop-m-len:' + len + 'ch"><a href="#" noPopup=1>' + text + '</a>';
}
pg.structures.menus.popupRedirTitle = pg.structures.menus.popupTitle;
pg.structures.menus.popupRedirTopLinks = pg.structures.menus.popupTopLinks;
copyStructure('menus', 'shortmenus');
pg.structures.shortmenus.popupTopLinks = function (x) {
return pg.structures.menus.popupTopLinks(x, true);
};
pg.structures.shortmenus.popupRedirTopLinks = pg.structures.shortmenus.popupTopLinks;
pg.structures.lite = {};
pg.structures.lite.popupLayout = function () {
return ['popupTitle', 'popupPreview'];
};
pg.structures.lite.popupTitle = function (x) {
log(x.article + ': structures.lite.popupTitle');
//return navlinkStringToHTML('<b><<mainlink>></b>',x.article,x.params);
return '<div><span class="popup_mainlink"><b>' + x.article.toString() + '</b></span></div>';
};
// ENDFILE: structures.js
// STARTFILE: autoedit.js
function substitute(data, cmdBody) {
// alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags);
const fromRe = RegExp(cmdBody.from, cmdBody.flags);
return data.replace(fromRe, cmdBody.to);
}
function execCmds(data, cmdList) {
for (let i = 0; i < cmdList.length; ++i) {
data = cmdList[i].action(data, cmdList[i]);
}
return data;
}
function parseCmd(str) {
// returns a list of commands
if (!str.length) {
return [];
}
let p = false;
switch (str.charAt(0)) {
case 's':
p = parseSubstitute(str);
break;
default:
return false;
}
if (p) {
return [p].concat(parseCmd(p.remainder));
}
return false;
}
// FIXME: Only used once here, confusing with native (and more widely-used) unescape, should probably be replaced
// Then again, unescape is semi-soft-deprecated, so we should look into replacing that too
function unEscape(str, sep) {
return str
.split('\\\\')
.join('\\')
.split('\\' + sep)
.join(sep)
.split('\\n')
.join('\n');
}
function parseSubstitute(str) {
// takes a string like s/a/b/flags;othercmds and parses it
let from, to, flags, tmp;
if (str.length < 4) {
return false;
}
const sep = str.charAt(1);
str = str.substring(2);
tmp = skipOver(str, sep);
if (tmp) {
from = tmp.segment;
str = tmp.remainder;
} else {
return false;
}
tmp = skipOver(str, sep);
if (tmp) {
to = tmp.segment;
str = tmp.remainder;
} else {
return false;
}
flags = '';
if (str.length) {
tmp = skipOver(str, ';') || skipToEnd(str, ';');
if (tmp) {
flags = tmp.segment;
str = tmp.remainder;
}
}
return {
action: substitute,
from: from,
to: to,
flags: flags,
remainder: str,
};
}
function skipOver(str, sep) {
const endSegment = findNext(str, sep);
if (endSegment < 0) {
return false;
}
const segment = unEscape(str.substring(0, endSegment), sep);
return { segment: segment, remainder: str.substring(endSegment + 1) };
}
/*eslint-disable*/
function skipToEnd(str, sep) {
return { segment: str, remainder: '' };
}
/*eslint-enable */
function findNext(str, ch) {
for (let i = 0; i < str.length; ++i) {
if (str.charAt(i) == '\\') {
i += 2;
}
if (str.charAt(i) == ch) {
return i;
}
}
return -1;
}
function setCheckbox(param, box) {
const val = mw.util.getParamValue(param);
if (val) {
switch (val) {
case '1':
case 'yes':
case 'true':
box.checked = true;
break;
case '0':
case 'no':
case 'false':
box.checked = false;
}
}
}
function autoEdit() {
setupPopups(() => {
if (mw.util.getParamValue('autoimpl') !== popupString('autoedit_version')) {
return false;
}
if (
mw.util.getParamValue('autowatchlist') &&
mw.util.getParamValue('actoken') === autoClickToken()
) {
pg.fn.modifyWatchlist(mw.util.getParamValue('title'), mw.util.getParamValue('action'));
}
if (!document.editform) {
return false;
}
if (autoEdit.alreadyRan) {
return false;
}
autoEdit.alreadyRan = true;
const cmdString = mw.util.getParamValue('autoedit');
if (cmdString) {
try {
const editbox = document.editform.wpTextbox1;
const cmdList = parseCmd(cmdString);
const input = editbox.value;
const output = execCmds(input, cmdList);
editbox.value = output;
} catch (dang) {
return;
}
// wikEd user script compatibility
if (typeof wikEdUseWikEd != 'undefined') {
if (wikEdUseWikEd === true) {
WikEdUpdateFrame();
}
}
}
setCheckbox('autominor', document.editform.wpMinoredit);
setCheckbox('autowatch', document.editform.wpWatchthis);
const rvid = mw.util.getParamValue('autorv');
if (rvid) {
const url =
pg.wiki.apiwikibase +
'?action=query&format=json&formatversion=2&prop=revisions&revids=' +
rvid;
startDownload(url, null, autoEdit2);
} else {
autoEdit2();
}
});
}
function autoEdit2(d) {
let summary = mw.util.getParamValue('autosummary');
let summaryprompt = mw.util.getParamValue('autosummaryprompt');
let summarynotice = '';
if (d && d.data && mw.util.getParamValue('autorv')) {
const s = getRvSummary(summary, d.data);
if (s === false) {
summaryprompt = true;
summarynotice = popupString(
'Failed to get revision information, please edit manually.\n\n'
);
summary = simplePrintf(summary, [
mw.util.getParamValue('autorv'),
'(unknown)',
'(unknown)',
]);
} else {
summary = s;
}
}
if (summaryprompt) {
const txt =
summarynotice + popupString('Enter a non-empty edit summary or press cancel to abort');
const response = prompt(txt, summary);
if (response) {
summary = response;
} else {
return;
}
}
if (summary) {
document.editform.wpSummary.value = summary;
}
// Attempt to avoid possible premature clicking of the save button
// (maybe delays in updates to the DOM are to blame?? or a red herring)
setTimeout(autoEdit3, 100);
}
function autoClickToken() {
return mw.user.sessionId();
}
function autoEdit3() {
if (mw.util.getParamValue('actoken') != autoClickToken()) {
return;
}
const btn = mw.util.getParamValue('autoclick');
if (btn) {
if (document.editform && document.editform[btn]) {
const button = document.editform[btn];
const msg = tprintf(
'The %s button has been automatically clicked. Please wait for the next page to load.',
[button.value]
);
bannerMessage(msg);
document.title = '(' + document.title + ')';
button.click();
} else {
alert(
tprintf('Could not find button %s. Please check the settings in your javascript file.', [
btn,
])
);
}
}
}
function bannerMessage(s) {
const headings = document.getElementsByTagName('h1');
if (headings) {
const div = document.createElement('div');
div.innerHTML = '<font size=+1><b>' + pg.escapeQuotesHTML(s) + '</b></font>';
headings[0].parentNode.insertBefore(div, headings[0]);
}
}
function getRvSummary(template, json) {
try {
const o = getJsObj(json);
const edit = anyChild(o.query.pages).revisions[0];
const timestamp = edit.timestamp
.split(/[A-Z]/g)
.join(' ')
.replace(/^ *| *$/g, '');
return simplePrintf(template, [
edit.revid,
timestamp,
edit.userhidden ? '(hidden)' : edit.user,
]);
} catch (badness) {
return false;
}
}
// ENDFILE: autoedit.js
// STARTFILE: downloader.js
/**
* @file
* {@link Downloader}, a xmlhttprequest wrapper, and helper functions.
*/
/**
* Creates a new Downloader
*
* @constructor
* @class The Downloader class. Create a new instance of this class to download stuff.
* @param {string} url The url to download. This can be omitted and supplied later.
*/
function Downloader(url) {
if (typeof XMLHttpRequest != 'undefined') {
this.http = new XMLHttpRequest();
}
/**
* The url to download
*
* @type {string}
*/
this.url = url;
/**
* A universally unique ID number
*
* @type {number}
*/
this.id = null;
/**
* Modification date, to be culled from the incoming headers
*
* @type {Date}
* @private
*/
this.lastModified = null;
/**
* What to do when the download completes successfully
*
* @type {Function}
* @private
*/
this.callbackFunction = null;
/**
* What to do on failure
*
* @type {Function}
* @private
*/
this.onFailure = null;
/**
* Flag set on <code>abort</code>
*
* @type {boolean}
*/
this.aborted = false;
/**
* HTTP method. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html for
* possibilities.
*
* @type {string}
*/
this.method = 'GET';
/**
* Async flag.
*
* @type {boolean}
*/
this.async = true;
}
new Downloader();
/** Submits the http request. */
Downloader.prototype.send = function (x) {
if (!this.http) {
return null;
}
return this.http.send(x);
};
/** Aborts the download, setting the <code>aborted</code> field to true. */
Downloader.prototype.abort = function () {
if (!this.http) {
return null;
}
this.aborted = true;
return this.http.abort();
};
/** Returns the downloaded data. */
Downloader.prototype.getData = function () {
if (!this.http) {
return null;
}
return this.http.responseText;
};
/** Prepares the download. */
Downloader.prototype.setTarget = function () {
if (!this.http) {
return null;
}
this.http.open(this.method, this.url, this.async);
this.http.setRequestHeader('Api-User-Agent', pg.api.userAgent);
};
/** Gets the state of the download. */
Downloader.prototype.getReadyState = function () {
if (!this.http) {
return null;
}
return this.http.readyState;
};
pg.misc.downloadsInProgress = {};
/**
* Starts the download.
* Note that setTarget {@link Downloader#setTarget} must be run first
*/
Downloader.prototype.start = function () {
if (!this.http) {
return;
}
pg.misc.downloadsInProgress[this.id] = this;
this.http.send(null);
};
/**
* Gets the 'Last-Modified' date from the download headers.
* Should be run after the download completes.
* Returns <code>null</code> on failure.
*
* @return {Date}
*/
Downloader.prototype.getLastModifiedDate = function () {
if (!this.http) {
return null;
}
let lastmod = null;
try {
lastmod = this.http.getResponseHeader('Last-Modified');
} catch (err) {}
if (lastmod) {
return new Date(lastmod);
}
return null;
};
/**
* Sets the callback function.
*
* @param {Function} f callback function, called as <code>f(this)</code> on success
*/
Downloader.prototype.setCallback = function (f) {
if (!this.http) {
return;
}
this.http.onreadystatechange = f;
};
Downloader.prototype.getStatus = function () {
if (!this.http) {
return null;
}
return this.http.status;
};
//////////////////////////////////////////////////
// helper functions
/**
* Creates a new {@link Downloader} and prepares it for action.
*
* @param {string} url The url to download
* @param {number} id The ID of the {@link Downloader} object
* @param {Function} callback The callback function invoked on success
* @return {string|Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
*/
function newDownload(url, id, callback, onfailure) {
const d = new Downloader(url);
if (!d.http) {
return 'ohdear';
}
d.id = id;
d.setTarget();
if (!onfailure) {
onfailure = 2;
}
const f = function () {
if (d.getReadyState() == 4) {
delete pg.misc.downloadsInProgress[this.id];
try {
if (d.getStatus() == 200) {
d.data = d.getData();
d.lastModified = d.getLastModifiedDate();
callback(d);
} else if (typeof onfailure == typeof 1) {
if (onfailure > 0) {
// retry
newDownload(url, id, callback, onfailure - 1);
}
} else if (typeof onfailure === 'function') {
onfailure(d, url, id, callback);
}
} catch (somerr) {
/* ignore it */
}
}
};
d.setCallback(f);
return d;
}
/**
* Simulates a download from cached data.
* The supplied data is put into a {@link Downloader} as if it had downloaded it.
*
* @param {string} url The url.
* @param {number} id The ID.
* @param {Function} callback The callback, which is invoked immediately as <code>callback(d)</code>,
* where <code>d</code> is the new {@link Downloader}.
* @param {string} data The (cached) data.
* @param {Date} lastModified The (cached) last modified date.
*/
function fakeDownload(url, id, callback, data, lastModified, owner) {
const d = newDownload(url, callback);
d.owner = owner;
d.id = id;
d.data = data;
d.lastModified = lastModified;
return callback(d);
}
/**
* Starts a download.
*
* @param {string} url The url to download
* @param {number} id The ID of the {@link Downloader} object
* @param {Function} callback The callback function invoked on success
* @return {string|Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
*/
function startDownload(url, id, callback) {
const d = newDownload(url, id, callback);
if (typeof d == typeof '') {
return d;
}
d.start();
return d;
}
/**
* Aborts all downloads which have been started.
*/
function abortAllDownloads() {
for (const x in pg.misc.downloadsInProgress) {
try {
pg.misc.downloadsInProgress[x].aborted = true;
pg.misc.downloadsInProgress[x].abort();
delete pg.misc.downloadsInProgress[x];
} catch (e) {}
}
}
// ENDFILE: downloader.js
// STARTFILE: livepreview.js
// TODO: location is often not correct (eg relative links in previews)
// NOTE: removed md5 and image and math parsing. was broken, lots of bytes.
/**
* InstaView - a Mediawiki to HTML converter in JavaScript
* Version 0.6.1
* Copyright (C) Pedro Fayolle 2005-2006
* https://en.wikipedia.org/wiki/User:Pilaf
* Distributed under the BSD license
*
* Changelog:
*
* 0.6.1
* - Fixed problem caused by \r characters
* - Improved inline formatting parser
*
* 0.6
* - Changed name to InstaView
* - Some major code reorganizations and factored out some common functions
* - Handled conversion of relative links (i.e. [[/foo]])
* - Fixed misrendering of adjacent definition list items
* - Fixed bug in table headings handling
* - Changed date format in signatures to reflect Mediawiki's
* - Fixed handling of [[:Image:...]]
* - Updated MD5 function (hopefully it will work with UTF-8)
* - Fixed bug in handling of links inside images
*
* To do:
* - Better support for math tags
* - Full support for <nowiki>
* - Parser-based (as opposed to RegExp-based) inline wikicode handling (make it one-pass and
* bullet-proof)
* - Support for templates (through AJAX)
* - Support for coloured links (AJAX)
*/
const Insta = {};
function setupLivePreview() {
// options
Insta.conf = {
baseUrl: '',
user: {},
wiki: {
lang: pg.wiki.lang,
interwiki: pg.wiki.interwiki,
default_thumb_width: 180,
},
paths: {
articles: pg.wiki.articlePath + '/',
// Only used for Insta previews with images. (not in popups)
math: '/math/',
images: '//upload.wikimedia.org/wikipedia/en/', // FIXME getImageUrlStart(pg.wiki.hostname),
images_fallback: '//upload.wikimedia.org/wikipedia/commons/',
},
locale: {
user: mw.config.get('wgFormattedNamespaces')[pg.nsUserId],
image: mw.config.get('wgFormattedNamespaces')[pg.nsImageId],
category: mw.config.get('wgFormattedNamespaces')[pg.nsCategoryId],
// shouldn't be used in popup previews, i think
months: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
},
};
// options with default values or backreferences
Insta.conf.user.name = Insta.conf.user.name || 'Wikipedian';
Insta.conf.user.signature =
'[[' +
Insta.conf.locale.user +
':' +
Insta.conf.user.name +
'|' +
Insta.conf.user.name +
']]';
//Insta.conf.paths.images = '//upload.wikimedia.org/wikipedia/' + Insta.conf.wiki.lang + '/';
// define constants
Insta.BLOCK_IMAGE = new RegExp(
'^\\[\\[(?:File|Image|' +
Insta.conf.locale.image +
'):.*?\\|.*?(?:frame|thumbnail|thumb|none|right|left|center)',
'i'
);
}
Insta.dump = function (from, to) {
if (typeof from == 'string') {
from = document.getElementById(from);
}
if (typeof to == 'string') {
to = document.getElementById(to);
}
to.innerHTML = this.convert(from.value);
};
Insta.convert = function (wiki) {
let ll = typeof wiki == 'string' ? wiki.replace(/\r/g, '').split(/\n/) : wiki, // lines of wikicode
o = '', // output
p = 0, // para flag
r; // result of passing a regexp to compareLineStringOrReg()
// some shorthands
function remain() {
return ll.length;
}
function sh() {
return ll.shift();
} // shift
function ps(s) {
o += s;
} // push
// similar to C's printf, uses ? as placeholders, ?? to escape question marks
function f() {
let i = 1,
a = arguments,
f = a[0],
o = '',
c,
p;
for (; i < a.length; i++) {
if ((p = f.indexOf('?')) + 1) {
// allow character escaping
i -= c = f.charAt(p + 1) == '?' ? 1 : 0;
o += f.substring(0, p) + (c ? '?' : a[i]);
f = f.substr(p + 1 + c);
} else {
break;
}
}
return o + f;
}
function html_entities(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
// Wiki text parsing to html is a nightmare.
// The below functions deliberately don't escape the ampersand since this would make it more
// difficult, and we don't absolutely need to for how we need it. This means that any
// unescaped ampersands in wikitext will remain unescaped and can cause invalid HTML.
// Browsers should all be able to handle it though. We also escape significant wikimarkup
// characters to prevent further matching on the processed text.
function htmlescape_text(s) {
return s
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/:/g, ':')
.replace(/\[/g, '[')
.replace(/]/g, ']');
}
function htmlescape_attr(s) {
return htmlescape_text(s).replace(/'/g, ''').replace(/"/g, '"');
}
// return the first non matching character position between two strings
function str_imatch(a, b) {
for (var i = 0, l = Math.min(a.length, b.length); i < l; i++) {
if (a.charAt(i) != b.charAt(i)) {
break;
}
}
return i;
}
// compare current line against a string or regexp
// if passed a string it will compare only the first string.length characters
// if passed a regexp the result is stored in r
function compareLineStringOrReg(c) {
return typeof c == 'string' ?
ll[0] && ll[0].substr(0, c.length) == c :
(r = ll[0] && ll[0].match(c));
}
function compareLineString(c) {
return ll[0] == c;
} // compare current line against a string
function charAtPoint(p) {
return ll[0].charAt(p);
} // return char at pos p
function endl(s) {
ps(s);
sh();
}
function parse_list() {
let prev = '';
while (remain() && compareLineStringOrReg(/^([*#:;]+)(.*)$/)) {
const l_match = r;
sh();
const ipos = str_imatch(prev, l_match[1]);
// close uncontinued lists
for (let prevPos = prev.length - 1; prevPos >= ipos; prevPos--) {
const pi = prev.charAt(prevPos);
if (pi == '*') {
ps('</ul>');
} else if (pi == '#') {
ps('</ol>');
}
// close a dl only if the new item is not a dl item (:, ; or empty)
else if ($.inArray(l_match[1].charAt(prevPos), ['', '*', '#'])) {
ps('</dl>');
}
}
// open new lists
for (let matchPos = ipos; matchPos < l_match[1].length; matchPos++) {
const li = l_match[1].charAt(matchPos);
if (li == '*') {
ps('<ul>');
} else if (li == '#') {
ps('<ol>');
}
// open a new dl only if the prev item is not a dl item (:, ; or empty)
else if ($.inArray(prev.charAt(matchPos), ['', '*', '#'])) {
ps('<dl>');
}
}
switch (l_match[1].charAt(l_match[1].length - 1)) {
case '*':
case '#':
ps('<li>' + parse_inline_nowiki(l_match[2]));
break;
case ';':
ps('<dt>');
var dt_match = l_match[2].match(/(.*?)(:.*?)$/);
// handle ;dt :dd format
if (dt_match) {
ps(parse_inline_nowiki(dt_match[1]));
ll.unshift(dt_match[2]);
} else {
ps(parse_inline_nowiki(l_match[2]));
}
break;
case ':':
ps('<dd>' + parse_inline_nowiki(l_match[2]));
}
prev = l_match[1];
}
// close remaining lists
for (let i = prev.length - 1; i >= 0; i--) {
ps(f('</?>', prev.charAt(i) == '*' ? 'ul' : prev.charAt(i) == '#' ? 'ol' : 'dl'));
}
}
function parse_table() {
endl(f('<table>', compareLineStringOrReg(/^\{\|( .*)$/) ? r[1] : ''));
for (; remain(); ) {
if (compareLineStringOrReg('|')) {
switch (charAtPoint(1)) {
case '}':
endl('</table>');
return;
case '-':
endl(f('<tr>', compareLineStringOrReg(/\|-*(.*)/)[1]));
break;
default:
parse_table_data();
}
} else if (compareLineStringOrReg('!')) {
parse_table_data();
} else {
sh();
}
}
}
function parse_table_data() {
let td_line, match_i;
// 1: "|+", '|' or '+'
// 2: ??
// 3: attributes ??
// TODO: finish commenting this regexp
const td_match = sh().match(/^(\|\+|\||!)((?:([^[|]*?)\|(?!\|))?(.*))$/);
if (td_match[1] == '|+') {
ps('<caption');
} else {
ps('<t' + (td_match[1] == '|' ? 'd' : 'h'));
}
if (typeof td_match[3] != 'undefined') {
//ps(' ' + td_match[3])
match_i = 4;
} else {
match_i = 2;
}
ps('>');
if (td_match[1] != '|+') {
// use || or !! as a cell separator depending on context
// NOTE: when split() is passed a regexp make sure to use non-capturing brackets
td_line = td_match[match_i].split(td_match[1] == '|' ? '||' : /(?:\|\||!!)/);
ps(parse_inline_nowiki(td_line.shift()));
while (td_line.length) {
ll.unshift(td_match[1] + td_line.pop());
}
} else {
ps(parse_inline_nowiki(td_match[match_i]));
}
let tc = 0,
td = [];
while (remain()) {
td.push(sh());
if (compareLineStringOrReg('|')) {
if (!tc) {
break;
}
// we're at the outer-most level (no nested tables), skip to td parse
else if (charAtPoint(1) == '}') {
tc--;
}
} else if (!tc && compareLineStringOrReg('!')) {
break;
} else if (compareLineStringOrReg('{|')) {
tc++;
}
}
if (td.length) {
ps(Insta.convert(td));
}
}
function parse_pre() {
ps('<pre>');
do {
endl(parse_inline_nowiki(ll[0].substring(1)) + '\n');
} while (remain() && compareLineStringOrReg(' '));
ps('</pre>');
}
function parse_block_image() {
ps(parse_image(sh()));
}
function parse_image(str) {
// get what's in between "[[Image:" and "]]"
let tag = str.substring(str.indexOf(':') + 1, str.length - 2);
let width;
let attr = [],
filename,
caption = '';
let thumb = 0,
frame = 0,
center = 0;
let align = '';
if (tag.match(/\|/)) {
// manage nested links
let nesting = 0;
let last_attr;
for (let i = tag.length - 1; i > 0; i--) {
if (tag.charAt(i) == '|' && !nesting) {
last_attr = tag.substr(i + 1);
tag = tag.substring(0, i);
break;
} else {
switch (tag.substr(i - 1, 2)) {
case ']]':
nesting++;
i--;
break;
case '[[':
nesting--;
i--;
}
}
}
attr = tag.split(/\s*\|\s*/);
attr.push(last_attr);
filename = attr.shift();
let w_match;
for (; attr.length; attr.shift()) {
w_match = attr[0].match(/^(\d*)(?:[px]*\d*)?px$/);
if (w_match) {
width = w_match[1];
} else {
switch (attr[0]) {
case 'thumb':
case 'thumbnail':
thumb = true;
frame = true;
break;
case 'frame':
frame = true;
break;
case 'none':
case 'right':
case 'left':
center = false;
align = attr[0];
break;
case 'center':
center = true;
align = 'none';
break;
default:
if (attr.length == 1) {
caption = attr[0];
}
}
}
}
} else {
filename = tag;
}
return '';
}
function parse_inline_nowiki(str) {
let start,
lastend = 0;
let substart = 0,
nestlev = 0,
open,
close,
subloop;
let html = '';
while ((start = str.indexOf('<nowiki>', substart)) != -1) {
html += parse_inline_wiki(str.substring(lastend, start));
start += 8;
substart = start;
subloop = true;
do {
open = str.indexOf('<nowiki>', substart);
close = str.indexOf('</nowiki>', substart);
if (close <= open || open == -1) {
if (close == -1) {
return html + html_entities(str.substr(start));
}
substart = close + 9;
if (nestlev) {
nestlev--;
} else {
lastend = substart;
html += html_entities(str.substring(start, lastend - 9));
subloop = false;
}
} else {
substart = open + 8;
nestlev++;
}
} while (subloop);
}
return html + parse_inline_wiki(str.substr(lastend));
}
function parse_inline_images(str) {
let start,
substart = 0,
nestlev = 0;
let loop, close, open, wiki, html;
while ((start = str.indexOf('[[', substart)) != -1) {
if (
str.substr(start + 2).match(RegExp('^(Image|File|' + Insta.conf.locale.image + '):', 'i'))
) {
loop = true;
substart = start;
do {
substart += 2;
close = str.indexOf(']]', substart);
open = str.indexOf('[[', substart);
if (close <= open || open == -1) {
if (close == -1) {
return str;
}
substart = close;
if (nestlev) {
nestlev--;
} else {
wiki = str.substring(start, close + 2);
html = parse_image(wiki);
str = str.replace(wiki, html);
substart = start + html.length;
loop = false;
}
} else {
substart = open;
nestlev++;
}
} while (loop);
} else {
break;
}
}
return str;
}
// the output of this function doesn't respect the FILO structure of HTML
// but since most browsers can handle it I'll save myself the hassle
function parse_inline_formatting(str) {
let italic,
bold,
i,
li,
o = '';
while ((i = str.indexOf("''", li)) + 1) {
o += str.substring(li, i);
li = i + 2;
if (str.charAt(i + 2) == "'") {
li++;
bold = !bold;
o += bold ? '<b>' : '</b>';
} else {
italic = !italic;
o += italic ? '<i>' : '</i>';
}
}
return o + str.substr(li);
}
function parse_inline_wiki(str) {
str = parse_inline_images(str);
// math
str = str.replace(/<(?:)math>(.*?)<\/math>/gi, '');
// Build a Mediawiki-formatted date string
let date = new Date();
let minutes = date.getUTCMinutes();
if (minutes < 10) {
minutes = '0' + minutes;
}
date = f(
'?:?, ? ? ? (UTC)',
date.getUTCHours(),
minutes,
date.getUTCDate(),
Insta.conf.locale.months[date.getUTCMonth()],
date.getUTCFullYear()
);
// text formatting
str =
str
// signatures
.replace(/~{5}(?!~)/g, date)
.replace(/~{4}(?!~)/g, Insta.conf.user.name + ' ' + date)
.replace(/~{3}(?!~)/g, Insta.conf.user.name)
// [[:Category:...]], [[:Image:...]], etc...
.replace(
RegExp(
'\\[\\[:((?:' +
Insta.conf.locale.category +
'|Image|File|' +
Insta.conf.locale.image +
'|' +
Insta.conf.wiki.interwiki +
'):[^|]*?)\\]\\](\\w*)',
'gi'
),
($0, $1, $2) => f(
"<a href='?'>?</a>",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($1) + htmlescape_text($2)
)
)
// remove straight category and interwiki tags
.replace(
RegExp(
'\\[\\[(?:' +
Insta.conf.locale.category +
'|' +
Insta.conf.wiki.interwiki +
'):.*?\\]\\]',
'gi'
),
''
)
// [[:Category:...|Links]], [[:Image:...|Links]], etc...
.replace(
RegExp(
'\\[\\[:((?:' +
Insta.conf.locale.category +
'|Image|File|' +
Insta.conf.locale.image +
'|' +
Insta.conf.wiki.interwiki +
'):.*?)\\|([^\\]]+?)\\]\\](\\w*)',
'gi'
),
($0, $1, $2, $3) => f(
"<a href='?'>?</a>",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($2) + htmlescape_text($3)
)
)
// [[/Relative links]]
.replace(/\[\[(\/[^|]*?)\]\]/g, ($0, $1) => f(
"<a href='?'>?</a>",
Insta.conf.baseUrl + htmlescape_attr($1),
htmlescape_text($1)
))
// [[/Replaced|Relative links]]
.replace(/\[\[(\/.*?)\|(.+?)\]\]/g, ($0, $1, $2) => f(
"<a href='?'>?</a>",
Insta.conf.baseUrl + htmlescape_attr($1),
htmlescape_text($2)
))
// [[Common links]]
.replace(/\[\[([^[|]*?)\]\](\w*)/g, ($0, $1, $2) => f(
"<a href='?'>?</a>",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($1) + htmlescape_text($2)
))
// [[Replaced|Links]]
.replace(/\[\[([^[]*?)\|([^\]]+?)\]\](\w*)/g, ($0, $1, $2, $3) => f(
"<a href='?'>?</a>",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($2) + htmlescape_text($3)
))
// [[Stripped:Namespace|Namespace]]
.replace(/\[\[([^\]]*?:)?(.*?)( *\(.*?\))?\|\]\]/g, ($0, $1, $2, $3) => f(
"<a href='?'>?</a>",
Insta.conf.paths.articles +
htmlescape_attr($1) +
htmlescape_attr($2) +
htmlescape_attr($3),
htmlescape_text($2)
))
// External links
.replace(
/\[(https?|news|ftp|mailto|gopher|irc):(\/*)([^\]]*?) (.*?)\]/g,
($0, $1, $2, $3, $4) => f(
"<a class='external' href='?:?'>?</a>",
htmlescape_attr($1),
htmlescape_attr($2) + htmlescape_attr($3),
htmlescape_text($4)
)
)
.replace(/\[http:\/\/(.*?)\]/g, ($0, $1) => f("<a class='external' href='http://?'>[#]</a>", htmlescape_attr($1)))
.replace(/\[(news|ftp|mailto|gopher|irc):(\/*)(.*?)\]/g, ($0, $1, $2, $3) => f(
"<a class='external' href='?:?'>?:?</a>",
htmlescape_attr($1),
htmlescape_attr($2) + htmlescape_attr($3),
htmlescape_text($1),
htmlescape_text($2) + htmlescape_text($3)
))
.replace(
/(^| )(https?|news|ftp|mailto|gopher|irc):(\/*)([^ $]*[^.,!?;: $])/g,
($0, $1, $2, $3, $4) => f(
"?<a class='external' href='?:?'>?:?</a>",
htmlescape_text($1),
htmlescape_attr($2),
htmlescape_attr($3) + htmlescape_attr($4),
htmlescape_text($2),
htmlescape_text($3) + htmlescape_text($4)
)
)
.replace('__NOTOC__', '')
.replace('__NOINDEX__', '')
.replace('__INDEX__', '')
.replace('__NOEDITSECTION__', '');
return parse_inline_formatting(str);
}
// begin parsing
for (; remain(); ) {
if (compareLineStringOrReg(/^(={1,6})(.*)\1(.*)$/)) {
p = 0;
endl(f('<h?>?</h?>?', r[1].length, parse_inline_nowiki(r[2]), r[1].length, r[3]));
} else if (compareLineStringOrReg(/^[*#:;]/)) {
p = 0;
parse_list();
} else if (compareLineStringOrReg(' ')) {
p = 0;
parse_pre();
} else if (compareLineStringOrReg('{|')) {
p = 0;
parse_table();
} else if (compareLineStringOrReg(/^----+$/)) {
p = 0;
endl('<hr />');
} else if (compareLineStringOrReg(Insta.BLOCK_IMAGE)) {
p = 0;
parse_block_image();
} else {
// handle paragraphs
if (compareLineString('')) {
p = remain() > 1 && ll[1] === '';
if (p) {
endl('<p><br>');
}
} else {
if (!p) {
ps('<p>');
p = 1;
}
ps(parse_inline_nowiki(ll[0]) + ' ');
}
sh();
}
}
return o;
};
function wiki2html(txt, baseurl) {
Insta.conf.baseUrl = baseurl;
return Insta.convert(txt);
}
// ENDFILE: livepreview.js
// STARTFILE: pageinfo.js
function popupFilterPageSize(data) {
return formatBytes(data.length);
}
function popupFilterCountLinks(data) {
const num = countLinks(data);
return String(num) + ' ' + (num != 1 ? popupString('wikiLinks') : popupString('wikiLink'));
}
function popupFilterCountImages(data) {
const num = countImages(data);
return String(num) + ' ' + (num != 1 ? popupString('images') : popupString('image'));
}
function popupFilterCountCategories(data) {
const num = countCategories(data);
return (
String(num) + ' ' + (num != 1 ? popupString('categories') : popupString('category'))
);
}
function popupFilterLastModified(data, download) {
const lastmod = download.lastModified;
const now = new Date();
const age = now - lastmod;
if (lastmod && getValueOf('popupLastModified')) {
return tprintf('%s old', [formatAge(age)]).replace(/ /g, ' ');
}
return '';
}
function popupFilterWikibaseItem(data, download) {
return download.wikibaseItem ?
tprintf('<a href="%s">%s</a>', [
download.wikibaseRepo.replace(/\$1/g, download.wikibaseItem),
download.wikibaseItem,
]) :
'';
}
function formatAge(age) {
// coerce into a number
let a = 0 + age,
aa = a;
const seclen = 1000;
const minlen = 60 * seclen;
const hourlen = 60 * minlen;
const daylen = 24 * hourlen;
const weeklen = 7 * daylen;
const numweeks = (a - (a % weeklen)) / weeklen;
a = a - numweeks * weeklen;
const sweeks = addunit(numweeks, 'week');
const numdays = (a - (a % daylen)) / daylen;
a = a - numdays * daylen;
const sdays = addunit(numdays, 'day');
const numhours = (a - (a % hourlen)) / hourlen;
a = a - numhours * hourlen;
const shours = addunit(numhours, 'hour');
const nummins = (a - (a % minlen)) / minlen;
a = a - nummins * minlen;
const smins = addunit(nummins, 'minute');
const numsecs = (a - (a % seclen)) / seclen;
a = a - numsecs * seclen;
const ssecs = addunit(numsecs, 'second');
if (aa > 4 * weeklen) {
return sweeks;
}
if (aa > weeklen) {
return sweeks + ' ' + sdays;
}
if (aa > daylen) {
return sdays + ' ' + shours;
}
if (aa > 6 * hourlen) {
return shours;
}
if (aa > hourlen) {
return shours + ' ' + smins;
}
if (aa > 10 * minlen) {
return smins;
}
if (aa > minlen) {
return smins + ' ' + ssecs;
}
return ssecs;
}
function addunit(num, str) {
return String(num) + ' ' + (num != 1 ? popupString(str + 's') : popupString(str));
}
function runPopupFilters(list, data, download) {
const ret = [];
for (let i = 0; i < list.length; ++i) {
if (list[i] && typeof list[i] == 'function') {
const s = list[i](data, download, download.owner.article);
if (s) {
ret.push(s);
}
}
}
return ret;
}
function getPageInfo(data, download) {
if (!data || data.length === 0) {
return popupString('Empty page');
}
const popupFilters = getValueOf('popupFilters') || [];
const extraPopupFilters = getValueOf('extraPopupFilters') || [];
const pageInfoArray = runPopupFilters(popupFilters.concat(extraPopupFilters), data, download);
let pageInfo = pageInfoArray.join(', ');
if (pageInfo !== '') {
pageInfo = upcaseFirst(pageInfo);
}
return pageInfo;
}
// this could be improved!
function countLinks(wikiText) {
return wikiText.split('[[').length - 1;
}
// if N = # matches, n = # brackets, then
// String.parenSplit(regex) intersperses the N+1 split elements
// with Nn other elements. So total length is
// L= N+1 + Nn = N(n+1)+1. So N=(L-1)/(n+1).
function countImages(wikiText) {
return (wikiText.parenSplit(pg.re.image).length - 1) / (pg.re.imageBracketCount + 1);
}
function countCategories(wikiText) {
return (wikiText.parenSplit(pg.re.category).length - 1) / (pg.re.categoryBracketCount + 1);
}
function popupFilterStubDetect(data, download, article) {
const counts = stubCount(data, article);
if (counts.real) {
return popupString('stub');
}
if (counts.sect) {
return popupString('section stub');
}
return '';
}
function popupFilterDisambigDetect(data, download, article) {
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
return '';
}
return isDisambig(data, article) ? popupString('disambig') : '';
}
function formatBytes(num) {
return num > 949 ?
Math.round(num / 100) / 10 + popupString('kB') :
num + ' ' + popupString('bytes');
}
// ENDFILE: pageinfo.js
// STARTFILE: titles.js
/**
* @file Defines the {@link Title} class, and associated crufty functions.
*
* <code>Title</code> deals with article titles and their various
* forms. {@link Stringwrapper} is the parent class of
* <code>Title</code>, which exists simply to make things a little
* neater.
*/
/**
* Creates a new Stringwrapper.
*
* @constructor
*
* @class the Stringwrapper class. This base class is not really
* useful on its own; it just wraps various common string operations.
*/
function Stringwrapper() {
/**
* Wrapper for this.toString().indexOf()
*
* @param {string} x
* @type {number}
*/
this.indexOf = function (x) {
return this.toString().indexOf(x);
};
/**
* Returns this.value.
*
* @type {string}
*/
this.toString = function () {
return this.value;
};
/**
* Wrapper for {@link String#parenSplit} applied to this.toString()
*
* @param {RegExp} x
* @type {Array}
*/
this.parenSplit = function (x) {
return this.toString().parenSplit(x);
};
/**
* Wrapper for this.toString().substring()
*
* @param {string} x
* @param {string} y (optional)
* @type {string}
*/
this.substring = function (x, y) {
if (typeof y == 'undefined') {
return this.toString().substring(x);
}
return this.toString().substring(x, y);
};
/**
* Wrapper for this.toString().split()
*
* @param {string} x
* @type {Array}
*/
this.split = function (x) {
return this.toString().split(x);
};
/**
* Wrapper for this.toString().replace()
*
* @param {string} x
* @param {string} y
* @type {string}
*/
this.replace = function (x, y) {
return this.toString().replace(x, y);
};
}
/**
* Creates a new <code>Title</code>.
*
* @constructor
*
* @class The Title class. Holds article titles and converts them into
* various forms. Also deals with anchors, by which we mean the bits
* of the article URL after a # character, representing locations
* within an article.
*
* @param {string} value The initial value to assign to the
* article. This must be the canonical title (see {@link
* Title#value}. Omit this in the constructor and use another function
* to set the title if this is unavailable.
*/
function Title(val) {
/**
* The canonical article title. This must be in UTF-8 with no
* entities, escaping or nasties. Also, underscores should be
* replaced with spaces.
*
* @type {string}
* @private
*/
this.value = null;
/**
* The canonical form of the anchor. This should be exactly as
* it appears in the URL, i.e. with the .C3.0A bits in.
*
* @type {string}
*/
this.anchor = '';
this.setUtf(val);
}
Title.prototype = new Stringwrapper();
/**
* Returns the canonical representation of the article title, optionally without anchor.
*
* @param {boolean} omitAnchor
* @fixme Decide specs for anchor
* @return String The article title and the anchor.
*/
Title.prototype.toString = function (omitAnchor) {
return this.value + (!omitAnchor && this.anchor ? '#' + this.anchorString() : '');
};
Title.prototype.anchorString = function () {
if (!this.anchor) {
return '';
}
const split = this.anchor.parenSplit(/((?:[.][0-9A-F]{2})+)/);
const len = split.length;
let value;
for (let j = 1; j < len; j += 2) {
// FIXME s/decodeURI/decodeURIComponent/g ?
value = split[j].split('.').join('%');
try {
value = decodeURIComponent(value);
} catch (e) {
// cannot decode
}
split[j] = value.split('_').join(' ');
}
return split.join('');
};
Title.prototype.urlAnchor = function () {
const split = this.anchor.parenSplit('/((?:[%][0-9A-F]{2})+)/');
const len = split.length;
for (let j = 1; j < len; j += 2) {
split[j] = split[j].split('%').join('.');
}
return split.join('');
};
Title.prototype.anchorFromUtf = function (str) {
this.anchor = encodeURIComponent(str.split(' ').join('_'))
.split('%3A')
.join(':')
.split("'")
.join('%27')
.split('%')
.join('.');
};
Title.fromURL = function (h) {
return new Title().fromURL(h);
};
Title.prototype.fromURL = function (h) {
if (typeof h != 'string') {
this.value = null;
return this;
}
// NOTE : playing with decodeURI, encodeURI, escape, unescape,
// we seem to be able to replicate the IE borked encoding
// IE doesn't do this new-fangled utf-8 thing.
// and it's worse than that.
// IE seems to treat the query string differently to the rest of the url
// the query is treated as bona-fide utf8, but the first bit of the url is pissed around with
// we fix up & for all browsers, just in case.
const splitted = h.split('?');
splitted[0] = splitted[0].split('&').join('%26');
h = splitted.join('?');
const contribs = pg.re.contribs.exec(h);
if (contribs) {
if (contribs[1] == 'title=') {
contribs[3] = contribs[3].split('+').join(' ');
}
const u = new Title(contribs[3]);
this.setUtf(
this.decodeNasties(
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + u.stripNamespace()
)
);
return this;
}
const email = pg.re.email.exec(h);
if (email) {
this.setUtf(
this.decodeNasties(
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] +
':' +
new Title(email[3]).stripNamespace()
)
);
return this;
}
const backlinks = pg.re.backlinks.exec(h);
if (backlinks) {
this.setUtf(this.decodeNasties(new Title(backlinks[3])));
return this;
}
//A dummy title object for a Special:Diff link.
const specialdiff = pg.re.specialdiff.exec(h);
if (specialdiff) {
this.setUtf(
this.decodeNasties(
new Title(mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':Diff')
)
);
return this;
}
// no more special cases to check --
// hopefully it's not a disguised user-related or specially treated special page
// Includes references
const m = pg.re.main.exec(h);
if (m === null) {
this.value = null;
} else {
const fromBotInterface = /[?](.+[&])?title=/.test(h);
if (fromBotInterface) {
m[2] = m[2].split('+').join('_');
}
const extracted = m[2] + (m[3] ? '#' + m[3] : '');
if (pg.flag.isSafari && /%25[0-9A-Fa-f]{2}/.test(extracted)) {
// Fix Safari issue
// Safari sometimes encodes % as %25 in UTF-8 encoded strings like %E5%A3 -> %25E5%25A3.
this.setUtf(decodeURIComponent(unescape(extracted)));
} else {
this.setUtf(this.decodeNasties(extracted));
}
}
return this;
};
Title.prototype.decodeNasties = function (txt) {
// myDecodeURI uses decodeExtras, which removes _,
// thus ruining citations previews, which are formated as "cite_note-1"
try {
let ret = decodeURI(this.decodeEscapes(txt));
ret = ret.replace(/[_ ]*$/, '');
return ret;
} catch (e) {
return txt; // cannot decode
}
};
// Decode valid %-encodings, otherwise escape them
Title.prototype.decodeEscapes = function (txt) {
const split = txt.parenSplit(/((?:[%][0-9A-Fa-f]{2})+)/);
const len = split.length;
// No %-encoded items found, so replace the literal %
if (len === 1) {
return split[0].replace(/%(?![0-9a-fA-F][0-9a-fA-F])/g, '%25');
}
for (let i = 1; i < len; i = i + 2) {
split[i] = decodeURIComponent(split[i]);
}
return split.join('');
};
Title.fromAnchor = function (a) {
return new Title().fromAnchor(a);
};
Title.prototype.fromAnchor = function (a) {
if (!a) {
this.value = null;
return this;
}
return this.fromURL(a.href);
};
Title.fromWikiText = function (txt) {
return new Title().fromWikiText(txt);
};
Title.prototype.fromWikiText = function (txt) {
// FIXME - testing needed
txt = myDecodeURI(txt);
this.setUtf(txt);
return this;
};
Title.prototype.hintValue = function () {
if (!this.value) {
return '';
}
return safeDecodeURI(this.value);
};
Title.prototype.toUserName = function (withNs) {
if (this.namespaceId() != pg.nsUserId && this.namespaceId() != pg.nsUsertalkId) {
this.value = null;
return;
}
this.value =
(withNs ? mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' : '') +
this.stripNamespace().split('/')[0];
};
Title.prototype.userName = function (withNs) {
const t = new Title(this.value);
t.toUserName(withNs);
if (t.value) {
return t;
}
return null;
};
Title.prototype.toTalkPage = function () {
// convert article to a talk page, or if we can't, return null
// In other words: return null if this ALREADY IS a talk page
// and return the corresponding talk page otherwise
//
// Per https://www.mediawiki.org/wiki/Manual:Namespace#Subject_and_talk_namespaces
// * All discussion namespaces have odd-integer indices
// * The discussion namespace index for a specific namespace with index n is n + 1
if (this.value === null) {
return null;
}
const namespaceId = this.namespaceId();
if (namespaceId >= 0 && namespaceId % 2 === 0) {
//non-special and subject namespace
const localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId + 1];
if (typeof localizedNamespace !== 'undefined') {
if (localizedNamespace === '') {
this.value = this.stripNamespace();
} else {
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
}
return this.value;
}
}
this.value = null;
return null;
};
// Return canonical, localized namespace
Title.prototype.namespace = function () {
return mw.config.get('wgFormattedNamespaces')[this.namespaceId()];
};
Title.prototype.namespaceId = function () {
const n = this.value.indexOf(':');
if (n < 0) {
return 0;
} //mainspace
const namespaceId =
mw.config.get('wgNamespaceIds')[
this.value.substring(0, n).split(' ').join('_').toLowerCase()
];
if (typeof namespaceId == 'undefined') {
return 0;
} //mainspace
return namespaceId;
};
Title.prototype.talkPage = function () {
const t = new Title(this.value);
t.toTalkPage();
if (t.value) {
return t;
}
return null;
};
Title.prototype.isTalkPage = function () {
if (this.talkPage() === null) {
return true;
}
return false;
};
Title.prototype.toArticleFromTalkPage = function () {
//largely copy/paste from toTalkPage above.
if (this.value === null) {
return null;
}
const namespaceId = this.namespaceId();
if (namespaceId >= 0 && namespaceId % 2 == 1) {
//non-special and talk namespace
const localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId - 1];
if (typeof localizedNamespace !== 'undefined') {
if (localizedNamespace === '') {
this.value = this.stripNamespace();
} else {
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
}
return this.value;
}
}
this.value = null;
return null;
};
Title.prototype.articleFromTalkPage = function () {
const t = new Title(this.value);
t.toArticleFromTalkPage();
if (t.value) {
return t;
}
return null;
};
Title.prototype.articleFromTalkOrArticle = function () {
const t = new Title(this.value);
if (t.toArticleFromTalkPage()) {
return t;
}
return this;
};
Title.prototype.isIpUser = function () {
return pg.re.ipUser.test(this.userName());
};
Title.prototype.stripNamespace = function () {
// returns a string, not a Title
const n = this.value.indexOf(':');
if (n < 0) {
return this.value;
}
const namespaceId = this.namespaceId();
if (namespaceId === pg.nsMainspaceId) {
return this.value;
}
return this.value.substring(n + 1);
};
Title.prototype.setUtf = function (value) {
if (!value) {
this.value = '';
return;
}
const anch = value.indexOf('#');
if (anch < 0) {
this.value = value.split('_').join(' ');
this.anchor = '';
return;
}
this.value = value.substring(0, anch).split('_').join(' ');
this.anchor = value.substring(anch + 1);
this.ns = null; // wait until namespace() is called
};
Title.prototype.setUrl = function (urlfrag) {
const anch = urlfrag.indexOf('#');
this.value = safeDecodeURI(urlfrag.substring(0, anch));
this.anchor = this.value.substring(anch + 1);
};
Title.prototype.append = function (x) {
this.setUtf(this.value + x);
};
Title.prototype.urlString = function (x) {
if (!x) {
x = {};
}
let v = this.toString(true);
if (!x.omitAnchor && this.anchor) {
v += '#' + this.urlAnchor();
}
if (!x.keepSpaces) {
v = v.split(' ').join('_');
}
return encodeURI(v).split('&').join('%26').split('?').join('%3F').split('+').join('%2B');
};
Title.prototype.removeAnchor = function () {
return new Title(this.toString(true));
};
Title.prototype.toUrl = function () {
return pg.wiki.titlebase + this.urlString();
};
function parseParams(url) {
const specialDiff = pg.re.specialdiff.exec(url);
if (specialDiff) {
const split = specialDiff[1].split('/');
if (split.length == 1) {
return { oldid: split[0], diff: 'prev' };
} else if (split.length == 2) {
return { oldid: split[0], diff: split[1] };
}
}
const ret = {};
if (url.indexOf('?') == -1) {
return ret;
}
url = url.split('#')[0];
const s = url.split('?').slice(1).join();
const t = s.split('&');
for (let i = 0; i < t.length; ++i) {
const z = t[i].split('=');
z.push(null);
ret[z[0]] = z[1];
}
//Diff revision with no oldid is interpreted as a diff to the previous revision by MediaWiki
if (ret.diff && typeof ret.oldid === 'undefined') {
ret.oldid = 'prev';
}
//Documentation seems to say something different, but oldid can also accept prev/next, and
//Echo is emitting such URLs. Simple fixup during parameter decoding:
if (ret.oldid && (ret.oldid === 'prev' || ret.oldid === 'next' || ret.oldid === 'cur')) {
const helper = ret.diff;
ret.diff = ret.oldid;
ret.oldid = helper;
}
return ret;
}
// (a) myDecodeURI (first standard decodeURI, then pg.re.urlNoPopup)
// (b) change spaces to underscores
// (c) encodeURI (just the straight one, no pg.re.urlNoPopup)
function myDecodeURI(str) {
let ret;
// FIXME decodeURIComponent??
try {
ret = decodeURI(str.toString());
} catch (summat) {
return str;
}
for (let i = 0; i < pg.misc.decodeExtras.length; ++i) {
const from = pg.misc.decodeExtras[i].from;
const to = pg.misc.decodeExtras[i].to;
ret = ret.split(from).join(to);
}
return ret;
}
function safeDecodeURI(str) {
const ret = myDecodeURI(str);
return ret || str;
}
///////////
// TESTS //
///////////
function isDisambig(data, article) {
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
return false;
}
return !article.isTalkPage() && pg.re.disambig.test(data);
}
function stubCount(data, article) {
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
return false;
}
let sectStub = 0;
let realStub = 0;
if (pg.re.stub.test(data)) {
const s = data.parenSplit(pg.re.stub);
for (let i = 1; i < s.length; i = i + 2) {
if (s[i]) {
++sectStub;
} else {
++realStub;
}
}
}
return { real: realStub, sect: sectStub };
}
function isValidImageName(str) {
// extend as needed...
return str.indexOf('{') == -1;
}
function isInStrippableNamespace(article) {
// Does the namespace allow subpages
// Note, would be better if we had access to wgNamespacesWithSubpages
return article.namespaceId() !== 0;
}
function isInMainNamespace(article) {
return article.namespaceId() === 0;
}
function anchorContainsImage(a) {
// iterate over children of anchor a
// see if any are images
if (a === null) {
return false;
}
const kids = a.childNodes;
for (let i = 0; i < kids.length; ++i) {
if (kids[i].nodeName == 'IMG') {
return true;
}
}
return false;
}
function isPopupLink(a) {
// NB for performance reasons, TOC links generally return true
// they should be stripped out later
if (!markNopopupSpanLinks.done) {
markNopopupSpanLinks();
}
if (a.inNopopupSpan) {
return false;
}
// FIXME is this faster inline?
if (a.onmousedown || a.getAttribute('nopopup')) {
return false;
}
const h = a.href;
if (h === document.location.href + '#') {
return false;
}
if (!pg.re.basenames.test(h)) {
return false;
}
if (!pg.re.urlNoPopup.test(h)) {
return true;
}
return (
(pg.re.email.test(h) ||
pg.re.contribs.test(h) ||
pg.re.backlinks.test(h) ||
pg.re.specialdiff.test(h)) &&
h.indexOf('&limit=') == -1
);
}
function markNopopupSpanLinks() {
if (!getValueOf('popupOnlyArticleLinks')) {
fixVectorMenuPopups();
}
const s = $('.nopopups').toArray();
for (let i = 0; i < s.length; ++i) {
const as = s[i].getElementsByTagName('a');
for (let j = 0; j < as.length; ++j) {
as[j].inNopopupSpan = true;
}
}
markNopopupSpanLinks.done = true;
}
function fixVectorMenuPopups() {
$('nav.vector-menu h3:first a:first').prop('inNopopupSpan', true);
}
// ENDFILE: titles.js
// STARTFILE: getpage.js
//////////////////////////////////////////////////
// Wiki-specific downloading
//
// Schematic for a getWiki call
//
// getPageWithCaching
// |
// false | true
// getPage<-[findPictureInCache]->-onComplete(a fake download)
// \.
// (async)->addPageToCache(download)->-onComplete(download)
// check cache to see if page exists
function getPageWithCaching(url, onComplete, owner) {
log('getPageWithCaching, url=' + url);
const i = findInPageCache(url);
let d;
if (i > -1) {
d = fakeDownload(
url,
owner.idNumber,
onComplete,
pg.cache.pages[i].data,
pg.cache.pages[i].lastModified,
owner
);
} else {
d = getPage(url, onComplete, owner);
if (d && owner && owner.addDownload) {
owner.addDownload(d);
d.owner = owner;
}
}
}
function getPage(url, onComplete, owner) {
log('getPage');
const callback = function (d) {
if (!d.aborted) {
addPageToCache(d);
onComplete(d);
}
};
return startDownload(url, owner.idNumber, callback);
}
function findInPageCache(url) {
for (let i = 0; i < pg.cache.pages.length; ++i) {
if (url == pg.cache.pages[i].url) {
return i;
}
}
return -1;
}
function addPageToCache(download) {
log('addPageToCache ' + download.url);
const page = {
url: download.url,
data: download.data,
lastModified: download.lastModified,
};
return pg.cache.pages.push(page);
}
// ENDFILE: getpage.js
// STARTFILE: parensplit.js
//////////////////////////////////////////////////
// parenSplit
// String.prototype.parenSplit should do what ECMAscript says String.prototype.split does,
// interspersing paren matches (regex capturing groups) between the split elements.
// i.e. 'abc'.split(/(b)/)) should return ['a','b','c'], not ['a','c']
if (String('abc'.split(/(b)/)) != 'a,b,c') {
// broken String.split, e.g. konq, IE < 10
String.prototype.parenSplit = function (re) {
re = nonGlobalRegex(re);
let s = this;
let m = re.exec(s);
let ret = [];
while (m && s) {
// without the following loop, we have
// 'ab'.parenSplit(/a|(b)/) != 'ab'.split(/a|(b)/)
for (let i = 0; i < m.length; ++i) {
if (typeof m[i] == 'undefined') {
m[i] = '';
}
}
ret.push(s.substring(0, m.index));
ret = ret.concat(m.slice(1));
s = s.substring(m.index + m[0].length);
m = re.exec(s);
}
ret.push(s);
return ret;
};
} else {
String.prototype.parenSplit = function (re) {
return this.split(re);
};
String.prototype.parenSplit.isNative = true;
}
function nonGlobalRegex(re) {
const s = re.toString();
let flags = '';
for (var j = s.length; s.charAt(j) != '/'; --j) {
if (s.charAt(j) != 'g') {
flags += s.charAt(j);
}
}
const t = s.substring(1, j);
return RegExp(t, flags);
}
// ENDFILE: parensplit.js
// STARTFILE: tools.js
// IE madness with encoding
// ========================
//
// suppose throughout that the page is in utf8, like wikipedia
//
// if a is an anchor DOM element and a.href should consist of
//
// http://host.name.here/wiki/foo?bar=baz
//
// then IE gives foo as "latin1-encoded" utf8; we have foo = decode_utf8(decodeURI(foo_ie))
// but IE gives bar=baz correctly as plain utf8
//
// ---------------------------------
//
// IE's xmlhttp doesn't understand utf8 urls. Have to use encodeURI here.
//
// ---------------------------------
//
// summat else
// Source: http://aktuell.de.selfhtml.org/artikel/javascript/utf8b64/utf8.htm
function getJsObj(json) {
try {
const json_ret = JSON.parse(json);
if (json_ret.warnings) {
for (let w = 0; w < json_ret.warnings.length; w++) {
if (json_ret.warnings[w]['*']) {
log(json_ret.warnings[w]['*']);
} else {
log(json_ret.warnings[w].warnings);
}
}
} else if (json_ret.error) {
errlog(json_ret.error.code + ': ' + json_ret.error.info);
}
return json_ret;
} catch (someError) {
errlog('Something went wrong with getJsObj, json=' + json);
return 1;
}
}
function anyChild(obj) {
for (const p in obj) {
return obj[p];
}
return null;
}
function upcaseFirst(str) {
if (typeof str != typeof '' || str === '') {
return '';
}
return str.charAt(0).toUpperCase() + str.substring(1);
}
function findInArray(arr, foo) {
if (!arr || !arr.length) {
return -1;
}
const len = arr.length;
for (let i = 0; i < len; ++i) {
if (arr[i] == foo) {
return i;
}
}
return -1;
}
/* eslint-disable no-unused-vars */
function nextOne(array, value) {
// NB if the array has two consecutive entries equal
// then this will loop on successive calls
const i = findInArray(array, value);
if (i < 0) {
return null;
}
return array[i + 1];
}
/* eslint-enable no-unused-vars */
function literalizeRegex(str) {
return mw.util.escapeRegExp(str);
}
String.prototype.entify = function () {
//var shy='­';
return this.split('&')
.join('&')
.split('<')
.join('<')
.split('>')
.join('>' /*+shy*/)
.split('"')
.join('"');
};
// Array filter function
function removeNulls(val) {
return val !== null;
}
function joinPath(list) {
return list.filter(removeNulls).join('/');
}
function simplePrintf(str, subs) {
if (!str || !subs) {
return str;
}
const ret = [];
const s = str.parenSplit(/(%s|\$[0-9]+)/);
let i = 0;
do {
ret.push(s.shift());
if (!s.length) {
break;
}
const cmd = s.shift();
if (cmd == '%s') {
if (i < subs.length) {
ret.push(subs[i]);
} else {
ret.push(cmd);
}
++i;
} else {
const j = parseInt(cmd.replace('$', ''), 10) - 1;
if (j > -1 && j < subs.length) {
ret.push(subs[j]);
} else {
ret.push(cmd);
}
}
} while (s.length > 0);
return ret.join('');
}
/* eslint-disable no-unused-vars */
function isString(x) {
return typeof x === 'string' || x instanceof String;
}
function isNumber(x) {
return typeof x === 'number' || x instanceof Number;
}
function isRegExp(x) {
return x instanceof RegExp;
}
function isArray(x) {
return x instanceof Array;
}
function isObject(x) {
return x instanceof Object;
}
function isFunction(x) {
return !isRegExp(x) && (typeof x === 'function' || x instanceof Function);
}
/* eslint-enable no-unused-vars */
function repeatString(s, mult) {
let ret = '';
for (let i = 0; i < mult; ++i) {
ret += s;
}
return ret;
}
function zeroFill(s, min) {
min = min || 2;
const t = s.toString();
return repeatString('0', min - t.length) + t;
}
function map(f, o) {
if (isArray(o)) {
return map_array(f, o);
}
return map_object(f, o);
}
function map_array(f, o) {
const ret = [];
for (let i = 0; i < o.length; ++i) {
ret.push(f(o[i]));
}
return ret;
}
function map_object(f, o) {
const ret = {};
for (const i in o) {
ret[o] = f(o[i]);
}
return ret;
}
pg.escapeQuotesHTML = function (text) {
return text
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
};
pg.unescapeQuotesHTML = function (html) {
// From https://stackoverflow.com/a/7394787
// This seems to be implemented correctly on all major browsers now, so we
// don't have to make our own function.
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
};
// ENDFILE: tools.js
// STARTFILE: dab.js
//////////////////////////////////////////////////
// Dab-fixing code
//
function retargetDab(newTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) {
log('retargetDab: newTarget=' + newTarget + ' oldTarget=' + oldTarget);
return changeLinkTargetLink({
newTarget: newTarget,
text: newTarget.split(' ').join(' '),
hint: tprintf('disambigHint', [newTarget]),
summary: simplePrintf(getValueOf('popupFixDabsSummary'), [
friendlyCurrentArticleName,
newTarget,
]),
clickButton: getValueOf('popupDabsAutoClick'),
minor: true,
oldTarget: oldTarget,
watch: getValueOf('popupWatchDisambiggedPages'),
title: titleToEdit,
});
}
function listLinks(wikitext, oldTarget, titleToEdit) {
// mediawiki strips trailing spaces, so we do the same
// testcase: https://en.wikipedia.org/w/index.php?title=Radial&oldid=97365633
const reg = /\[\[([^|]*?) *(\||\]\])/gi;
let ret = [];
const splitted = wikitext.parenSplit(reg);
// ^[a-z]+ should match interwiki links, hopefully (case-insensitive)
// and ^[a-z]* should match those and [[:Category...]] style links too
const omitRegex = /^[a-z]*:|^[Ss]pecial:|^[Ii]mage|^[Cc]ategory/;
const friendlyCurrentArticleName = oldTarget.toString();
const wikPos = getValueOf('popupDabWiktionary');
for (let i = 1; i < splitted.length; i = i + 3) {
if (
typeof splitted[i] == typeof 'string' &&
splitted[i].length > 0 &&
!omitRegex.test(splitted[i])
) {
ret.push(retargetDab(splitted[i], oldTarget, friendlyCurrentArticleName, titleToEdit));
} /* if */
} /* for loop */
ret = rmDupesFromSortedList(ret.sort());
if (wikPos) {
const wikTarget =
'wiktionary:' +
friendlyCurrentArticleName.replace(/^(.+)\s+[(][^)]+[)]\s*$/, '$1');
let meth;
if (wikPos.toLowerCase() == 'first') {
meth = 'unshift';
} else {
meth = 'push';
}
ret[meth](retargetDab(wikTarget, oldTarget, friendlyCurrentArticleName, titleToEdit));
}
ret.push(
changeLinkTargetLink({
newTarget: null,
text: popupString('remove this link').split(' ').join(' '),
hint: popupString('remove all links to this disambig page from this article'),
clickButton: getValueOf('popupDabsAutoClick'),
oldTarget: oldTarget,
summary: simplePrintf(getValueOf('popupRmDabLinkSummary'), [friendlyCurrentArticleName]),
watch: getValueOf('popupWatchDisambiggedPages'),
title: titleToEdit,
})
);
return ret;
}
function rmDupesFromSortedList(list) {
const ret = [];
for (let i = 0; i < list.length; ++i) {
if (ret.length === 0 || list[i] != ret[ret.length - 1]) {
ret.push(list[i]);
}
}
return ret;
}
function makeFixDab(data, navpop) {
// grab title from parent popup if there is one; default exists in changeLinkTargetLink
const titleToEdit = navpop.parentPopup && navpop.parentPopup.article.toString();
const list = listLinks(data, navpop.originalArticle, titleToEdit);
if (list.length === 0) {
log('listLinks returned empty list');
return null;
}
let html = '<hr />' + popupString('Click to disambiguate this link to:') + '<br>';
html += list.join(', ');
return html;
}
function makeFixDabs(wikiText, navpop) {
if (
getValueOf('popupFixDabs') &&
isDisambig(wikiText, navpop.article) &&
Title.fromURL(location.href).namespaceId() != pg.nsSpecialId &&
navpop.article.talkPage()
) {
setPopupHTML(makeFixDab(wikiText, navpop), 'popupFixDab', navpop.idNumber);
}
}
function popupRedlinkHTML(article) {
return changeLinkTargetLink({
newTarget: null,
text: popupString('remove this link').split(' ').join(' '),
hint: popupString('remove all links to this page from this article'),
clickButton: getValueOf('popupRedlinkAutoClick'),
oldTarget: article.toString(),
summary: simplePrintf(getValueOf('popupRedlinkSummary'), [article.toString()]),
});
}
// ENDFILE: dab.js
// STARTFILE: htmloutput.js
// this has to use a timer loop as we don't know if the DOM element exists when we want to set the text
function setPopupHTML(str, elementId, popupId, onSuccess, append) {
if (typeof popupId === 'undefined') {
//console.error('popupId is not defined in setPopupHTML, html='+str.substring(0,100));
popupId = pg.idNumber;
}
const popupElement = document.getElementById(elementId + popupId);
if (popupElement) {
if (!append) {
popupElement.innerHTML = '';
}
if (isString(str)) {
popupElement.innerHTML += str;
} else {
popupElement.appendChild(str);
}
if (onSuccess) {
onSuccess();
}
setTimeout(checkPopupPosition, 100);
return true;
} else {
// call this function again in a little while...
setTimeout(() => {
setPopupHTML(str, elementId, popupId, onSuccess);
}, 600);
}
return null;
}
function setPopupTrailer(str, id) {
return setPopupHTML(str, 'popupData', id);
}
// args.navpopup is mandatory
// optional: args.redir, args.redirTarget
// FIXME: ye gods, this is ugly stuff
function fillEmptySpans(args) {
// if redir is present and true then redirTarget is mandatory
let redir = true;
let rcid;
if (typeof args != 'object' || typeof args.redir == 'undefined' || !args.redir) {
redir = false;
}
const a = args.navpopup.parentAnchor;
let article,
hint = null,
oldid = null,
params = {};
if (redir && typeof args.redirTarget == typeof {}) {
article = args.redirTarget;
//hint=article.hintValue();
} else {
article = new Title().fromAnchor(a);
hint = a.originalTitle || article.hintValue();
params = parseParams(a.href);
oldid = getValueOf('popupHistoricalLinks') ? params.oldid : null;
rcid = params.rcid;
}
const x = {
article: article,
hint: hint,
oldid: oldid,
rcid: rcid,
navpop: args.navpopup,
params: params,
};
const structure = pg.structures[getValueOf('popupStructure')];
if (typeof structure != 'object') {
setPopupHTML(
'popupError',
'Unknown structure (this should never happen): ' + pg.option.popupStructure,
args.navpopup.idNumber
);
return;
}
const spans = flatten(pg.misc.layout);
const numspans = spans.length;
const redirs = pg.misc.redirSpans;
for (let i = 0; i < numspans; ++i) {
const found = redirs && redirs.indexOf(spans[i]) !== -1;
//log('redir='+redir+', found='+found+', spans[i]='+spans[i]);
if ((found && !redir) || (!found && redir)) {
//log('skipping this set of the loop');
continue;
}
const structurefn = structure[spans[i]];
if (structurefn === undefined) {
// nothing to do for this structure part
continue;
}
let setfn = setPopupHTML;
if (
getValueOf('popupActiveNavlinks') &&
(spans[i].indexOf('popupTopLinks') === 0 || spans[i].indexOf('popupRedirTopLinks') === 0)
) {
setfn = setPopupTipsAndHTML;
}
switch (typeof structurefn) {
case 'function':
log(
'running ' +
spans[i] +
'({article:' +
x.article +
', hint:' +
x.hint +
', oldid: ' +
x.oldid +
'})'
);
setfn(structurefn(x), spans[i], args.navpopup.idNumber);
break;
case 'string':
setfn(structurefn, spans[i], args.navpopup.idNumber);
break;
default:
errlog('unknown thing with label ' + spans[i] + ' (span index was ' + i + ')');
break;
}
}
}
// flatten an array
function flatten(list, start) {
const ret = [];
if (typeof start == 'undefined') {
start = 0;
}
for (let i = start; i < list.length; ++i) {
if (typeof list[i] == typeof []) {
return ret.concat(flatten(list[i])).concat(flatten(list, i + 1));
} else {
ret.push(list[i]);
}
}
return ret;
}
// Generate html for whole popup
function popupHTML(a) {
getValueOf('popupStructure');
const structure = pg.structures[pg.option.popupStructure];
if (typeof structure != 'object') {
//return 'Unknown structure: '+pg.option.popupStructure;
// override user choice
pg.option.popupStructure = pg.optionDefault.popupStructure;
return popupHTML(a);
}
if (typeof structure.popupLayout != 'function') {
return 'Bad layout';
}
pg.misc.layout = structure.popupLayout();
if (typeof structure.popupRedirSpans === 'function') {
pg.misc.redirSpans = structure.popupRedirSpans();
} else {
pg.misc.redirSpans = [];
}
return makeEmptySpans(pg.misc.layout, a.navpopup);
}
function makeEmptySpans(list, navpop) {
let ret = '';
for (let i = 0; i < list.length; ++i) {
if (typeof list[i] == typeof '') {
ret += emptySpanHTML(list[i], navpop.idNumber, 'div');
} else if (typeof list[i] == typeof [] && list[i].length > 0) {
ret = ret.parenSplit(/(<\/[^>]*?>$)/).join(makeEmptySpans(list[i], navpop));
} else if (typeof list[i] == typeof {} && list[i].nodeType) {
ret += emptySpanHTML(list[i].name, navpop.idNumber, list[i].nodeType);
}
}
return ret;
}
function emptySpanHTML(name, id, tag, classname) {
tag = tag || 'span';
if (!classname) {
classname = emptySpanHTML.classAliases[name];
}
classname = classname || name;
if (name == getValueOf('popupDragHandle')) {
classname += ' popupDragHandle';
}
return simplePrintf('<%s id="%s" class="%s"></%s>', [tag, name + id, classname, tag]);
}
emptySpanHTML.classAliases = { popupSecondPreview: 'popupPreview' };
// generate html for popup image
// <a id="popupImageLinkn"><img id="popupImagen">
// where n=idNumber
function imageHTML(article, idNumber) {
return simplePrintf(
'<a id="popupImageLink$1">' +
'<img align="right" valign="top" id="popupImg$1" style="display: none;"></img>' +
'</a>',
[idNumber]
);
}
function popTipsSoonFn(id, when, popData) {
if (!when) {
when = 250;
}
const popTips = function () {
setupTooltips(document.getElementById(id), false, true, popData);
};
return function () {
setTimeout(popTips, when, popData);
};
}
function setPopupTipsAndHTML(html, divname, idnumber, popData) {
setPopupHTML(
html,
divname,
idnumber,
getValueOf('popupSubpopups') ? popTipsSoonFn(divname + idnumber, null, popData) : null
);
}
// ENDFILE: htmloutput.js
// STARTFILE: mouseout.js
//////////////////////////////////////////////////
// fuzzy checks
function fuzzyCursorOffMenus(x, y, fuzz, parent) {
if (!parent) {
return null;
}
const uls = parent.getElementsByTagName('ul');
for (let i = 0; i < uls.length; ++i) {
if (uls[i].className == 'popup_menu') {
if (uls[i].offsetWidth > 0) {
return false;
}
} // else {document.title+='.';}
}
return true;
}
function checkPopupPosition() {
// stop the popup running off the right of the screen
// FIXME avoid pg.current.link
if (pg.current.link && pg.current.link.navpopup) {
pg.current.link.navpopup.limitHorizontalPosition();
}
}
function mouseOutWikiLink() {
//console ('mouseOutWikiLink');
const a = this;
removeModifierKeyHandler(a);
if (a.navpopup === null || typeof a.navpopup === 'undefined') {
return;
}
if (!a.navpopup.isVisible()) {
a.navpopup.banish();
return;
}
restoreTitle(a);
Navpopup.tracker.addHook(posCheckerHook(a.navpopup));
}
function posCheckerHook(navpop) {
return function () {
if (!navpop.isVisible()) {
return true; /* remove this hook */
}
if (Navpopup.tracker.dirty) {
return false;
}
const x = Navpopup.tracker.x,
y = Navpopup.tracker.y;
const mouseOverNavpop =
navpop.isWithin(x, y, navpop.fuzz, navpop.mainDiv) ||
!fuzzyCursorOffMenus(x, y, navpop.fuzz, navpop.mainDiv);
// FIXME it'd be prettier to do this internal to the Navpopup objects
let t = getValueOf('popupHideDelay');
if (t) {
t = t * 1000;
}
if (!t) {
if (!mouseOverNavpop) {
if (navpop.parentAnchor) {
restoreTitle(navpop.parentAnchor);
}
navpop.banish();
return true; /* remove this hook */
}
return false;
}
// we have a hide delay set
const d = Date.now();
if (!navpop.mouseLeavingTime) {
navpop.mouseLeavingTime = d;
return false;
}
if (mouseOverNavpop) {
navpop.mouseLeavingTime = null;
return false;
}
if (d - navpop.mouseLeavingTime > t) {
navpop.mouseLeavingTime = null;
navpop.banish();
return true; /* remove this hook */
}
return false;
};
}
function runStopPopupTimer(navpop) {
// at this point, we should have left the link but remain within the popup
// so we call this function again until we leave the popup.
if (!navpop.stopPopupTimer) {
navpop.stopPopupTimer = setInterval(posCheckerHook(navpop), 500);
navpop.addHook(
() => {
clearInterval(navpop.stopPopupTimer);
},
'hide',
'before'
);
}
}
// ENDFILE: mouseout.js
// STARTFILE: previewmaker.js
/**
* @file
* Defines the {@link Previewmaker} object, which generates short previews from wiki markup.
*/
/**
* Creates a new Previewmaker
*
* @constructor
* @class The Previewmaker class. Use an instance of this to generate short previews from Wikitext.
* @param {string} wikiText The Wikitext source of the page we wish to preview.
* @param {string} baseUrl The url we should prepend when creating relative urls.
* @param {Navpopup} owner The navpop associated to this preview generator
*/
function Previewmaker(wikiText, baseUrl, owner) {
/** The wikitext which is manipulated to generate the preview. */
this.originalData = wikiText;
this.baseUrl = baseUrl;
this.owner = owner;
this.maxCharacters = getValueOf('popupMaxPreviewCharacters');
this.maxSentences = getValueOf('popupMaxPreviewSentences');
this.setData();
}
Previewmaker.prototype.setData = function () {
const maxSize = Math.max(10000, 2 * this.maxCharacters);
this.data = this.originalData.substring(0, maxSize);
};
/**
* Remove HTML comments
*
* @private
*/
Previewmaker.prototype.killComments = function () {
// this also kills one trailing newline, eg [[diamyo]]
this.data = this.data.replace(
/^<!--[^$]*?-->\n|\n<!--[^$]*?-->(?=\n)|<!--[^$]*?-->/g,
''
);
};
/**
* @private
*/
Previewmaker.prototype.killDivs = function () {
// say goodbye, divs (can be nested, so use * not *?)
this.data = this.data.replace(/< *div[^>]* *>[\s\S]*?< *\/ *div *>/gi, '');
};
/**
* @private
*/
Previewmaker.prototype.killGalleries = function () {
this.data = this.data.replace(/< *gallery[^>]* *>[\s\S]*?< *\/ *gallery *>/gi, '');
};
/**
* @private
*/
Previewmaker.prototype.kill = function (opening, closing, subopening, subclosing, repl) {
let oldk = this.data;
let k = this.killStuff(this.data, opening, closing, subopening, subclosing, repl);
while (k.length < oldk.length) {
oldk = k;
k = this.killStuff(k, opening, closing, subopening, subclosing, repl);
}
this.data = k;
};
/**
* @private
*/
Previewmaker.prototype.killStuff = function (
txt,
opening,
closing,
subopening,
subclosing,
repl
) {
const op = this.makeRegexp(opening);
const cl = this.makeRegexp(closing, '^');
const sb = subopening ? this.makeRegexp(subopening, '^') : null;
const sc = subclosing ? this.makeRegexp(subclosing, '^') : cl;
if (!op || !cl) {
alert('Navigation Popups error: op or cl is null! something is wrong.');
return;
}
if (!op.test(txt)) {
return txt;
}
let ret = '';
const opResult = op.exec(txt);
ret = txt.substring(0, opResult.index);
txt = txt.substring(opResult.index + opResult[0].length);
let depth = 1;
while (txt.length > 0) {
let removal = 0;
if (depth == 1 && cl.test(txt)) {
depth--;
removal = cl.exec(txt)[0].length;
} else if (depth > 1 && sc.test(txt)) {
depth--;
removal = sc.exec(txt)[0].length;
} else if (sb && sb.test(txt)) {
depth++;
removal = sb.exec(txt)[0].length;
}
if (!removal) {
removal = 1;
}
txt = txt.substring(removal);
if (depth === 0) {
break;
}
}
return ret + (repl || '') + txt;
};
/**
* @private
*/
Previewmaker.prototype.makeRegexp = function (x, prefix, suffix) {
prefix = prefix || '';
suffix = suffix || '';
let reStr = '';
let flags = '';
if (isString(x)) {
reStr = prefix + literalizeRegex(x) + suffix;
} else if (isRegExp(x)) {
let s = x.toString().substring(1);
const sp = s.split('/');
flags = sp[sp.length - 1];
sp[sp.length - 1] = '';
s = sp.join('/');
s = s.substring(0, s.length - 1);
reStr = prefix + s + suffix;
} else {
log('makeRegexp failed');
}
log('makeRegexp: got reStr=' + reStr + ', flags=' + flags);
return RegExp(reStr, flags);
};
/**
* @private
*/
Previewmaker.prototype.killBoxTemplates = function () {
// taxobox removal... in fact, there's a saudiprincebox_begin, so let's be more general
// also, have float_begin, ... float_end
this.kill(/[{][{][^{}\s|]*?(float|box)[_ ](begin|start)/i, /[}][}]\s*/, '{{');
// infoboxes etc
// from [[User:Zyxw/popups.js]]: kill frames too
this.kill(/[{][{][^{}\s|]*?(infobox|elementbox|frame)[_ ]/i, /[}][}]\s*/, '{{');
};
/**
* @private
*/
Previewmaker.prototype.killTemplates = function () {
this.kill('{{', '}}', '{', '}', ' ');
};
/**
* @private
*/
Previewmaker.prototype.killTables = function () {
// tables are bad, too
// this can be slow, but it's an inprovement over a browser hang
// torture test: [[Comparison_of_Intel_Central_Processing_Units]]
this.kill('{|', /[|]}\s*/, '{|');
this.kill(/<table.*?>/i, /<\/table.*?>/i, /<table.*?>/i);
// remove lines starting with a pipe for the hell of it (?)
this.data = this.data.replace(/^[|].*$/mg, '');
};
/**
* @private
*/
Previewmaker.prototype.killImages = function () {
const forbiddenNamespaceAliases = [];
$.each(mw.config.get('wgNamespaceIds'), (_localizedNamespaceLc, _namespaceId) => {
if (_namespaceId != pg.nsImageId && _namespaceId != pg.nsCategoryId) {
return;
}
forbiddenNamespaceAliases.push(_localizedNamespaceLc.split(' ').join('[ _]')); //todo: escape regexp fragments!
});
// images and categories are a nono
this.kill(
RegExp('[[][[]\\s*(' + forbiddenNamespaceAliases.join('|') + ')\\s*:', 'i'),
/\]\]\s*/,
'[',
']'
);
};
/**
* @private
*/
Previewmaker.prototype.killHTML = function () {
// kill <ref ...>...</ref>
this.kill(/<ref\b[^/>]*?>/i, /<\/ref>/i);
// let's also delete entire lines starting with <. it's worth a try.
this.data = this.data.replace(/(^|\n) *<.*/g, '\n');
// and those pesky html tags, but not <nowiki> or <blockquote>
const splitted = this.data.parenSplit(/(<[\w\W]*?(?:>|$|(?=<)))/);
const len = splitted.length;
for (let i = 1; i < len; i = i + 2) {
switch (splitted[i]) {
case '<nowiki>':
case '</nowiki>':
case '<blockquote>':
case '</blockquote>':
break;
default:
splitted[i] = '';
}
}
this.data = splitted.join('');
};
/**
* @private
*/
Previewmaker.prototype.killChunks = function () {
// heuristics alert
// chunks of italic text? you crazy, man?
const italicChunkRegex = /((^|\n)\s*:*\s*''[^']([^']|'''|'[^']){20}(.|\n[^\n])*''[.!?\s]*\n)+/g;
// keep stuff separated, though, so stick in \n (fixes [[Union Jack]]?
this.data = this.data.replace(italicChunkRegex, '\n');
};
/**
* @private
*/
Previewmaker.prototype.mopup = function () {
// we simply *can't* be doing with horizontal rules right now
this.data = this.data.replace(/^-{4,}/mg, '');
// no indented lines
this.data = this.data.replace(/(^|\n) *:[^\n]*/g, '');
// replace __TOC__, __NOTOC__ and whatever else there is
// this'll probably do
this.data = this.data.replace(/^__[A-Z_]*__ *$/gmi, '');
};
/**
* @private
*/
Previewmaker.prototype.firstBit = function () {
// dont't be givin' me no subsequent paragraphs, you hear me?
/// first we "normalize" section headings, removing whitespace after, adding before
let d = this.data;
if (getValueOf('popupPreviewCutHeadings')) {
this.data = this.data.replace(/\s*(==+[^=]*==+)\s*/g, '\n\n$1 ');
/// then we want to get rid of paragraph breaks whose text ends badly
this.data = this.data.replace(/([:;]) *\n{2,}/g, '$1\n');
this.data = this.data.replace(/^[\s\n]*/, '');
const stuff = /^([^\n]|\n[^\n\s])*/.exec(this.data);
if (stuff) {
d = stuff[0];
}
if (!getValueOf('popupPreviewFirstParOnly')) {
d = this.data;
}
/// now put \n\n after sections so that bullets and numbered lists work
d = d.replace(/(==+[^=]*==+)\s*/g, '$1\n\n');
}
// Split sentences. Superfluous sentences are RIGHT OUT.
// note: exactly 1 set of parens here needed to make the slice work
d = d.parenSplit(/([!?.]+["']*\s)/g);
// leading space is bad, mmkay?
d[0] = d[0].replace(/^\s*/, '');
const notSentenceEnds = /([^.][a-z][.] *[a-z]|etc|sic|Dr|Mr|Mrs|Ms|St|no|op|cit|\[[^\]]*|\s[A-Zvclm])$/i;
d = this.fixSentenceEnds(d, notSentenceEnds);
this.fullLength = d.join('').length;
let n = this.maxSentences;
let dd = this.firstSentences(d, n);
do {
dd = this.firstSentences(d, n);
--n;
} while (dd.length > this.maxCharacters && n !== 0);
this.data = dd;
};
/**
* @private
*/
Previewmaker.prototype.fixSentenceEnds = function (strs, reg) {
// take an array of strings, strs
// join strs[i] to strs[i+1] & strs[i+2] if strs[i] matches regex reg
for (let i = 0; i < strs.length - 2; ++i) {
if (reg.test(strs[i])) {
const a = [];
for (let j = 0; j < strs.length; ++j) {
if (j < i) {
a[j] = strs[j];
}
if (j == i) {
a[i] = strs[i] + strs[i + 1] + strs[i + 2];
}
if (j > i + 2) {
a[j - 2] = strs[j];
}
}
return this.fixSentenceEnds(a, reg);
}
}
return strs;
};
/**
* @private
*/
Previewmaker.prototype.firstSentences = function (strs, howmany) {
const t = strs.slice(0, 2 * howmany);
return t.join('');
};
/**
* @private
*/
Previewmaker.prototype.killBadWhitespace = function () {
// also cleans up isolated '''', eg [[Suntory Sungoliath]]
this.data = this.data.replace(/^ *'+ *$/gm, '');
};
/**
* Runs the various methods to generate the preview.
* The preview is stored in the <code>html</html> field.
*
* @private
*/
Previewmaker.prototype.makePreview = function () {
if (
this.owner.article.namespaceId() != pg.nsTemplateId &&
this.owner.article.namespaceId() != pg.nsImageId
) {
this.killComments();
this.killDivs();
this.killGalleries();
this.killBoxTemplates();
if (getValueOf('popupPreviewKillTemplates')) {
this.killTemplates();
} else {
this.killMultilineTemplates();
}
this.killTables();
this.killImages();
this.killHTML();
this.killChunks();
this.mopup();
this.firstBit();
this.killBadWhitespace();
} else {
this.killHTML();
}
this.html = wiki2html(this.data, this.baseUrl); // needs livepreview
this.fixHTML();
this.stripLongTemplates();
};
/**
* @private
*/
Previewmaker.prototype.esWiki2HtmlPart = function (data) {
const reLinks = /(?:\[\[([^|\]]*)(?:\|([^|\]]*))*]]([a-z]*))/gi; //match a wikilink
reLinks.lastIndex = 0; //reset regex
let match;
let result = '';
let postfixIndex = 0;
while ((match = reLinks.exec(data))) {
//match all wikilinks
//FIXME: the way that link is built here isn't perfect. It is clickable, but popups preview won't recognize it in some cases.
result +=
pg.escapeQuotesHTML(data.substring(postfixIndex, match.index)) +
'<a href="' +
Insta.conf.paths.articles +
pg.escapeQuotesHTML(match[1]) +
'">' +
pg.escapeQuotesHTML((match[2] ? match[2] : match[1]) + match[3]) +
'</a>';
postfixIndex = reLinks.lastIndex;
}
//append the rest
result += pg.escapeQuotesHTML(data.substring(postfixIndex));
return result;
};
Previewmaker.prototype.editSummaryPreview = function () {
const reAes = /\/\* *(.*?) *\*\//g; //match the first section marker
reAes.lastIndex = 0; //reset regex
let match;
match = reAes.exec(this.data);
if (match) {
//we have a section link. Split it, process it, combine it.
const prefix = this.data.substring(0, match.index - 1);
const section = match[1];
const postfix = this.data.substring(reAes.lastIndex);
let start = "<span class='autocomment'>";
let end = '</span>';
if (prefix.length > 0) {
start = this.esWiki2HtmlPart(prefix) + ' ' + start + '- ';
}
if (postfix.length > 0) {
end = ': ' + end + this.esWiki2HtmlPart(postfix);
}
const t = new Title().fromURL(this.baseUrl);
t.anchorFromUtf(section);
const sectionLink =
Insta.conf.paths.articles +
pg.escapeQuotesHTML(t.toString(true)) +
'#' +
pg.escapeQuotesHTML(t.anchor);
return (
start + '<a href="' + sectionLink + '">→</a> ' + pg.escapeQuotesHTML(section) + end
);
}
//else there's no section link, htmlify the whole thing.
return this.esWiki2HtmlPart(this.data);
};
/** Test function for debugging preview problems one step at a time. */
/*eslint-disable */
function previewSteps(txt) {
try {
txt = txt || document.editform.wpTextbox1.value;
} catch (err) {
if (pg.cache.pages.length > 0) {
txt = pg.cache.pages[pg.cache.pages.length - 1].data;
} else {
alert('provide text or use an edit page');
}
}
txt = txt.substring(0, 10000);
var base = pg.wiki.articlebase + Title.fromURL(document.location.href).urlString();
var p = new Previewmaker(txt, base, pg.current.link.navpopup);
if (this.owner.article.namespaceId() != pg.nsTemplateId) {
p.killComments();
if (!confirm('done killComments(). Continue?\n---\n' + p.data)) {
return;
}
p.killDivs();
if (!confirm('done killDivs(). Continue?\n---\n' + p.data)) {
return;
}
p.killGalleries();
if (!confirm('done killGalleries(). Continue?\n---\n' + p.data)) {
return;
}
p.killBoxTemplates();
if (!confirm('done killBoxTemplates(). Continue?\n---\n' + p.data)) {
return;
}
if (getValueOf('popupPreviewKillTemplates')) {
p.killTemplates();
if (!confirm('done killTemplates(). Continue?\n---\n' + p.data)) {
return;
}
} else {
p.killMultilineTemplates();
if (!confirm('done killMultilineTemplates(). Continue?\n---\n' + p.data)) {
return;
}
}
p.killTables();
if (!confirm('done killTables(). Continue?\n---\n' + p.data)) {
return;
}
p.killImages();
if (!confirm('done killImages(). Continue?\n---\n' + p.data)) {
return;
}
p.killHTML();
if (!confirm('done killHTML(). Continue?\n---\n' + p.data)) {
return;
}
p.killChunks();
if (!confirm('done killChunks(). Continue?\n---\n' + p.data)) {
return;
}
p.mopup();
if (!confirm('done mopup(). Continue?\n---\n' + p.data)) {
return;
}
p.firstBit();
if (!confirm('done firstBit(). Continue?\n---\n' + p.data)) {
return;
}
p.killBadWhitespace();
if (!confirm('done killBadWhitespace(). Continue?\n---\n' + p.data)) {
return;
}
}
p.html = wiki2html(p.data, base); // needs livepreview
p.fixHTML();
if (!confirm('done fixHTML(). Continue?\n---\n' + p.html)) {
return;
}
p.stripLongTemplates();
if (!confirm('done stripLongTemplates(). Continue?\n---\n' + p.html)) {
return;
}
alert('finished preview - end result follows.\n---\n' + p.html);
}
/*eslint-enable */
/**
* Works around livepreview bugs.
*
* @private
*/
Previewmaker.prototype.fixHTML = function () {
if (!this.html) {
return;
}
let ret = this.html;
// fix question marks in wiki links
// maybe this'll break some stuff :-(
ret = ret.replace(
RegExp('(<a href="' + pg.wiki.articlePath + '/[^"]*)[?](.*?")', 'g'),
'$1%3F$2'
);
ret = ret.replace(
RegExp("(<a href='" + pg.wiki.articlePath + "/[^']*)[?](.*?')", 'g'),
'$1%3F$2'
);
// FIXME fix up % too
this.html = ret;
};
/**
* Generates the preview and displays it in the current popup.
*
* Does nothing if the generated preview is invalid or consists of whitespace only.
* Also activates wikilinks in the preview for subpopups if the popupSubpopups option is true.
*/
Previewmaker.prototype.showPreview = function () {
this.makePreview();
if (typeof this.html != typeof '') {
return;
}
if (/^\s*$/.test(this.html)) {
return;
}
setPopupHTML('<hr />', 'popupPrePreviewSep', this.owner.idNumber);
setPopupTipsAndHTML(this.html, 'popupPreview', this.owner.idNumber, {
owner: this.owner,
});
const more = this.fullLength > this.data.length ? this.moreLink() : '';
setPopupHTML(more, 'popupPreviewMore', this.owner.idNumber);
};
/**
* @private
*/
Previewmaker.prototype.moreLink = function () {
const a = document.createElement('a');
a.className = 'popupMoreLink';
a.innerHTML = popupString('more...');
const savedThis = this;
a.onclick = function () {
savedThis.maxCharacters += 2000;
savedThis.maxSentences += 20;
savedThis.setData();
savedThis.showPreview();
};
return a;
};
/**
* @private
*/
Previewmaker.prototype.stripLongTemplates = function () {
// operates on the HTML!
this.html = this.html.replace(
/^.{0,1000}[{][{][^}]*?(<(p|br)( \/)?>\s*){2,}([^{}]*?[}][}])?/gi,
''
);
this.html = this.html.split('\n').join(' '); // workaround for <pre> templates
this.html = this.html.replace(/[{][{][^}]*<pre>[^}]*[}][}]/gi, '');
};
/**
* @private
*/
Previewmaker.prototype.killMultilineTemplates = function () {
this.kill('{{{', '}}}');
this.kill(/\s*[{][{][^{}]*\n/, '}}', '{{');
};
// ENDFILE: previewmaker.js
// STARTFILE: querypreview.js
function loadAPIPreview(queryType, article, navpop) {
const art = new Title(article).urlString();
let url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';
let htmlGenerator = function (/*a, d*/) {
alert('invalid html generator');
};
let usernameart = '';
switch (queryType) {
case 'history':
url +=
'titles=' + art + '&prop=revisions&rvlimit=' + getValueOf('popupHistoryPreviewLimit');
htmlGenerator = APIhistoryPreviewHTML;
break;
case 'category':
url += 'list=categorymembers&cmtitle=' + art;
htmlGenerator = APIcategoryPreviewHTML;
break;
case 'userinfo':
var username = new Title(article).userName();
usernameart = encodeURIComponent(username);
if (pg.re.ipUser.test(username)) {
url += 'list=blocks&bkprop=range|restrictions&bkip=' + usernameart;
} else {
url +=
'list=users|usercontribs&usprop=blockinfo|groups|editcount|registration|gender&ususers=' +
usernameart +
'&meta=globaluserinfo&guiprop=groups|unattached&guiuser=' +
usernameart +
'&uclimit=1&ucprop=timestamp&ucuser=' +
usernameart;
}
htmlGenerator = APIuserInfoPreviewHTML;
break;
case 'contribs':
usernameart = encodeURIComponent(new Title(article).userName());
url +=
'list=usercontribs&ucuser=' +
usernameart +
'&uclimit=' +
getValueOf('popupContribsPreviewLimit');
htmlGenerator = APIcontribsPreviewHTML;
break;
case 'imagepagepreview':
var trail = '';
if (getValueOf('popupImageLinks')) {
trail = '&list=imageusage&iutitle=' + art;
}
url += 'titles=' + art + '&prop=revisions|imageinfo&rvslots=main&rvprop=content' + trail;
htmlGenerator = APIimagepagePreviewHTML;
break;
case 'backlinks':
url += 'list=backlinks&bltitle=' + art;
htmlGenerator = APIbacklinksPreviewHTML;
break;
case 'revision':
if (article.oldid) {
url += 'revids=' + article.oldid;
} else {
url += 'titles=' + article.removeAnchor().urlString();
}
url +=
'&prop=revisions|pageprops|info|images|categories&meta=wikibase&rvslots=main&rvprop=ids|timestamp|flags|comment|user|content&cllimit=max&imlimit=max';
htmlGenerator = APIrevisionPreviewHTML;
break;
}
pendingNavpopTask(navpop);
const callback = function (d) {
log('callback of API functions was hit');
if (queryType === 'userinfo') {
// We need to do another API request
fetchUserGroupNames(d.data).then(() => {
showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
});
return;
}
showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
};
const go = function () {
getPageWithCaching(url, callback, navpop);
return true;
};
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
go();
} else {
navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_' + queryType + '_QUERY_DATA');
}
}
function linkList(list) {
list.sort((x, y) => x == y ? 0 : x < y ? -1 : 1);
const buf = [];
for (let i = 0; i < list.length; ++i) {
buf.push(
wikiLink({
article: new Title(list[i]),
text: list[i].split(' ').join(' '),
action: 'view',
})
);
}
return buf.join(', ');
}
function getTimeOffset() {
const tz = mw.user.options.get('timecorrection');
if (tz) {
if (tz.indexOf('|') > -1) {
// New format
return parseInt(tz.split('|')[1], 10);
}
}
return 0;
}
function getTimeZone() {
if (!pg.user.timeZone) {
const tz = mw.user.options.get('timecorrection');
pg.user.timeZone = 'UTC';
if (tz) {
const tzComponents = tz.split('|');
if (tzComponents.length === 3 && tzComponents[0] === 'ZoneInfo') {
pg.user.timeZone = tzComponents[2];
} else {
errlog('Unexpected timezone information: ' + tz);
}
}
}
return pg.user.timeZone;
}
/**
* Should we use an offset or can we use proper timezones
*/
function useTimeOffset() {
if (typeof Intl.DateTimeFormat.prototype.formatToParts === 'undefined') {
// IE 11
return true;
}
const tz = mw.user.options.get('timecorrection');
if (tz && tz.indexOf('ZoneInfo|') === -1) {
// System| Default system time, default for users who didn't configure timezone
// Offset| Manual defined offset by user
return true;
}
return false;
}
/**
* Array of locales for the purpose of javascript locale based formatting
* Filters down to those supported by the browser. Empty [] === System's default locale
*/
function getLocales() {
if (!pg.user.locales) {
let userLanguage = document.querySelector('html').getAttribute('lang'); // make sure we have HTML locale
if (getValueOf('popupLocale')) {
userLanguage = getValueOf('popupLocale');
} else if (userLanguage === 'en') {
// en.wp tends to treat this as international english / unspecified
// but we have more specific settings in user options
if (getMWDateFormat() === 'mdy') {
userLanguage = 'en-US';
} else {
userLanguage = 'en-GB';
}
}
pg.user.locales = Intl.DateTimeFormat.supportedLocalesOf([userLanguage, navigator.language]);
}
return pg.user.locales;
}
/**
* Retrieve configured MW date format for this user
* These can be
* default
* dmy: time, dmy
* mdy: time, mdy
* ymd: time, ymd
* dmyt: dmy, time
* dmyts: dmy, time + seconds
* ISO 8601: YYYY-MM-DDThh:mm:ss (local time)
*
* This isn't too useful for us, as JS doesn't have formatters to match these private specifiers
*/
function getMWDateFormat() {
return mw.user.options.get('date');
}
/**
* Creates a HTML table that's shown in the history and user-contribs popups.
*
* @param {Object[]} h - a list of revisions, returned from the API
* @param {boolean} reallyContribs - true only if we're displaying user contributions
*/
function editPreviewTable(article, h, reallyContribs) {
let html = ['<table>'];
let day = null;
let curart = article;
let page = null;
let makeFirstColumnLinks;
if (reallyContribs) {
// We're showing user contributions, so make (diff | hist) links
makeFirstColumnLinks = function (currentRevision) {
let result = '(';
result +=
'<a href="' +
pg.wiki.titlebase +
new Title(currentRevision.title).urlString() +
'&diff=prev' +
'&oldid=' +
currentRevision.revid +
'">' +
popupString('diff') +
'</a>';
result += ' | ';
result +=
'<a href="' +
pg.wiki.titlebase +
new Title(currentRevision.title).urlString() +
'&action=history">' +
popupString('hist') +
'</a>';
result += ')';
return result;
};
} else {
// It's a regular history page, so make (cur | last) links
const firstRevid = h[0].revid;
makeFirstColumnLinks = function (currentRevision) {
let result = '(';
result +=
'<a href="' +
pg.wiki.titlebase +
new Title(curart).urlString() +
'&diff=' +
firstRevid +
'&oldid=' +
currentRevision.revid +
'">' +
popupString('cur') +
'</a>';
result += ' | ';
result +=
'<a href="' +
pg.wiki.titlebase +
new Title(curart).urlString() +
'&diff=prev&oldid=' +
currentRevision.revid +
'">' +
popupString('last') +
'</a>';
result += ')';
return result;
};
}
for (let i = 0; i < h.length; ++i) {
if (reallyContribs) {
page = h[i].title;
curart = new Title(page);
}
const minor = h[i].minor ? '<b>m </b>' : '';
const editDate = new Date(h[i].timestamp);
let thisDay = formattedDate(editDate);
const thisTime = formattedTime(editDate);
if (thisDay == day) {
thisDay = '';
} else {
day = thisDay;
}
if (thisDay) {
html.push(
'<tr><td colspan=3><span class="popup_history_date">' + thisDay + '</span></td></tr>'
);
}
html.push('<tr class="popup_history_row_' + (i % 2 ? 'odd' : 'even') + '">');
html.push('<td>' + makeFirstColumnLinks(h[i]) + '</td>');
html.push(
'<td>' +
'<a href="' +
pg.wiki.titlebase +
new Title(curart).urlString() +
'&oldid=' +
h[i].revid +
'">' +
thisTime +
'</a></td>'
);
let col3url = '',
col3txt = '';
if (!reallyContribs) {
const user = h[i].user;
if (!h[i].userhidden) {
if (pg.re.ipUser.test(user)) {
col3url =
pg.wiki.titlebase +
mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +
':Contributions&target=' +
new Title(user).urlString();
} else {
col3url =
pg.wiki.titlebase +
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] +
':' +
new Title(user).urlString();
}
col3txt = pg.escapeQuotesHTML(user);
} else {
col3url = getValueOf('popupRevDelUrl');
col3txt = pg.escapeQuotesHTML(popupString('revdel'));
}
} else {
col3url = pg.wiki.titlebase + curart.urlString();
col3txt = pg.escapeQuotesHTML(page);
}
html.push(
'<td>' +
(reallyContribs ? minor : '') +
'<a href="' +
col3url +
'">' +
col3txt +
'</a></td>'
);
let comment = '';
const c = h[i].comment || ( typeof h[i].slots !== 'undefined' ? h[i].slots.main.content : null );
if (c) {
comment = new Previewmaker(c, new Title(curart).toUrl()).editSummaryPreview();
} else if (h[i].commenthidden) {
comment = popupString('revdel');
}
html.push('<td>' + (!reallyContribs ? minor : '') + comment + '</td>');
html.push('</tr>');
html = [html.join('')];
}
html.push('</table>');
return html.join('');
}
function adjustDate(d, offset) {
// offset is in minutes
const o = offset * 60 * 1000;
return new Date(Number(d) + o);
}
/**
* This relies on the Date parser understanding en-US dates,
* which is pretty safe assumption, but not perfect.
*/
function convertTimeZone(date, timeZone) {
return new Date(date.toLocaleString('en-US', { timeZone: timeZone }));
}
function formattedDateTime(date) {
// fallback for IE11 and unknown timezones
if (useTimeOffset()) {
return formattedDate(date) + ' ' + formattedTime(date);
}
if (getMWDateFormat() === 'ISO 8601') {
const d2 = convertTimeZone(date, getTimeZone());
return (
map(zeroFill, [d2.getFullYear(), d2.getMonth() + 1, d2.getDate()]).join('-') +
'T' +
map(zeroFill, [d2.getHours(), d2.getMinutes(), d2.getSeconds()]).join(':')
);
}
const options = getValueOf('popupDateTimeFormatterOptions');
options.timeZone = getTimeZone();
return date.toLocaleString(getLocales(), options);
}
function formattedDate(date) {
// fallback for IE11 and unknown timezones
if (useTimeOffset()) {
// we adjust the UTC time, so we print the adjusted UTC, but not really UTC values
var d2 = adjustDate(date, getTimeOffset());
return map(zeroFill, [d2.getUTCFullYear(), d2.getUTCMonth() + 1, d2.getUTCDate()]).join('-');
}
if (getMWDateFormat() === 'ISO 8601') {
var d2 = convertTimeZone(date, getTimeZone());
return map(zeroFill, [d2.getFullYear(), d2.getMonth() + 1, d2.getDate()]).join('-');
}
const options = getValueOf('popupDateFormatterOptions');
options.timeZone = getTimeZone();
return date.toLocaleDateString(getLocales(), options);
}
function formattedTime(date) {
// fallback for IE11 and unknown timezones
if (useTimeOffset()) {
// we adjust the UTC time, so we print the adjusted UTC, but not really UTC values
var d2 = adjustDate(date, getTimeOffset());
return map(zeroFill, [d2.getUTCHours(), d2.getUTCMinutes(), d2.getUTCSeconds()]).join(':');
}
if (getMWDateFormat() === 'ISO 8601') {
var d2 = convertTimeZone(date, getTimeZone());
return map(zeroFill, [d2.getHours(), d2.getMinutes(), d2.getSeconds()]).join(':');
}
const options = getValueOf('popupTimeFormatterOptions');
options.timeZone = getTimeZone();
return date.toLocaleTimeString(getLocales(), options);
}
// Get the proper groupnames for the technicalgroups
function fetchUserGroupNames(userinfoResponse) {
const queryObj = getJsObj(userinfoResponse).query;
const user = anyChild(queryObj.users);
const messages = [];
if (user.groups) {
user.groups.forEach((groupName) => {
if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'oathauth-twofactorauth'].indexOf(groupName) === -1) {
messages.push('group-' + groupName + '-member');
}
});
}
if (queryObj.globaluserinfo && queryObj.globaluserinfo.groups) {
queryObj.globaluserinfo.groups.forEach((groupName) => {
messages.push('group-' + groupName + '-member');
});
}
return getMwApi().loadMessagesIfMissing(messages);
}
function showAPIPreview(queryType, html, id, navpop, download) {
// DJ: done
let target = 'popupPreview';
completedNavpopTask(navpop);
switch (queryType) {
case 'imagelinks':
case 'category':
target = 'popupPostPreview';
break;
case 'userinfo':
target = 'popupUserData';
break;
case 'revision':
insertPreview(download);
return;
}
setPopupTipsAndHTML(html, target, id);
}
function APIrevisionPreviewHTML(article, download) {
try {
const jsObj = getJsObj(download.data);
const page = anyChild(jsObj.query.pages);
if (page.missing) {
// TODO we need to fix this proper later on
download.owner = null;
return;
}
const content =
page && page.revisions && page.revisions[0] &&
page.revisions[0].slots && page.revisions[0].slots.main &&
page.revisions[0].slots.main.contentmodel === 'wikitext' ?
page.revisions[0].slots.main.content :
null;
if (typeof content === 'string') {
download.data = content;
download.lastModified = new Date(page.revisions[0].timestamp);
}
if (page.pageprops.wikibase_item) {
download.wikibaseItem = page.pageprops.wikibase_item;
download.wikibaseRepo = jsObj.query.wikibase.repo.url.base +
jsObj.query.wikibase.repo.url.articlepath;
}
} catch (someError) {
return 'Revision preview failed :(';
}
}
function APIbacklinksPreviewHTML(article, download /*, navpop*/) {
try {
const jsObj = getJsObj(download.data);
const list = jsObj.query.backlinks;
let html = [];
if (!list) {
return popupString('No backlinks found');
}
for (let i = 0; i < list.length; i++) {
const t = new Title(list[i].title);
html.push(
'<a href="' + pg.wiki.titlebase + t.urlString() + '">' + t.toString().entify() + '</a>'
);
}
html = html.join(', ');
if (jsObj.continue && jsObj.continue.blcontinue) {
html += popupString(' and more');
}
return html;
} catch (someError) {
return 'backlinksPreviewHTML went wonky';
}
}
pg.fn.APIsharedImagePagePreviewHTML = function APIsharedImagePagePreviewHTML(obj) {
log('APIsharedImagePagePreviewHTML');
const popupid = obj.requestid;
if (obj.query && obj.query.pages) {
const page = anyChild(obj.query.pages);
const content =
page && page.revisions && page.revisions[0] &&
page.revisions[0].slots && page.revisions[0].slots.main &&
page.revisions[0].slots.main.contentmodel === 'wikitext' ?
page.revisions[0].slots.main.content :
null;
if (
typeof content === 'string' &&
pg &&
pg.current &&
pg.current.link &&
pg.current.link.navpopup
) {
/* Not entirely safe, but the best we can do */
const p = new Previewmaker(
content,
pg.current.link.navpopup.article,
pg.current.link.navpopup
);
p.makePreview();
setPopupHTML(p.html, 'popupSecondPreview', popupid);
}
}
};
function APIimagepagePreviewHTML(article, download, navpop) {
try {
const jsObj = getJsObj(download.data);
const page = anyChild(jsObj.query.pages);
const content =
page && page.revisions && page.revisions[0] &&
page.revisions[0].slots && page.revisions[0].slots.main &&
page.revisions[0].slots.main.contentmodel === 'wikitext' ?
page.revisions[0].slots.main.content :
null;
let ret = '';
let alt = '';
try {
alt = navpop.parentAnchor.childNodes[0].alt;
} catch (e) {}
if (alt) {
ret = ret + '<hr /><b>' + popupString('Alt text:') + '</b> ' + pg.escapeQuotesHTML(alt);
}
if (typeof content === 'string') {
const p = prepPreviewmaker(content, article, navpop);
p.makePreview();
if (p.html) {
ret += '<hr />' + p.html;
}
if (getValueOf('popupSummaryData')) {
const info = getPageInfo(content, download);
log(info);
setPopupTrailer(info, navpop.idNumber);
}
}
if (page && page.imagerepository == 'shared') {
const art = new Title(article);
const encart = encodeURIComponent('File:' + art.stripNamespace());
const shared_url =
pg.wiki.apicommonsbase +
'?format=json&formatversion=2' +
'&callback=pg.fn.APIsharedImagePagePreviewHTML' +
'&requestid=' +
navpop.idNumber +
'&action=query&prop=revisions&rvslots=main&rvprop=content&titles=' +
encart;
ret =
ret +
'<hr />' +
popupString('Image from Commons') +
': <a href="' +
pg.wiki.commonsbase +
'?title=' +
encart +
'">' +
popupString('Description page') +
'</a>';
mw.loader.load(shared_url);
}
showAPIPreview(
'imagelinks',
APIimagelinksPreviewHTML(article, download),
navpop.idNumber,
download
);
return ret;
} catch (someError) {
return 'API imagepage preview failed :(';
}
}
function APIimagelinksPreviewHTML(article, download) {
try {
const jsobj = getJsObj(download.data);
const list = jsobj.query.imageusage;
if (list) {
const ret = [];
for (let i = 0; i < list.length; i++) {
ret.push(list[i].title);
}
if (ret.length === 0) {
return popupString('No image links found');
}
return '<h2>' + popupString('File links') + '</h2>' + linkList(ret);
} else {
return popupString('No image links found');
}
} catch (someError) {
return 'Image links preview generation failed :(';
}
}
function APIcategoryPreviewHTML(article, download) {
try {
const jsobj = getJsObj(download.data);
const list = jsobj.query.categorymembers;
let ret = [];
for (let p = 0; p < list.length; p++) {
ret.push(list[p].title);
}
if (ret.length === 0) {
return popupString('Empty category');
}
ret = '<h2>' + tprintf('Category members (%s shown)', [ret.length]) + '</h2>' + linkList(ret);
if (jsobj.continue && jsobj.continue.cmcontinue) {
ret += popupString(' and more');
}
return ret;
} catch (someError) {
return 'Category preview failed :(';
}
}
function APIuserInfoPreviewHTML(article, download) {
let ret = [];
let queryobj = {};
try {
queryobj = getJsObj(download.data).query;
} catch (someError) {
return 'Userinfo preview failed :(';
}
const user = anyChild(queryobj.users);
if (user) {
const globaluserinfo = queryobj.globaluserinfo;
if (user.invalid === '') {
ret.push(popupString('Invalid user'));
} else if (user.missing === '') {
ret.push(popupString('Not a registered username'));
}
if (user.blockedby) {
if (user.blockpartial) {
ret.push('<b>' + popupString('Has blocks') + '</b>');
} else {
ret.push('<b>' + popupString('BLOCKED') + '</b>');
}
}
if (globaluserinfo && ('locked' in globaluserinfo || 'hidden' in globaluserinfo)) {
let lockedSulAccountIsAttachedToThis = true;
for (let i = 0; globaluserinfo.unattached && i < globaluserinfo.unattached.length; i++) {
if (globaluserinfo.unattached[i].wiki === mw.config.get('wgDBname')) {
lockedSulAccountIsAttachedToThis = false;
break;
}
}
if (lockedSulAccountIsAttachedToThis) {
if ('locked' in globaluserinfo) {
ret.push('<b><i>' + popupString('LOCKED') + '</i></b>');
}
if ('hidden' in globaluserinfo) {
ret.push('<b><i>' + popupString('HIDDEN') + '</i></b>');
}
}
}
if (getValueOf('popupShowGender') && user.gender) {
switch (user.gender) {
case 'male':
ret.push(popupString('he/him') + ' · ');
break;
case 'female':
ret.push(popupString('she/her') + ' · ');
break;
}
}
if (user.groups) {
user.groups.forEach((groupName) => {
if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'oathauth-twofactorauth'].indexOf(groupName) === -1) {
ret.push(
pg.escapeQuotesHTML(mw.message('group-' + groupName + '-member', user.gender).text())
);
}
});
}
if (globaluserinfo && globaluserinfo.groups) {
globaluserinfo.groups.forEach((groupName) => {
ret.push(
'<i>' +
pg.escapeQuotesHTML(
mw.message('group-' + groupName + '-member', user.gender).text()
) +
'</i>'
);
});
}
if (user.registration) {
ret.push(
pg.escapeQuotesHTML(
(user.editcount ? user.editcount : '0') +
popupString(' edits since: ') +
(user.registration ? formattedDate(new Date(user.registration)) : '')
)
);
}
}
if (queryobj.usercontribs && queryobj.usercontribs.length) {
ret.push(
popupString('last edit on ') + formattedDate(new Date(queryobj.usercontribs[0].timestamp))
);
}
if (queryobj.blocks) {
ret.push(popupString('IP user')); //we only request list=blocks for IPs
for (let l = 0; l < queryobj.blocks.length; l++) {
let rbstr =
queryobj.blocks[l].rangestart === queryobj.blocks[l].rangeend ? 'BLOCK' : 'RANGEBLOCK';
rbstr = !Array.isArray(queryobj.blocks[l].restrictions) ?
'Has ' + rbstr.toLowerCase() + 's' :
rbstr + 'ED';
ret.push('<b>' + popupString(rbstr) + '</b>');
}
}
// if any element of ret ends with ' · ', merge it with the next element to avoid
// the .join(', ') call inserting a comma after it
for (let m = 0; m < ret.length - 1; m++) {
if (ret[m].length > 3 && ret[m].substring(ret[m].length - 3) === ' · ') {
ret[m] = ret[m] + ret[m + 1];
ret.splice(m + 1, 1); // delete element at index m+1
m--;
}
}
ret = '<hr />' + ret.join(', ');
return ret;
}
function APIcontribsPreviewHTML(article, download, navpop) {
return APIhistoryPreviewHTML(article, download, navpop, true);
}
function APIhistoryPreviewHTML(article, download, navpop, reallyContribs) {
try {
const jsobj = getJsObj(download.data);
let edits = [];
if (reallyContribs) {
edits = jsobj.query.usercontribs;
} else {
edits = anyChild(jsobj.query.pages).revisions;
}
const ret = editPreviewTable(article, edits, reallyContribs);
return ret;
} catch (someError) {
return popupString('History preview failed');
}
}
// ENDFILE: querypreview.js
// STARTFILE: debug.js
////////////////////////////////////////////////////////////////////
// Debugging functions
////////////////////////////////////////////////////////////////////
function setupDebugging() {
if (window.popupDebug) {
// popupDebug is set from .version
window.log = function (x) {
//if(gMsg!='')gMsg += '\n'; gMsg+=time() + ' ' + x; };
window.console.log(x);
};
window.errlog = function (x) {
window.console.error(x);
};
log('Initializing logger');
} else {
window.log = function () {};
window.errlog = function () {};
}
}
// ENDFILE: debug.js
// STARTFILE: images.js
// load image of type Title.
function loadImage(image, navpop) {
if (typeof image.stripNamespace != 'function') {
alert('loadImages bad');
}
// API call to retrieve image info.
if (!getValueOf('popupImages')) {
return;
}
if (!isValidImageName(image)) {
return false;
}
const art = image.urlString();
let url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query';
url += '&prop=imageinfo&iiprop=url|mime&iiurlwidth=' + getValueOf('popupImageSizeLarge');
url += '&titles=' + art;
pendingNavpopTask(navpop);
const callback = function (d) {
popupsInsertImage(navpop.idNumber, navpop, d);
};
const go = function () {
getPageWithCaching(url, callback, navpop);
return true;
};
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
go();
} else {
navpop.addHook(go, 'unhide', 'after', 'DOWNLOAD_IMAGE_QUERY_DATA');
}
}
function popupsInsertImage(id, navpop, download) {
log('popupsInsertImage');
let imageinfo;
try {
const jsObj = getJsObj(download.data);
const imagepage = anyChild(jsObj.query.pages);
if (typeof imagepage.imageinfo === 'undefined') {
return;
}
imageinfo = imagepage.imageinfo[0];
} catch (someError) {
log('popupsInsertImage failed :(');
return;
}
const popupImage = document.getElementById('popupImg' + id);
if (!popupImage) {
log('could not find insertion point for image');
return;
}
popupImage.width = getValueOf('popupImageSize');
popupImage.style.display = 'inline';
// Set the source for the image.
if (imageinfo.thumburl) {
popupImage.src = imageinfo.thumburl;
} else if (imageinfo.mime.indexOf('image') === 0) {
popupImage.src = imageinfo.url;
log('a thumb could not be found, using original image');
} else {
log("fullsize imagethumb, but not sure if it's an image");
}
const a = document.getElementById('popupImageLink' + id);
if (a === null) {
return null;
}
// Determine the action of the surrouding imagelink.
switch (getValueOf('popupThumbAction')) {
case 'imagepage':
if (pg.current.article.namespaceId() != pg.nsImageId) {
a.href = imageinfo.descriptionurl;
// FIXME: unreliable pg.idNumber
popTipsSoonFn('popupImage' + id)();
break;
}
/* falls through */
case 'sizetoggle':
a.onclick = toggleSize;
a.title = popupString('Toggle image size');
return;
case 'linkfull':
a.href = imageinfo.url;
a.title = popupString('Open full-size image');
return;
}
}
// Toggles the image between inline small and navpop fullwidth.
// It's the same image, no actual sizechange occurs, only display width.
function toggleSize() {
const imgContainer = this;
if (!imgContainer) {
alert('imgContainer is null :/');
return;
}
const img = imgContainer.firstChild;
if (!img) {
alert('img is null :/');
return;
}
if (!img.style.width || img.style.width === '') {
img.style.width = '100%';
} else {
img.style.width = '';
}
}
// Returns one title of an image from wikiText.
function getValidImageFromWikiText(wikiText) {
// nb in pg.re.image we're interested in the second bracketed expression
// this may change if the regex changes :-(
//var match=pg.re.image.exec(wikiText);
let matched = null;
let match;
// strip html comments, used by evil bots :-(
const t = removeMatchesUnless(
wikiText,
/(<!--[\s\S]*?-->)/,
1,
/^<!--[^[]*popup/i
);
while ((match = pg.re.image.exec(t))) {
// now find a sane image name - exclude templates by seeking {
const m = match[2] || match[6];
if (isValidImageName(m)) {
matched = m;
break;
}
}
pg.re.image.lastIndex = 0;
if (!matched) {
return null;
}
return mw.config.get('wgFormattedNamespaces')[pg.nsImageId] + ':' + upcaseFirst(matched);
}
function removeMatchesUnless(str, re1, parencount, re2) {
const split = str.parenSplit(re1);
const c = parencount + 1;
for (let i = 0; i < split.length; ++i) {
if (i % c === 0 || re2.test(split[i])) {
continue;
}
split[i] = '';
}
return split.join('');
}
// ENDFILE: images.js
// STARTFILE: namespaces.js
// Set up namespaces and other non-strings.js localization
// (currently that means redirs too)
function setNamespaces() {
pg.nsSpecialId = -1;
pg.nsMainspaceId = 0;
pg.nsImageId = 6;
pg.nsUserId = 2;
pg.nsUsertalkId = 3;
pg.nsCategoryId = 14;
pg.nsTemplateId = 10;
}
function setRedirs() {
const r = 'redirect';
const R = 'REDIRECT';
const redirLists = {
ar: [R, 'تحويل'],
be: [r, 'перанакіраваньне'],
bg: [r, 'пренасочване', 'виж'],
bs: [r, 'Preusmjeri', 'preusmjeri', 'PREUSMJERI'],
bn: [R, 'পুনর্নির্দেশ'],
cs: [R, 'PŘESMĚRUJ'],
cy: [r, 'ail-cyfeirio'],
de: [R, 'WEITERLEITUNG'],
el: [R, 'ΑΝΑΚΑΤΕΥΘΥΝΣΗ'],
eo: [R, 'ALIDIREKTU', 'ALIDIREKTI'],
es: [R, 'REDIRECCIÓN'],
et: [r, 'suuna'],
ga: [r, 'athsheoladh'],
gl: [r, 'REDIRECCIÓN', 'REDIRECIONAMENTO'],
he: [R, 'הפניה'],
hu: [R, 'ÁTIRÁNYÍTÁS'],
is: [r, 'tilvísun', 'TILVÍSUN'],
it: [R, 'RINVIA', 'Rinvia'],
ja: [R, '転送'],
mk: [r, 'пренасочување', 'види'],
nds: [r, 'wiederleiden'],
'nds-nl': [R, 'DEURVERWIEZING', 'DUURVERWIEZING'],
nl: [R, 'DOORVERWIJZING'],
nn: [r, 'omdiriger'],
pl: [R, 'PATRZ', 'PRZEKIERUJ', 'TAM'],
pt: [R, 'redir'],
ru: [R, 'ПЕРЕНАПРАВЛЕНИЕ', 'ПЕРЕНАПР'],
sk: [r, 'presmeruj'],
sr: [r, 'Преусмери', 'преусмери', 'ПРЕУСМЕРИ', 'Preusmeri', 'preusmeri', 'PREUSMERI'],
tr: [R, 'YÖNLENDİRME', 'yönlendirme', 'YÖNLENDİR', 'yönlendir'],
tt: [R, 'yünältü', 'перенаправление', 'перенапр'],
uk: [R, 'ПЕРЕНАПРАВЛЕННЯ', 'ПЕРЕНАПР'],
vi: [r, 'đổi'],
yi: [R, 'ווייטערפירן'],
zh: [R, '重定向'], // no comma
};
const redirList = redirLists[pg.wiki.lang] || [r, R];
// Mediawiki is very tolerant about what comes after the #redirect at the start
pg.re.redirect = RegExp(
'^\\s*[#](' + redirList.join('|') + ').*?\\[{2}([^\\|\\]]*)(|[^\\]]*)?\\]{2}\\s*(.*)',
'i'
);
}
function setInterwiki() {
if (pg.wiki.wikimedia) {
// From https://meta.wikimedia.org/wiki/List_of_Wikipedias
//en.wikipedia.org/w/api.php?action=sitematrix&format=json&smtype=language&smlangprop=code&formatversion=2
pg.wiki.interwiki =
'aa|ab|ace|af|ak|als|am|an|ang|ar|arc|arz|as|ast|av|ay|az|ba|bar|bat-smg|bcl|be|be-x-old|bg|bh|bi|bjn|bm|bn|bo|bpy|br|bs|bug|bxr|ca|cbk-zam|cdo|ce|ceb|ch|cho|chr|chy|ckb|co|cr|crh|cs|csb|cu|cv|cy|da|de|diq|dsb|dv|dz|ee|el|eml|en|eo|es|et|eu|ext|fa|ff|fi|fiu-vro|fj|fo|fr|frp|frr|fur|fy|ga|gag|gan|gd|gl|glk|gn|got|gu|gv|ha|hak|haw|he|hi|hif|ho|hr|hsb|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|ilo|io|is|it|iu|ja|jbo|jv|ka|kaa|kab|kbd|kg|ki|kj|kk|kl|km|kn|ko|koi|kr|krc|ks|ksh|ku|kv|kw|ky|la|lad|lb|lbe|lg|li|lij|lmo|ln|lo|lt|ltg|lv|map-bms|mdf|mg|mh|mhr|mi|mk|ml|mn|mo|mr|mrj|ms|mt|mus|mwl|my|myv|mzn|na|nah|nap|nds|nds-nl|ne|new|ng|nl|nn|no|nov|nrm|nv|ny|oc|om|or|os|pa|pag|pam|pap|pcd|pdc|pfl|pi|pih|pl|pms|pnb|pnt|ps|pt|qu|rm|rmy|rn|ro|roa-rup|roa-tara|ru|rue|rw|sa|sah|sc|scn|sco|sd|se|sg|sh|si|simple|sk|sl|sm|sn|so|sq|sr|srn|ss|st|stq|su|sv|sw|szl|ta|te|tet|tg|th|ti|tk|tl|tn|to|tpi|tr|ts|tt|tum|tw|ty|udm|ug|uk|ur|uz|ve|vec|vi|vls|vo|wa|war|wo|wuu|xal|xh|yi|yo|za|zea|zh|zh-classical|zh-min-nan|zh-yue|zu';
pg.re.interwiki = RegExp('^' + pg.wiki.interwiki + ':');
} else {
pg.wiki.interwiki = null;
pg.re.interwiki = /^$/;
}
}
// return a regexp pattern matching all variants to write the given namespace
function nsRe(namespaceId) {
const imageNamespaceVariants = [];
$.each(mw.config.get('wgNamespaceIds'), (_localizedNamespaceLc, _namespaceId) => {
if (_namespaceId != namespaceId) {
return;
}
_localizedNamespaceLc = upcaseFirst(_localizedNamespaceLc);
imageNamespaceVariants.push(
mw.util.escapeRegExp(_localizedNamespaceLc).split(' ').join('[ _]')
);
imageNamespaceVariants.push(mw.util.escapeRegExp(encodeURI(_localizedNamespaceLc)));
});
return '(?:' + imageNamespaceVariants.join('|') + ')';
}
function nsReImage() {
return nsRe(pg.nsImageId);
}
// ENDFILE: namespaces.js
// STARTFILE: selpop.js
function getEditboxSelection() {
// see http://www.webgurusforum.com/8/12/0
let editbox;
try {
editbox = document.editform.wpTextbox1;
} catch (dang) {
return;
}
// IE, Opera
if (document.selection) {
return document.selection.createRange().text;
}
// Mozilla
const selStart = editbox.selectionStart;
const selEnd = editbox.selectionEnd;
return editbox.value.substring(selStart, selEnd);
}
function doSelectionPopup() {
// popup if the selection looks like [[foo|anything afterwards at all
// or [[foo|bar]]text without ']]'
// or [[foo|bar]]
const sel = getEditboxSelection();
const open = sel.indexOf('[[');
const pipe = sel.indexOf('|');
const close = sel.indexOf(']]');
if (open == -1 || (pipe == -1 && close == -1)) {
return;
}
if ((pipe != -1 && open > pipe) || (close != -1 && open > close)) {
return;
}
const article = new Title(sel.substring(open + 2, pipe < 0 ? close : pipe));
if (getValueOf('popupOnEditSelection') == 'boxpreview') {
return doSeparateSelectionPopup(sel, article);
}
if (close > 0 && sel.substring(close + 2).indexOf('[[') >= 0) {
return;
}
const a = document.createElement('a');
a.href = pg.wiki.titlebase + article.urlString();
mouseOverWikiLink2(a);
if (a.navpopup) {
a.navpopup.addHook(
() => {
runStopPopupTimer(a.navpopup);
},
'unhide',
'after'
);
}
}
function doSeparateSelectionPopup(str, article) {
let div = document.getElementById('selectionPreview');
if (!div) {
div = document.createElement('div');
div.id = 'selectionPreview';
try {
const box = document.editform.wpTextbox1;
box.parentNode.insertBefore(div, box);
} catch (error) {
return;
}
}
const p = prepPreviewmaker(str, article, newNavpopup(document.createElement('a'), article));
p.makePreview();
if (p.html) {
div.innerHTML = p.html;
}
div.ranSetupTooltipsAlready = false;
popTipsSoonFn('selectionPreview')();
}
// ENDFILE: selpop.js
// STARTFILE: navpopup.js
/**
* @file Defines two classes: {@link Navpopup} and {@link Mousetracker}.
*
* <code>Navpopup</code> describes popups: when they appear, where, what
* they look like and so on.
*
* <code>Mousetracker</code> "captures" the mouse using
* <code>document.onmousemove</code>.
*/
/**
* Creates a new Mousetracker.
*
* @constructor
* @class The Mousetracker class. This monitors mouse movements and manages associated hooks.
*/
function Mousetracker() {
/**
* Interval to regularly run the hooks anyway, in milliseconds.
*
* @type {number}
*/
this.loopDelay = 400;
/**
* Timer for the loop.
*
* @type {Timer}
*/
this.timer = null;
/**
* Flag - are we switched on?
*
* @type {boolean}
*/
this.active = false;
/**
* Flag - are we probably inaccurate, i.e. not reflecting the actual mouse position?
*/
this.dirty = true;
/**
* Array of hook functions.
*
* @private
* @type {Array}
*/
this.hooks = [];
}
/**
* Adds a hook, to be called when we get events.
*
* @param {Function} f A function which is called as
* <code>f(x,y)</code>. It should return <code>true</code> when it
* wants to be removed, and <code>false</code> otherwise.
*/
Mousetracker.prototype.addHook = function (f) {
this.hooks.push(f);
};
/**
* Runs hooks, passing them the x
* and y coords of the mouse. Hook functions that return true are
* passed to {@link Mousetracker#removeHooks} for removal.
*
* @private
*/
Mousetracker.prototype.runHooks = function () {
if (!this.hooks || !this.hooks.length) {
return;
}
//log('Mousetracker.runHooks; we got some hooks to run');
let remove = false;
const removeObj = {};
// this method gets called a LOT -
// pre-cache some variables
const x = this.x,
y = this.y,
len = this.hooks.length;
for (let i = 0; i < len; ++i) {
//~ run the hook function, and remove it if it returns true
if (this.hooks[i](x, y) === true) {
remove = true;
removeObj[i] = true;
}
}
if (remove) {
this.removeHooks(removeObj);
}
};
/**
* Removes hooks.
*
* @private
* @param {Object} removeObj An object whose keys are the index
* numbers of functions for removal, with values that evaluate to true
*/
Mousetracker.prototype.removeHooks = function (removeObj) {
const newHooks = [];
const len = this.hooks.length;
for (let i = 0; i < len; ++i) {
if (!removeObj[i]) {
newHooks.push(this.hooks[i]);
}
}
this.hooks = newHooks;
};
/**
* Event handler for mouse wiggles.
* We simply grab the event, set x and y and run the hooks.
* This makes the cpu all hot and bothered :-(
*
* @private
* @param {Event} e Mousemove event
*/
Mousetracker.prototype.track = function (e) {
//~ Apparently this is needed in IE.
e = e || window.event;
let x, y;
if (e) {
if (e.pageX) {
x = e.pageX;
y = e.pageY;
} else if (typeof e.clientX != 'undefined') {
let left,
top,
docElt = document.documentElement;
if (docElt) {
left = docElt.scrollLeft;
}
left = left || document.body.scrollLeft || document.scrollLeft || 0;
if (docElt) {
top = docElt.scrollTop;
}
top = top || document.body.scrollTop || document.scrollTop || 0;
x = e.clientX + left;
y = e.clientY + top;
} else {
return;
}
this.setPosition(x, y);
}
};
/**
* Sets the x and y coordinates stored and takes appropriate action,
* running hooks as appropriate.
*
* @param {number} x Screen coordinates to set
* @param {number} y Screen coordinates to set
*/
Mousetracker.prototype.setPosition = function (x, y) {
this.x = x;
this.y = y;
if (this.dirty || this.hooks.length === 0) {
this.dirty = false;
return;
}
if (typeof this.lastHook_x != 'number') {
this.lastHook_x = -100;
this.lastHook_y = -100;
}
let diff = (this.lastHook_x - x) * (this.lastHook_y - y);
diff = diff >= 0 ? diff : -diff;
if (diff > 1) {
this.lastHook_x = x;
this.lastHook_y = y;
if (this.dirty) {
this.dirty = false;
} else {
this.runHooks();
}
}
};
/**
* Sets things in motion, unless they are already that is, registering an event handler on
* <code>document.onmousemove</code>. A half-hearted attempt is made to preserve the old event
* handler if there is one.
*/
Mousetracker.prototype.enable = function () {
if (this.active) {
return;
}
this.active = true;
//~ Save the current handler for mousemove events. This isn't too
//~ robust, of course.
this.savedHandler = document.onmousemove;
//~ Gotta save @tt{this} again for the closure, and use apply for
//~ the member function.
const savedThis = this;
document.onmousemove = function (e) {
savedThis.track.apply(savedThis, [e]);
};
if (this.loopDelay) {
this.timer = setInterval(() => {
//log('loop delay in mousetracker is working');
savedThis.runHooks();
}, this.loopDelay);
}
};
/**
* Disables the tracker, removing the event handler.
*/
Mousetracker.prototype.disable = function () {
if (!this.active) {
return;
}
if (typeof this.savedHandler === 'function') {
document.onmousemove = this.savedHandler;
} else {
delete document.onmousemove;
}
if (this.timer) {
clearInterval(this.timer);
}
this.active = false;
};
/**
* Creates a new Navpopup.
* Gets a UID for the popup and
*
* @param init Contructor object. If <code>init.draggable</code> is true or absent, the popup becomes draggable.
* @constructor
* @class The Navpopup class. This generates popup hints, and does some management of them.
*/
function Navpopup(/*init*/) {
//alert('new Navpopup(init)');
/**
* UID for each Navpopup instance.
* Read-only.
*
* @type {number}
*/
this.uid = Navpopup.uid++;
/**
* Read-only flag for current visibility of the popup.
*
* @type {boolean}
* @private
*/
this.visible = false;
/** Flag to be set when we want to cancel a previous request to
* show the popup in a little while.
*
* @private
* @type {boolean}
*/
this.noshow = false;
/** Categorised list of hooks.
* @see #runHooks
* @see #addHook
* @private
* @type {Object}
*/
this.hooks = {
create: [],
unhide: [],
hide: [],
};
/**
* list of unique IDs of hook functions, to avoid duplicates
*
* @private
*/
this.hookIds = {};
/** List of downloads associated with the popup.
* @private
* @type {Array}
*/
this.downloads = [];
/**
* Number of uncompleted downloads.
*
* @type {number}
*/
this.pending = null;
/**
* Tolerance in pixels when detecting whether the mouse has left the popup.
*
* @type {number}
*/
this.fuzz = 5;
/**
* Flag to toggle running {@link #limitHorizontalPosition} to regulate the popup's position.
*
* @type {boolean}
*/
this.constrained = true;
/**
* The popup width in pixels.
*
* @private
* @type {number}
*/
this.width = 0;
/**
* The popup width in pixels.
*
* @private
* @type {number}
*/
this.height = 0;
/**
* The main content DIV element.
*
* @type {HTMLDivElement}
*/
this.mainDiv = null;
this.createMainDiv();
// if (!init || typeof init.popups_draggable=='undefined' || init.popups_draggable) {
// this.makeDraggable(true);
// }
}
/**
* A UID for each Navpopup. This constructor property is just a counter.
*
* @type {number}
* @private
*/
Navpopup.uid = 0;
/**
* Retrieves the {@link #visible} attribute, indicating whether the popup is currently visible.
*
* @type {boolean}
*/
Navpopup.prototype.isVisible = function () {
return this.visible;
};
/**
* Repositions popup using CSS style.
*
* @private
* @param {number} x x-coordinate (px)
* @param {number} y y-coordinate (px)
* @param {boolean} noLimitHor Don't call {@link #limitHorizontalPosition}
*/
Navpopup.prototype.reposition = function (x, y, noLimitHor) {
log('reposition(' + x + ',' + y + ',' + noLimitHor + ')');
if (typeof x != 'undefined' && x !== null) {
this.left = x;
}
if (typeof y != 'undefined' && y !== null) {
this.top = y;
}
if (typeof this.left != 'undefined' && typeof this.top != 'undefined') {
this.mainDiv.style.left = this.left + 'px';
this.mainDiv.style.top = this.top + 'px';
}
if (!noLimitHor) {
this.limitHorizontalPosition();
}
//console.log('navpop'+this.uid+' - (left,top)=(' + this.left + ',' + this.top + '), css=('
//+ this.mainDiv.style.left + ',' + this.mainDiv.style.top + ')');
};
/**
* Prevents popups from being in silly locations. Hopefully.
* Should not be run if {@link #constrained} is true.
*
* @private
*/
Navpopup.prototype.limitHorizontalPosition = function () {
if (!this.constrained || this.tooWide) {
return;
}
this.updateDimensions();
const x = this.left;
const w = this.width;
const cWidth = document.body.clientWidth;
// log('limitHorizontalPosition: x='+x+
// ', this.left=' + this.left +
// ', this.width=' + this.width +
// ', cWidth=' + cWidth);
if (
x + w >= cWidth ||
(x > 0 &&
this.maxWidth &&
this.width < this.maxWidth &&
this.height > this.width &&
x > cWidth - this.maxWidth)
) {
// This is a very nasty hack. There has to be a better way!
// We find the "natural" width of the div by positioning it at the far left
// then reset it so that it should be flush right (well, nearly)
this.mainDiv.style.left = '-10000px';
this.mainDiv.style.width = this.maxWidth + 'px';
const naturalWidth = parseInt(this.mainDiv.offsetWidth, 10);
let newLeft = cWidth - naturalWidth - 1;
if (newLeft < 0) {
newLeft = 0;
this.tooWide = true;
} // still unstable for really wide popups?
log(
'limitHorizontalPosition: moving to (' +
newLeft +
',' +
this.top +
');' +
' naturalWidth=' +
naturalWidth +
', clientWidth=' +
cWidth
);
this.reposition(newLeft, null, true);
}
};
/**
* Counter indicating the z-order of the "highest" popup.
* We start the z-index at 1000 so that popups are above everything
* else on the screen.
*
* @private
* @type {number}
*/
Navpopup.highest = 1000;
/**
* Brings popup to the top of the z-order.
* We increment the {@link #highest} property of the contructor here.
*
* @private
*/
Navpopup.prototype.raise = function () {
this.mainDiv.style.zIndex = Navpopup.highest + 1;
++Navpopup.highest;
};
/**
* Shows the popup provided {@link #noshow} is not true.
* Updates the position, brings the popup to the top of the z-order and unhides it.
*/
Navpopup.prototype.show = function () {
//document.title+='s';
if (this.noshow) {
return;
}
//document.title+='t';
this.reposition();
this.raise();
this.unhide();
};
/**
* Checks to see if the mouse pointer has
* stabilised (checking every <code>time</code>/2 milliseconds) and runs the
* {@link #show} method if it has.
*
* @param {number} time The minimum time (ms) before the popup may be shown.
*/
Navpopup.prototype.showSoonIfStable = function (time) {
log('showSoonIfStable, time=' + time);
if (this.visible) {
return;
}
this.noshow = false;
//~ initialize these variables so that we never run @tt{show} after
//~ just half the time
this.stable_x = -10000;
this.stable_y = -10000;
const stableShow = function () {
log('stableShow called');
let new_x = Navpopup.tracker.x,
new_y = Navpopup.tracker.y;
let dx = savedThis.stable_x - new_x,
dy = savedThis.stable_y - new_y;
let fuzz2 = 0; // savedThis.fuzz * savedThis.fuzz;
//document.title += '[' + [savedThis.stable_x,new_x, savedThis.stable_y,new_y, dx, dy, fuzz2].join(',') + '] ';
if (dx * dx <= fuzz2 && dy * dy <= fuzz2) {
log('mouse is stable');
clearInterval(savedThis.showSoonStableTimer);
savedThis.reposition.apply(savedThis, [new_x + 2, new_y + 2]);
savedThis.show.apply(savedThis, []);
savedThis.limitHorizontalPosition.apply(savedThis, []);
return;
}
savedThis.stable_x = new_x;
savedThis.stable_y = new_y;
};
var savedThis = this;
this.showSoonStableTimer = setInterval(stableShow, time / 2);
};
/**
* Sets the {@link #noshow} flag and hides the popup. This should be called
* when the mouse leaves the link before
* (or after) it's actually been displayed.
*/
Navpopup.prototype.banish = function () {
log('banish called');
// hide and prevent showing with showSoon in the future
this.noshow = true;
if (this.showSoonStableTimer) {
log('clearing showSoonStableTimer');
clearInterval(this.showSoonStableTimer);
}
this.hide();
};
/**
* Runs hooks added with {@link #addHook}.
*
* @private
* @param {string} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
* @param {string} when Controls exactly when the hook is run: either 'before' or 'after'
*/
Navpopup.prototype.runHooks = function (key, when) {
if (!this.hooks[key]) {
return;
}
const keyHooks = this.hooks[key];
const len = keyHooks.length;
for (let i = 0; i < len; ++i) {
if (keyHooks[i] && keyHooks[i].when == when) {
if (keyHooks[i].hook.apply(this, [])) {
// remove the hook
if (keyHooks[i].hookId) {
delete this.hookIds[keyHooks[i].hookId];
}
keyHooks[i] = null;
}
}
}
};
/**
* Adds a hook to the popup. Hook functions are run with <code>this</code> set to refer to the
* Navpopup instance, and no arguments.
*
* @param {Function} hook The hook function. Functions that return true are deleted.
* @param {string} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
* @param {String} when Controls exactly when the hook is run: either 'before' or 'after'
* @param {String} uid A truthy string identifying the hook function; if it matches another hook
* in this position, it won't be added again.
*/
Navpopup.prototype.addHook = function (hook, key, when, uid) {
when = when || 'after';
if (!this.hooks[key]) {
return;
}
// if uid is specified, don't add duplicates
let hookId = null;
if (uid) {
hookId = [key, when, uid].join('|');
if (this.hookIds[hookId]) {
return;
}
this.hookIds[hookId] = true;
}
this.hooks[key].push({ hook: hook, when: when, hookId: hookId });
};
/**
* Creates the main DIV element, which contains all the actual popup content.
* Runs hooks with key 'create'.
*
* @private
*/
Navpopup.prototype.createMainDiv = function () {
if (this.mainDiv) {
return;
}
this.runHooks('create', 'before');
const mainDiv = document.createElement('div');
const savedThis = this;
mainDiv.onclick = function (e) {
savedThis.onclickHandler(e);
};
mainDiv.className = this.className ? this.className : 'navpopup_maindiv';
mainDiv.id = mainDiv.className + this.uid;
mainDiv.style.position = 'absolute';
mainDiv.style.minWidth = '350px';
mainDiv.style.display = 'none';
mainDiv.className = 'navpopup';
// easy access to javascript object through DOM functions
mainDiv.navpopup = this;
this.mainDiv = mainDiv;
document.body.appendChild(mainDiv);
this.runHooks('create', 'after');
};
/**
* Calls the {@link #raise} method.
*
* @private
*/
Navpopup.prototype.onclickHandler = function (/*e*/) {
this.raise();
};
/**
* Makes the popup draggable, using a {@link Drag} object.
*
* @private
*/
Navpopup.prototype.makeDraggable = function (handleName) {
if (!this.mainDiv) {
this.createMainDiv();
}
const drag = new Drag();
if (!handleName) {
drag.startCondition = function (e) {
try {
if (!e.shiftKey) {
return false;
}
} catch (err) {
return false;
}
return true;
};
}
let dragHandle;
if (handleName) {
dragHandle = document.getElementById(handleName);
}
if (!dragHandle) {
dragHandle = this.mainDiv;
}
const np = this;
drag.endHook = function (x, y) {
Navpopup.tracker.dirty = true;
np.reposition(x, y);
};
drag.init(dragHandle, this.mainDiv);
};
/**
* Hides the popup using CSS. Runs hooks with key 'hide'.
* Sets {@link #visible} appropriately.
* {@link #banish} should be called externally instead of this method.
*
* @private
*/
Navpopup.prototype.hide = function () {
this.runHooks('hide', 'before');
this.abortDownloads();
if (typeof this.visible != 'undefined' && this.visible) {
this.mainDiv.style.display = 'none';
this.visible = false;
}
this.runHooks('hide', 'after');
};
/**
* Shows the popup using CSS. Runs hooks with key 'unhide'.
* Sets {@link #visible} appropriately. {@link #show} should be called externally instead of this method.
*
* @private
*/
Navpopup.prototype.unhide = function () {
this.runHooks('unhide', 'before');
if (typeof this.visible != 'undefined' && !this.visible) {
this.mainDiv.style.display = 'inline';
this.visible = true;
}
this.runHooks('unhide', 'after');
};
/**
* Sets the <code>innerHTML</code> attribute of the main div containing the popup content.
*
* @param {string} html The HTML to set.
*/
Navpopup.prototype.setInnerHTML = function (html) {
this.mainDiv.innerHTML = html;
};
/**
* Updates the {@link #width} and {@link #height} attributes with the CSS properties.
*
* @private
*/
Navpopup.prototype.updateDimensions = function () {
this.width = parseInt(this.mainDiv.offsetWidth, 10);
this.height = parseInt(this.mainDiv.offsetHeight, 10);
};
/**
* Checks if the point (x,y) is within {@link #fuzz} of the
* {@link #mainDiv}.
*
* @param {number} x x-coordinate (px)
* @param {number} y y-coordinate (px)
* @type {boolean}
*/
Navpopup.prototype.isWithin = function (x, y) {
//~ If we're not even visible, no point should be considered as
//~ being within the popup.
if (!this.visible) {
return false;
}
this.updateDimensions();
const fuzz = this.fuzz || 0;
//~ Use a simple box metric here.
return (
x + fuzz >= this.left &&
x - fuzz <= this.left + this.width &&
y + fuzz >= this.top &&
y - fuzz <= this.top + this.height
);
};
/**
* Adds a download to {@link #downloads}.
*
* @param {Downloader} download
*/
Navpopup.prototype.addDownload = function (download) {
if (!download) {
return;
}
this.downloads.push(download);
};
/**
* Aborts the downloads listed in {@link #downloads}.
*
* @see Downloader#abort
*/
Navpopup.prototype.abortDownloads = function () {
for (let i = 0; i < this.downloads.length; ++i) {
const d = this.downloads[i];
if (d && d.abort) {
d.abort();
}
}
this.downloads = [];
};
/**
* A {@link Mousetracker} instance which is a property of the constructor (pseudo-global).
*/
Navpopup.tracker = new Mousetracker();
// ENDFILE: navpopup.js
// STARTFILE: diff.js
/*
* Javascript Diff Algorithm
* By John Resig (http://ejohn.org/) and [[:en:User:Lupin]]
*
* More Info:
* http://ejohn.org/projects/javascript-diff-algorithm/
*/
function delFmt(x) {
if (!x.length) {
return '';
}
return "<del class='popupDiff'>" + x.join('') + '</del>';
}
function insFmt(x) {
if (!x.length) {
return '';
}
return "<ins class='popupDiff'>" + x.join('') + '</ins>';
}
function countCrossings(a, b, i, eject) {
// count the crossings on the edge starting at b[i]
if (!b[i].row && b[i].row !== 0) {
return -1;
}
let count = 0;
for (let j = 0; j < a.length; ++j) {
if (!a[j].row && a[j].row !== 0) {
continue;
}
if ((j - b[i].row) * (i - a[j].row) > 0) {
if (eject) {
return true;
}
count++;
}
}
return count;
}
function shortenDiffString(str, context) {
const re = /(<del[\s\S]*?<\/del>|<ins[\s\S]*?<\/ins>)/;
const splitted = str.parenSplit(re);
let ret = [''];
for (let i = 0; i < splitted.length; i += 2) {
if (splitted[i].length < 2 * context) {
ret[ret.length - 1] += splitted[i];
if (i + 1 < splitted.length) {
ret[ret.length - 1] += splitted[i + 1];
}
continue;
} else {
if (i > 0) {
ret[ret.length - 1] += splitted[i].substring(0, context);
}
if (i + 1 < splitted.length) {
ret.push(splitted[i].substring(splitted[i].length - context) + splitted[i + 1]);
}
}
}
while (ret.length > 0 && !ret[0]) {
ret = ret.slice(1);
}
return ret;
}
function diffString(o, n, simpleSplit) {
const splitRe = /([[]{2}|[\]]{2}|[{]{2,3}|[}]{2,3}|[|]|=|<|>|[*:]+|\s|\b)/;
// We need to split the strings o and n first, and entify() the parts
// individually, so that the HTML entities are never cut apart. (AxelBoldt)
let out, i, oSplitted, nSplitted;
if (simpleSplit) {
oSplitted = o.split(/\b/);
nSplitted = n.split(/\b/);
} else {
oSplitted = o.parenSplit(splitRe);
nSplitted = n.parenSplit(splitRe);
}
for (i = 0; i < oSplitted.length; ++i) {
oSplitted[i] = oSplitted[i].entify();
}
for (i = 0; i < nSplitted.length; ++i) {
nSplitted[i] = nSplitted[i].entify();
}
out = diff(oSplitted, nSplitted);
let str = '';
let acc = []; // accumulator for prettier output
// crossing pairings -- eg 'A B' vs 'B A' -- cause problems, so let's iron them out
// this doesn't always do things optimally but it should be fast enough
let maxOutputPair = 0;
for (i = 0; i < out.n.length; ++i) {
if (out.n[i].paired) {
if (maxOutputPair > out.n[i].row) {
// tangle - delete pairing
out.o[out.n[i].row] = out.o[out.n[i].row].text;
out.n[i] = out.n[i].text;
}
if (maxOutputPair < out.n[i].row) {
maxOutputPair = out.n[i].row;
}
}
}
// output the stuff preceding the first paired old line
for (i = 0; i < out.o.length && !out.o[i].paired; ++i) {
acc.push(out.o[i]);
}
str += delFmt(acc);
acc = [];
// main loop
for (i = 0; i < out.n.length; ++i) {
// output unpaired new "lines"
while (i < out.n.length && !out.n[i].paired) {
acc.push(out.n[i++]);
}
str += insFmt(acc);
acc = [];
if (i < out.n.length) {
// this new "line" is paired with the (out.n[i].row)th old "line"
str += out.n[i].text;
// output unpaired old rows starting after this new line's partner
let m = out.n[i].row + 1;
while (m < out.o.length && !out.o[m].paired) {
acc.push(out.o[m++]);
}
str += delFmt(acc);
acc = [];
}
}
return str;
}
// see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object
// FIXME: use obj.hasOwnProperty instead of this kludge!
const jsReservedProperties = RegExp(
'^(constructor|prototype|__((define|lookup)[GS]etter)__' +
'|eval|hasOwnProperty|propertyIsEnumerable' +
'|to(Source|String|LocaleString)|(un)?watch|valueOf)$'
);
function diffBugAlert(word) {
if (!diffBugAlert.list[word]) {
diffBugAlert.list[word] = 1;
alert('Bad word: ' + word + '\n\nPlease report this bug.');
}
}
diffBugAlert.list = {};
function makeDiffHashtable(src) {
const ret = {};
for (let i = 0; i < src.length; i++) {
if (jsReservedProperties.test(src[i])) {
src[i] += '<!-- -->';
}
if (!ret[src[i]]) {
ret[src[i]] = [];
}
try {
ret[src[i]].push(i);
} catch (err) {
diffBugAlert(src[i]);
}
}
return ret;
}
function diff(o, n) {
// pass 1: make hashtable ns with new rows as keys
const ns = makeDiffHashtable(n);
// pass 2: make hashtable os with old rows as keys
const os = makeDiffHashtable(o);
// pass 3: pair unique new rows and matching unique old rows
let i;
for (i in ns) {
if (ns[i].length == 1 && os[i] && os[i].length == 1) {
n[ns[i][0]] = { text: n[ns[i][0]], row: os[i][0], paired: true };
o[os[i][0]] = { text: o[os[i][0]], row: ns[i][0], paired: true };
}
}
// pass 4: pair matching rows immediately following paired rows (not necessarily unique)
for (i = 0; i < n.length - 1; i++) {
if (
n[i].paired &&
!n[i + 1].paired &&
n[i].row + 1 < o.length &&
!o[n[i].row + 1].paired &&
n[i + 1] == o[n[i].row + 1]
) {
n[i + 1] = { text: n[i + 1], row: n[i].row + 1, paired: true };
o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1, paired: true };
}
}
// pass 5: pair matching rows immediately preceding paired rows (not necessarily unique)
for (i = n.length - 1; i > 0; i--) {
if (
n[i].paired &&
!n[i - 1].paired &&
n[i].row > 0 &&
!o[n[i].row - 1].paired &&
n[i - 1] == o[n[i].row - 1]
) {
n[i - 1] = { text: n[i - 1], row: n[i].row - 1, paired: true };
o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1, paired: true };
}
}
return { o: o, n: n };
}
// ENDFILE: diff.js
// STARTFILE: init.js
function setSiteInfo() {
if (window.popupLocalDebug) {
pg.wiki.hostname = 'en.wikipedia.org';
} else {
pg.wiki.hostname = location.hostname; // use in preference to location.hostname for flexibility (?)
}
pg.wiki.wikimedia = /(wiki([pm]edia|source|books|news|quote|versity|species|voyage|data)|metawiki|wiktionary|mediawiki)[.]org/.test(pg.wiki.hostname);
pg.wiki.wikia = /[.]wikia[.]com$/i.test(pg.wiki.hostname);
pg.wiki.isLocal = /^localhost/.test(pg.wiki.hostname);
pg.wiki.commons =
pg.wiki.wikimedia && pg.wiki.hostname != 'commons.wikimedia.org' ?
'commons.wikimedia.org' :
null;
pg.wiki.lang = mw.config.get('wgContentLanguage');
const port = location.port ? ':' + location.port : '';
pg.wiki.sitebase = pg.wiki.hostname + port;
}
function setUserInfo() {
const params = {
action: 'query',
list: 'users',
ususers: mw.config.get('wgUserName'),
usprop: 'rights',
};
pg.user.canReview = false;
if (getValueOf('popupReview')) {
getMwApi()
.get(params)
.done((data) => {
const rights = data.query.users[0].rights;
pg.user.canReview = rights.indexOf('review') !== -1; // TODO: Should it be a getValueOf('ReviewRight') ?
});
}
}
function fetchSpecialPageNames() {
const params = {
action: 'query',
meta: 'siteinfo',
siprop: 'specialpagealiases',
formatversion: 2,
// cache for an hour
uselang: 'content',
maxage: 3600,
};
return getMwApi()
.get(params)
.then((data) => {
pg.wiki.specialpagealiases = data.query.specialpagealiases;
});
}
function setTitleBase() {
const protocol = window.popupLocalDebug ? 'http:' : location.protocol;
pg.wiki.articlePath = mw.config.get('wgArticlePath').replace(/\/\$1/, ''); // as in http://some.thing.com/wiki/Article
pg.wiki.botInterfacePath = mw.config.get('wgScript');
pg.wiki.APIPath = mw.config.get('wgScriptPath') + '/api.php';
// default mediawiki setting is paths like http://some.thing.com/articlePath/index.php?title=foo
const titletail = pg.wiki.botInterfacePath + '?title=';
//var titletail2 = joinPath([pg.wiki.botInterfacePath, 'wiki.phtml?title=']);
// other sites may need to add code here to set titletail depending on how their urls work
pg.wiki.titlebase = protocol + '//' + pg.wiki.sitebase + titletail;
//pg.wiki.titlebase2 = protocol + '//' + joinPath([pg.wiki.sitebase, titletail2]);
pg.wiki.wikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.botInterfacePath;
pg.wiki.apiwikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.APIPath;
pg.wiki.articlebase = protocol + '//' + pg.wiki.sitebase + pg.wiki.articlePath;
pg.wiki.commonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.botInterfacePath;
pg.wiki.apicommonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.APIPath;
pg.re.basenames = RegExp(
'^(' +
map(literalizeRegex, [
pg.wiki.titlebase, //pg.wiki.titlebase2,
pg.wiki.articlebase,
]).join('|') +
')'
);
}
//////////////////////////////////////////////////
// Global regexps
function setMainRegex() {
const reStart = '[^:]*://';
let preTitles =
// Take customizable wgScript into account (it isn't guaranteed to be index.php)
'(?:' + literalizeRegex(mw.config.get('wgScript')) + '|' +
// handle index.php (likely to work even if different from wgScript) and legacy wiki.phtml
literalizeRegex(mw.config.get('wgScriptPath')) + '/(?:index[.]php|wiki[.]phtml))';
preTitles += '[?]title=|' + literalizeRegex(pg.wiki.articlePath + '/');
const reEnd = '(' + preTitles + ')([^&?#]*)[^#]*(?:#(.+))?';
pg.re.main = RegExp(reStart + literalizeRegex(pg.wiki.sitebase) + reEnd);
}
function buildSpecialPageGroup(specialPageObj) {
const variants = [];
variants.push(mw.util.escapeRegExp(specialPageObj.realname));
variants.push(mw.util.escapeRegExp(encodeURI(specialPageObj.realname)));
specialPageObj.aliases.forEach((alias) => {
variants.push(mw.util.escapeRegExp(alias));
variants.push(mw.util.escapeRegExp(encodeURI(alias)));
});
return variants.join('|');
}
function setRegexps() {
setMainRegex();
const sp = nsRe(pg.nsSpecialId);
pg.re.urlNoPopup = RegExp('((title=|/)' + sp + '(?:%3A|:)|section=[0-9]|^#$)');
pg.wiki.specialpagealiases.forEach((specialpage) => {
if (specialpage.realname === 'Contributions') {
pg.re.contribs = RegExp(
'(title=|/)' +
sp +
'(?:%3A|:)(?:' +
buildSpecialPageGroup(specialpage) +
')' +
'(&target=|/|/' +
nsRe(pg.nsUserId) +
':)(.*)',
'i'
);
} else if (specialpage.realname === 'Diff') {
pg.re.specialdiff = RegExp(
'/' + sp + '(?:%3A|:)(?:' + buildSpecialPageGroup(specialpage) + ')/([^?#]*)',
'i'
);
} else if (specialpage.realname === 'Emailuser') {
pg.re.email = RegExp(
'(title=|/)' +
sp +
'(?:%3A|:)(?:' +
buildSpecialPageGroup(specialpage) +
')' +
'(&target=|/|/(?:' +
nsRe(pg.nsUserId) +
':)?)(.*)',
'i'
);
} else if (specialpage.realname === 'Whatlinkshere') {
pg.re.backlinks = RegExp(
'(title=|/)' +
sp +
'(?:%3A|:)(?:' +
buildSpecialPageGroup(specialpage) +
')' +
'(&target=|/)([^&]*)',
'i'
);
}
});
const im = nsReImage();
// note: tries to get images in infobox templates too, e.g. movie pages, album pages etc
// (^|\[\[)image: *([^|\]]*[^|\] ]) *
// (^|\[\[)image: *([^|\]]*[^|\] ])([^0-9\]]*([0-9]+) *px)?
// $4 = 120 as in 120px
pg.re.image = RegExp(
'(^|\\[\\[)' +
im +
': *([^|\\]]*[^|\\] ])' +
'([^0-9\\]]*([0-9]+) *px)?|(?:\\n *[|]?|[|]) *' +
'(' +
getValueOf('popupImageVarsRegexp') +
')' +
' *= *(?:\\[\\[ *)?(?:' +
im +
':)?' +
'([^|]*?)(?:\\]\\])? *[|]? *\\n',
'img'
);
pg.re.imageBracketCount = 6;
pg.re.category = RegExp('\\[\\[' + nsRe(pg.nsCategoryId) + ': *([^|\\]]*[^|\\] ]) *', 'i');
pg.re.categoryBracketCount = 1;
pg.re.ipUser = RegExp(
'^' +
// IPv6
'(?::(?::|(?::[0-9A-Fa-f]{1,4}){1,7})|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}::|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){7})' +
// IPv4
'|(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' +
'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]))$'
);
pg.re.stub = RegExp(getValueOf('popupStubRegexp'), 'im');
pg.re.disambig = RegExp(getValueOf('popupDabRegexp'), 'im');
// FIXME replace with general parameter parsing function, this is daft
pg.re.oldid = /[?&]oldid=([^&]*)/;
pg.re.diff = /[?&]diff=([^&]*)/;
}
//////////////////////////////////////////////////
// miscellany
function setupCache() {
// page caching
pg.cache.pages = [];
}
function setMisc() {
pg.current.link = null;
pg.current.links = [];
pg.current.linksHash = {};
setupCache();
pg.timer.checkPopupPosition = null;
pg.counter.loop = 0;
// ids change with each popup: popupImage0, popupImage1 etc
pg.idNumber = 0;
// for myDecodeURI
pg.misc.decodeExtras = [
{ from: '%2C', to: ',' },
{ from: '_', to: ' ' },
{ from: '%24', to: '$' },
{ from: '%26', to: '&' }, // no ,
];
}
function getMwApi() {
if (!pg.api.client) {
pg.api.userAgent = 'Navigation popups/1.0 (' + mw.config.get('wgServerName') + ')';
pg.api.client = new mw.Api({
ajax: {
headers: {
'Api-User-Agent': pg.api.userAgent,
},
},
});
}
return pg.api.client;
}
// We need a callback since this might end up asynchronous because of
// the mw.loader.using() call.
function setupPopups(callback) {
if (setupPopups.completed) {
if (typeof callback === 'function') {
callback();
}
return;
}
// These dependencies should alse be enforced from the gadget,
// but not everyone loads this as a gadget, so double check
mw.loader
.using([
'mediawiki.util',
'mediawiki.api',
'mediawiki.user',
'user.options',
'mediawiki.jqueryMsg',
])
.then(fetchSpecialPageNames)
.then(() => {
// NB translatable strings should be set up first (strings.js)
// basics
setupDebugging();
setSiteInfo();
setTitleBase();
setOptions(); // see options.js
setUserInfo();
// namespaces etc
setNamespaces();
setInterwiki();
// regexps
setRegexps();
setRedirs();
// other stuff
setMisc();
setupLivePreview();
// main deal here
setupTooltips();
log('In setupPopups(), just called setupTooltips()');
Navpopup.tracker.enable();
setupPopups.completed = true;
if (typeof callback === 'function') {
callback();
}
});
}
// ENDFILE: init.js
// STARTFILE: navlinks.js
//////////////////////////////////////////////////
// navlinks... let the fun begin
//
function defaultNavlinkSpec() {
let str = '';
str += '<b><<mainlink|shortcut= >></b>';
if (getValueOf('popupLastEditLink')) {
str +=
'*<<lastEdit|shortcut=/>>|<<lastContrib>>|<<sinceMe>>if(oldid){|<<oldEdit>>|<<diffCur>>}';
}
// user links
// contribs - log - count - email - block
// count only if applicable; block only if popupAdminLinks
str += 'if(user){<br><<contribs|shortcut=c>>*<<userlog|shortcut=L|log>>';
str += 'if(ipuser){*<<arin>>}if(wikimedia){*<<count|shortcut=#>>}';
str +=
'if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>|<<blocklog|log>>}}';
// editing links
// talkpage -> edit|new - history - un|watch - article|edit
// other page -> edit - history - un|watch - talk|edit|new
const editstr = '<<edit|shortcut=e>>';
const editOldidStr =
'if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' +
editstr +
'}';
const historystr = '<<history|shortcut=h>>|<<editors|shortcut=E|>>';
const watchstr = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
str +=
'<br>if(talk){' +
editOldidStr +
'|<<new|shortcut=+>>' +
'*' +
historystr +
'*' +
watchstr +
'*' +
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
'}else{' + // not a talk page
editOldidStr +
'*' +
historystr +
'*' +
watchstr +
'*' +
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
// misc links
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>*<<move|shortcut=m>>';
// admin links
str +=
'if(admin){<br><<unprotect|unprotectShort>>|<<protect|shortcut=p>>|<<protectlog|log>>*' +
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>|<<deletelog|log>>}';
return str;
}
function navLinksHTML(article, hint, params) {
//oldid, rcid) {
const str = '<span class="popupNavLinks">' + defaultNavlinkSpec() + '</span>';
// BAM
return navlinkStringToHTML(str, article, params);
}
function expandConditionalNavlinkString(s, article, z, recursionCount) {
const oldid = z.oldid,
rcid = z.rcid,
diff = z.diff;
// nested conditionals (up to 10 deep) are ok, hopefully! (work from the inside out)
if (typeof recursionCount != typeof 0) {
recursionCount = 0;
}
const conditionalSplitRegex = RegExp(
//(1 if \\( (2 2) \\) {(3 3)} (4 else {(5 5)} 4)1)
'(;?\\s*if\\s*\\(\\s*([\\w]*)\\s*\\)\\s*\\{([^{}]*)\\}(\\s*else\\s*\\{([^{}]*?)\\}|))',
'i'
);
const splitted = s.parenSplit(conditionalSplitRegex);
// $1: whole conditional
// $2: test condition
// $3: true expansion
// $4: else clause (possibly empty)
// $5: false expansion (possibly null)
const numParens = 5;
let ret = splitted[0];
for (let i = 1; i < splitted.length; i = i + numParens + 1) {
const testString = splitted[i + 2 - 1];
const trueString = splitted[i + 3 - 1];
let falseString = splitted[i + 5 - 1];
if (typeof falseString == 'undefined' || !falseString) {
falseString = '';
}
let testResult = null;
switch (testString) {
case 'user':
testResult = !!article.userName();
break;
case 'talk':
testResult = !article.talkPage(); // talkPage converts _articles_ to talkPages
break;
case 'admin':
testResult = !!getValueOf('popupAdminLinks');
break;
case 'oldid':
testResult = !!(typeof oldid != 'undefined' && oldid);
break;
case 'rcid':
testResult = !!(typeof rcid != 'undefined' && rcid);
break;
case 'ipuser':
testResult = !!article.isIpUser();
break;
case 'mainspace_en':
testResult = isInMainNamespace(article) && pg.wiki.hostname == 'en.wikipedia.org';
break;
case 'wikimedia':
testResult = !!pg.wiki.wikimedia;
break;
case 'diff':
testResult = !!(typeof diff != 'undefined' && diff);
break;
}
switch (testResult) {
case null:
ret += splitted[i];
break;
case true:
ret += trueString;
break;
case false:
ret += falseString;
break;
}
// append non-conditional string
ret += splitted[i + numParens];
}
if (conditionalSplitRegex.test(ret) && recursionCount < 10) {
return expandConditionalNavlinkString(ret, article, z, recursionCount + 1);
}
return ret;
}
function navlinkStringToArray(s, article, params) {
s = expandConditionalNavlinkString(s, article, params);
const splitted = s.parenSplit(/<<(.*?)>>/);
const ret = [];
for (let i = 0; i < splitted.length; ++i) {
if (i % 2) {
// i odd, so s is a tag
const t = new navlinkTag();
const ss = splitted[i].split('|');
t.id = ss[0];
for (let j = 1; j < ss.length; ++j) {
const sss = ss[j].split('=');
if (sss.length > 1) {
t[sss[0]] = sss[1];
} else {
// no assignment (no "="), so treat this as a title (overwriting the last one)
t.text = popupString(sss[0]);
}
}
t.article = article;
const oldid = params.oldid,
rcid = params.rcid,
diff = params.diff;
if (typeof oldid !== 'undefined' && oldid !== null) {
t.oldid = oldid;
}
if (typeof rcid !== 'undefined' && rcid !== null) {
t.rcid = rcid;
}
if (typeof diff !== 'undefined' && diff !== null) {
t.diff = diff;
}
if (!t.text && t.id !== 'mainlink') {
t.text = popupString(t.id);
}
ret.push(t);
} else {
// plain HTML
ret.push(splitted[i]);
}
}
return ret;
}
function navlinkSubstituteHTML(s) {
return s
.split('*')
.join(getValueOf('popupNavLinkSeparator'))
.split('<menurow>')
.join('<li class="popup_menu_row">')
.split('</menurow>')
.join('</li>')
.split('<menu>')
.join('<ul class="popup_menu">')
.split('</menu>')
.join('</ul>');
}
function navlinkDepth(magic, s) {
return s.split('<' + magic + '>').length - s.split('</' + magic + '>').length;
}
// navlinkString: * becomes the separator
// <<foo|bar=baz|fubar>> becomes a foo-link with attribute bar='baz'
// and visible text 'fubar'
// if(test){...} and if(test){...}else{...} work too (nested ok)
function navlinkStringToHTML(s, article, params) {
//limitAlert(navlinkStringToHTML, 5, 'navlinkStringToHTML\n' + article + '\n' + (typeof article));
const p = navlinkStringToArray(s, article, params);
let html = '';
let menudepth = 0; // nested menus not currently allowed, but doesn't do any harm to code for it
let menurowdepth = 0;
for (let i = 0; i < p.length; ++i) {
if (typeof p[i] == typeof '') {
html += navlinkSubstituteHTML(p[i]);
menudepth += navlinkDepth('menu', p[i]);
menurowdepth += navlinkDepth('menurow', p[i]);
// if (menudepth === 0) {
// tagType='span';
// } else if (menurowdepth === 0) {
// tagType='li';
// } else {
// tagType = null;
// }
} else if (typeof p[i].type != 'undefined' && p[i].type == 'navlinkTag') {
if (menudepth > 0 && menurowdepth === 0) {
html += '<li class="popup_menu_item">' + p[i].html() + '</li>';
} else {
html += p[i].html();
}
}
}
return html;
}
function navlinkTag() {
this.type = 'navlinkTag';
}
navlinkTag.prototype.html = function () {
this.getNewWin();
this.getPrintFunction();
let html = '';
let opening, closing;
const tagType = 'span';
if (!tagType) {
opening = '';
closing = '';
} else {
opening = '<' + tagType + ' class="popup_' + this.id + '">';
closing = '</' + tagType + '>';
}
if (typeof this.print != 'function') {
errlog('Oh dear - invalid print function for a navlinkTag, id=' + this.id);
} else {
html = this.print(this);
if (typeof html != typeof '') {
html = '';
} else if (typeof this.shortcut != 'undefined') {
html = addPopupShortcut(html, this.shortcut);
}
}
return opening + html + closing;
};
navlinkTag.prototype.getNewWin = function () {
getValueOf('popupLinksNewWindow');
if (typeof pg.option.popupLinksNewWindow[this.id] === 'undefined') {
this.newWin = null;
}
this.newWin = pg.option.popupLinksNewWindow[this.id];
};
navlinkTag.prototype.getPrintFunction = function () {
//think about this some more
// this.id and this.article should already be defined
if (typeof this.id != typeof '' || typeof this.article != typeof {}) {
return;
}
this.noPopup = 1;
switch (this.id) {
case 'contribs':
case 'history':
case 'whatLinksHere':
case 'userPage':
case 'monobook':
case 'userTalk':
case 'talk':
case 'article':
case 'lastEdit':
this.noPopup = null;
}
switch (this.id) {
case 'email':
case 'contribs':
case 'block':
case 'unblock':
case 'userlog':
case 'userSpace':
case 'deletedContribs':
this.article = this.article.userName();
}
switch (this.id) {
case 'userTalk':
case 'newUserTalk':
case 'editUserTalk':
case 'userPage':
case 'monobook':
case 'editMonobook':
case 'blocklog':
this.article = this.article.userName(true);
/* fall through */
case 'pagelog':
case 'deletelog':
case 'protectlog':
delete this.oldid;
}
if (this.id == 'editMonobook' || this.id == 'monobook') {
this.article.append('/monobook.js');
}
if (this.id != 'mainlink') {
// FIXME anchor handling should be done differently with Title object
this.article = this.article.removeAnchor();
// if (typeof this.text=='undefined') this.text=popupString(this.id);
}
switch (this.id) {
case 'undelete':
this.print = specialLink;
this.specialpage = 'Undelete';
this.sep = '/';
break;
case 'whatLinksHere':
this.print = specialLink;
this.specialpage = 'Whatlinkshere';
break;
case 'relatedChanges':
this.print = specialLink;
this.specialpage = 'Recentchangeslinked';
break;
case 'move':
this.print = specialLink;
this.specialpage = 'Movepage';
break;
case 'contribs':
this.print = specialLink;
this.specialpage = 'Contributions';
break;
case 'deletedContribs':
this.print = specialLink;
this.specialpage = 'Deletedcontributions';
break;
case 'email':
this.print = specialLink;
this.specialpage = 'EmailUser';
this.sep = '/';
break;
case 'block':
this.print = specialLink;
this.specialpage = 'Blockip';
this.sep = '&ip=';
break;
case 'unblock':
this.print = specialLink;
this.specialpage = 'Ipblocklist';
this.sep = '&action=unblock&ip=';
break;
case 'userlog':
this.print = specialLink;
this.specialpage = 'Log';
this.sep = '&user=';
break;
case 'blocklog':
this.print = specialLink;
this.specialpage = 'Log';
this.sep = '&type=block&page=';
break;
case 'pagelog':
this.print = specialLink;
this.specialpage = 'Log';
this.sep = '&page=';
break;
case 'protectlog':
this.print = specialLink;
this.specialpage = 'Log';
this.sep = '&type=protect&page=';
break;
case 'deletelog':
this.print = specialLink;
this.specialpage = 'Log';
this.sep = '&type=delete&page=';
break;
case 'userSpace':
this.print = specialLink;
this.specialpage = 'PrefixIndex';
this.sep = '&namespace=2&prefix=';
break;
case 'search':
this.print = specialLink;
this.specialpage = 'Search';
this.sep = '&fulltext=Search&search=';
break;
case 'thank':
this.print = specialLink;
this.specialpage = 'Thanks';
this.sep = '/';
this.article.value = this.diff !== 'prev' ? this.diff : this.oldid;
break;
case 'unwatch':
case 'watch':
this.print = magicWatchLink;
this.action =
this.id +
'&autowatchlist=1&autoimpl=' +
popupString('autoedit_version') +
'&actoken=' +
autoClickToken();
break;
case 'history':
case 'historyfeed':
case 'unprotect':
case 'protect':
this.print = wikiLink;
this.action = this.id;
break;
case 'delete':
this.print = wikiLink;
this.action = 'delete';
if (this.article.namespaceId() == pg.nsImageId) {
const img = this.article.stripNamespace();
this.action += '&image=' + img;
}
break;
case 'markpatrolled':
case 'edit': // editOld should keep the oldid, but edit should not.
delete this.oldid;
/* fall through */
case 'view':
case 'purge':
case 'render':
this.print = wikiLink;
this.action = this.id;
break;
case 'raw':
this.print = wikiLink;
this.action = 'raw';
break;
case 'new':
this.print = wikiLink;
this.action = 'edit§ion=new';
break;
case 'mainlink':
if (typeof this.text == 'undefined') {
this.text = this.article.toString().entify();
}
if (getValueOf('popupSimplifyMainLink') && isInStrippableNamespace(this.article)) {
// only show the /subpage part of the title text
const s = this.text.split('/');
this.text = s[s.length - 1];
if (this.text === '' && s.length > 1) {
this.text = s[s.length - 2];
}
}
this.print = titledWikiLink;
if (
typeof this.title === 'undefined' &&
pg.current.link &&
typeof pg.current.link.href !== 'undefined'
) {
this.title = safeDecodeURI(
pg.current.link.originalTitle ? pg.current.link.originalTitle : this.article
);
if (typeof this.oldid !== 'undefined' && this.oldid) {
this.title = tprintf('Revision %s of %s', [this.oldid, this.title]);
}
}
this.action = 'view';
break;
case 'userPage':
case 'article':
case 'monobook':
case 'editMonobook':
case 'editArticle':
delete this.oldid;
//alert(this.id+'\n'+this.article + '\n'+ typeof this.article);
this.article = this.article.articleFromTalkOrArticle();
//alert(this.id+'\n'+this.article + '\n'+ typeof this.article);
this.print = wikiLink;
if (this.id.indexOf('edit') === 0) {
this.action = 'edit';
} else {
this.action = 'view';
}
break;
case 'userTalk':
case 'talk':
this.article = this.article.talkPage();
delete this.oldid;
this.print = wikiLink;
this.action = 'view';
break;
case 'arin':
this.print = arinLink;
break;
case 'count':
this.print = editCounterLink;
break;
case 'google':
this.print = googleLink;
break;
case 'editors':
this.print = editorListLink;
break;
case 'globalsearch':
this.print = globalSearchLink;
break;
case 'lastEdit':
this.print = titledDiffLink;
this.title = popupString('Show the last edit');
this.from = 'prev';
this.to = 'cur';
break;
case 'oldEdit':
this.print = titledDiffLink;
this.title = popupString('Show the edit made to get revision') + ' ' + this.oldid;
this.from = 'prev';
this.to = this.oldid;
break;
case 'editOld':
this.print = wikiLink;
this.action = 'edit';
break;
case 'undo':
this.print = wikiLink;
this.action = 'edit&undo=';
break;
case 'revert':
this.print = wikiLink;
this.action = 'revert';
break;
case 'nullEdit':
this.print = wikiLink;
this.action = 'nullEdit';
break;
case 'diffCur':
this.print = titledDiffLink;
this.title = tprintf('Show changes since revision %s', [this.oldid]);
this.from = this.oldid;
this.to = 'cur';
break;
case 'editUserTalk':
case 'editTalk':
delete this.oldid;
this.article = this.article.talkPage();
this.action = 'edit';
this.print = wikiLink;
break;
case 'newUserTalk':
case 'newTalk':
this.article = this.article.talkPage();
this.action = 'edit§ion=new';
this.print = wikiLink;
break;
case 'lastContrib':
case 'sinceMe':
this.print = magicHistoryLink;
break;
case 'togglePreviews':
this.text = popupString(pg.option.simplePopups ? 'enable previews' : 'disable previews');
/* fall through */
case 'disablePopups':
case 'purgePopups':
this.print = popupMenuLink;
break;
default:
this.print = function () {
return 'Unknown navlink type: ' + String(this.id);
};
}
};
//
// end navlinks
//////////////////////////////////////////////////
// ENDFILE: navlinks.js
// STARTFILE: shortcutkeys.js
function popupHandleKeypress(evt) {
const keyCode = window.event ? window.event.keyCode : evt.keyCode ? evt.keyCode : evt.which;
if (!keyCode || !pg.current.link || !pg.current.link.navpopup) {
return;
}
if (keyCode == 27) {
// escape
killPopup();
return false; // swallow keypress
}
const letter = String.fromCharCode(keyCode);
const links = pg.current.link.navpopup.mainDiv.getElementsByTagName('A');
let startLink = 0;
let i, j;
if (popupHandleKeypress.lastPopupLinkSelected) {
for (i = 0; i < links.length; ++i) {
if (links[i] == popupHandleKeypress.lastPopupLinkSelected) {
startLink = i;
}
}
}
for (j = 0; j < links.length; ++j) {
i = (startLink + j + 1) % links.length;
if (links[i].getAttribute('popupkey') == letter) {
if (evt && evt.preventDefault) {
evt.preventDefault();
}
links[i].focus();
popupHandleKeypress.lastPopupLinkSelected = links[i];
return false; // swallow keypress
}
}
// pass keypress on
if (document.oldPopupOnkeypress) {
return document.oldPopupOnkeypress(evt);
}
return true;
}
function addPopupShortcuts() {
if (document.onkeypress != popupHandleKeypress) {
document.oldPopupOnkeypress = document.onkeypress;
}
document.onkeypress = popupHandleKeypress;
}
function rmPopupShortcuts() {
popupHandleKeypress.lastPopupLinkSelected = null;
try {
if (document.oldPopupOnkeypress && document.oldPopupOnkeypress == popupHandleKeypress) {
// panic
document.onkeypress = null; //function () {};
return;
}
document.onkeypress = document.oldPopupOnkeypress;
} catch (nasties) {
/* IE goes here */
}
}
function addLinkProperty(html, property) {
// take "<a href=...>...</a> and add a property
// not sophisticated at all, easily broken
const i = html.indexOf('>');
if (i < 0) {
return html;
}
return html.substring(0, i) + ' ' + property + html.substring(i);
}
function addPopupShortcut(html, key) {
if (!getValueOf('popupShortcutKeys')) {
return html;
}
const ret = addLinkProperty(html, 'popupkey="' + key + '"');
if (key == ' ') {
key = popupString('spacebar');
}
return ret.replace(/^(.*?)(title=")(.*?)(".*)$/i, '$1$2$3 [' + key + ']$4');
}
// ENDFILE: shortcutkeys.js
// STARTFILE: diffpreview.js
/**
* Load diff data.
*
* lets jump through hoops to find the rev ids we need to retrieve
*
* @param {Title} article
* @param {string} oldid
* @param {string} diff
* @param {Navpopup} navpop
*/
function loadDiff(article, oldid, diff, navpop) {
navpop.diffData = { oldRev: {}, newRev: {} };
mw.loader.using('mediawiki.api').then(() => {
const api = getMwApi();
const params = {
action: 'compare',
prop: 'ids|title',
};
params.fromtitle = article.toString();
switch (diff) {
case 'cur':
switch (oldid) {
case null:
case '':
case 'prev':
// this can only work if we have the title
// cur -> prev
params.torelative = 'prev';
break;
default:
params.fromrev = oldid;
params.torelative = 'cur';
break;
}
break;
case 'prev':
if (oldid && oldid !== 'cur') {
params.fromrev = oldid;
}
params.torelative = 'prev';
break;
case 'next':
params.fromrev = oldid || 0;
params.torelative = 'next';
break;
default:
params.fromrev = oldid || 0;
params.torev = diff || 0;
break;
}
api.get(params).then((data) => {
navpop.diffData.oldRev.revid = data.compare.fromrevid;
navpop.diffData.newRev.revid = data.compare.torevid;
addReviewLink(navpop, 'popupMiscTools');
const go = function () {
pendingNavpopTask(navpop);
let url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';
url += 'revids=' + navpop.diffData.oldRev.revid + '|' + navpop.diffData.newRev.revid;
url += '&prop=revisions&rvslots=main&rvprop=ids|timestamp|content';
getPageWithCaching(url, doneDiff, navpop);
return true; // remove hook once run
};
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
go();
} else {
navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_DIFFS');
}
});
});
}
// Put a "mark patrolled" link to an element target
// TODO: Allow patrol a revision, as well as a diff
function addReviewLink(navpop, target) {
if (!pg.user.canReview) {
return;
}
// If 'newRev' is older than 'oldRev' than it could be confusing, so we do not show the review link.
if (navpop.diffData.newRev.revid <= navpop.diffData.oldRev.revid) {
return;
}
const params = {
action: 'query',
prop: 'info|flagged',
revids: navpop.diffData.oldRev.revid,
formatversion: 2,
};
getMwApi()
.get(params)
.then((data) => {
const stable_revid =
(data.query.pages[0].flagged && data.query.pages[0].flagged.stable_revid) || 0;
// The diff can be reviewed if the old version is the last reviewed version
// TODO: Other possible conditions that we may want to implement instead of this one:
// * old version is patrolled and the new version is not patrolled
// * old version is patrolled and the new version is more recent than the last reviewed version
if (stable_revid == navpop.diffData.oldRev.revid) {
const a = document.createElement('a');
a.innerHTML = popupString('mark patrolled');
a.title = popupString('markpatrolledHint');
a.onclick = function () {
const params = {
action: 'review',
revid: navpop.diffData.newRev.revid,
comment: tprintf('defaultpopupReviewedSummary', [
navpop.diffData.oldRev.revid,
navpop.diffData.newRev.revid,
]),
};
getMwApi()
.postWithToken('csrf', params)
.done(() => {
a.style.display = 'none';
// TODO: Update current page and other already constructed popups
})
.fail(() => {
alert(popupString('Could not marked this edit as patrolled'));
});
};
setPopupHTML(a, target, navpop.idNumber, null, true);
}
});
}
function doneDiff(download) {
if (!download.owner || !download.owner.diffData) {
return;
}
const navpop = download.owner;
completedNavpopTask(navpop);
let pages,
revisions = [];
try {
// Process the downloads
pages = getJsObj(download.data).query.pages;
for (var i = 0; i < pages.length; i++) {
revisions = revisions.concat(pages[i].revisions);
}
for (i = 0; i < revisions.length; i++) {
if (revisions[i].revid == navpop.diffData.oldRev.revid) {
navpop.diffData.oldRev.revision = revisions[i];
} else if (revisions[i].revid == navpop.diffData.newRev.revid) {
navpop.diffData.newRev.revision = revisions[i];
}
}
} catch (someError) {
errlog('Could not get diff');
}
insertDiff(navpop);
}
function rmBoringLines(a, b, context) {
if (typeof context == 'undefined') {
context = 2;
}
// this is fairly slow... i think it's quicker than doing a word-based diff from the off, though
const aa = [],
aaa = [];
const bb = [],
bbb = [];
let i, j;
// first, gather all disconnected nodes in a and all crossing nodes in a and b
for (i = 0; i < a.length; ++i) {
if (!a[i].paired) {
aa[i] = 1;
} else if (countCrossings(b, a, i, true)) {
aa[i] = 1;
bb[a[i].row] = 1;
}
}
// pick up remaining disconnected nodes in b
for (i = 0; i < b.length; ++i) {
if (bb[i] == 1) {
continue;
}
if (!b[i].paired) {
bb[i] = 1;
}
}
// another pass to gather context: we want the neighbours of included nodes which are not
// yet included we have to add in partners of these nodes, but we don't want to add context
// for *those* nodes in the next pass
for (i = 0; i < b.length; ++i) {
if (bb[i] == 1) {
for (j = Math.max(0, i - context); j < Math.min(b.length, i + context); ++j) {
if (!bb[j]) {
bb[j] = 1;
aa[b[j].row] = 0.5;
}
}
}
}
for (i = 0; i < a.length; ++i) {
if (aa[i] == 1) {
for (j = Math.max(0, i - context); j < Math.min(a.length, i + context); ++j) {
if (!aa[j]) {
aa[j] = 1;
bb[a[j].row] = 0.5;
}
}
}
}
for (i = 0; i < bb.length; ++i) {
if (bb[i] > 0) {
// it's a row we need
if (b[i].paired) {
bbb.push(b[i].text);
} // joined; partner should be in aa
else {
bbb.push(b[i]);
}
}
}
for (i = 0; i < aa.length; ++i) {
if (aa[i] > 0) {
// it's a row we need
if (a[i].paired) {
aaa.push(a[i].text);
} // joined; partner should be in aa
else {
aaa.push(a[i]);
}
}
}
return { a: aaa, b: bbb };
}
function stripOuterCommonLines(a, b, context) {
let i = 0;
while (i < a.length && i < b.length && a[i] == b[i]) {
++i;
}
let j = a.length - 1;
let k = b.length - 1;
while (j >= 0 && k >= 0 && a[j] == b[k]) {
--j;
--k;
}
return {
a: a.slice(Math.max(0, i - 1 - context), Math.min(a.length + 1, j + context + 1)),
b: b.slice(Math.max(0, i - 1 - context), Math.min(b.length + 1, k + context + 1)),
};
}
function insertDiff(navpop) {
// for speed reasons, we first do a line-based diff, discard stuff that seems boring, then
// do a word-based diff
// FIXME: sometimes this gives misleading diffs as distant chunks are squashed together
let oldlines = navpop.diffData.oldRev.revision.slots.main.content.split('\n');
let newlines = navpop.diffData.newRev.revision.slots.main.content.split('\n');
let inner = stripOuterCommonLines(oldlines, newlines, getValueOf('popupDiffContextLines'));
oldlines = inner.a;
newlines = inner.b;
let truncated = false;
getValueOf('popupDiffMaxLines');
if (
oldlines.length > pg.option.popupDiffMaxLines ||
newlines.length > pg.option.popupDiffMaxLines
) {
// truncate
truncated = true;
inner = stripOuterCommonLines(
oldlines.slice(0, pg.option.popupDiffMaxLines),
newlines.slice(0, pg.option.popupDiffMaxLines),
pg.option.popupDiffContextLines
);
oldlines = inner.a;
newlines = inner.b;
}
const lineDiff = diff(oldlines, newlines);
const lines2 = rmBoringLines(lineDiff.o, lineDiff.n);
const oldlines2 = lines2.a;
const newlines2 = lines2.b;
const simpleSplit = !String.prototype.parenSplit.isNative;
let html = '<hr />';
if (getValueOf('popupDiffDates')) {
html += diffDatesTable(navpop);
html += '<hr />';
}
html += shortenDiffString(
diffString(oldlines2.join('\n'), newlines2.join('\n'), simpleSplit),
getValueOf('popupDiffContextCharacters')
).join('<hr />');
setPopupTipsAndHTML(
html.split('\n').join('<br>') +
(truncated ?
'<hr /><b>' + popupString('Diff truncated for performance reasons') + '</b>' :
''),
'popupPreview',
navpop.idNumber
);
}
function diffDatesTable(navpop) {
let html = '<table class="popup_diff_dates">';
html += diffDatesTableRow(navpop.diffData.newRev.revision, tprintf('New revision'));
html += diffDatesTableRow(navpop.diffData.oldRev.revision, tprintf('Old revision'));
html += '</table>';
return html;
}
function diffDatesTableRow(revision, label) {
let txt = '';
const lastModifiedDate = new Date(revision.timestamp);
txt = formattedDateTime(lastModifiedDate);
const revlink = generalLink({
url: mw.config.get('wgScript') + '?oldid=' + revision.revid,
text: label,
title: label,
});
return simplePrintf('<tr><td>%s</td><td>%s</td></tr>', [revlink, txt]);
}
// ENDFILE: diffpreview.js
// STARTFILE: links.js
/////////////////////
// LINK GENERATION //
/////////////////////
// titledDiffLink --> titledWikiLink --> generalLink
// wikiLink --> titledWikiLink --> generalLink
// editCounterLink --> generalLink
// TODO Make these functions return Element objects, not just raw HTML strings.
function titledDiffLink(l) {
// article, text, title, from, to) {
return titledWikiLink({
article: l.article,
action: l.to + '&oldid=' + l.from,
newWin: l.newWin,
noPopup: l.noPopup,
text: l.text,
title: l.title,
/* hack: no oldid here */
actionName: 'diff',
});
}
function wikiLink(l) {
//{article:article, action:action, text:text, oldid, newid}) {
if (
!(typeof l.article == typeof {} && typeof l.action == typeof '' && typeof l.text == typeof '')
) {
return null;
}
if (typeof l.oldid == 'undefined') {
l.oldid = null;
}
const savedOldid = l.oldid;
if (!/^(edit|view|revert|render)$|^raw/.test(l.action)) {
l.oldid = null;
}
let hint = popupString(l.action + 'Hint'); // revertHint etc etc etc
const oldidData = [l.oldid, safeDecodeURI(l.article)];
let revisionString = tprintf('revision %s of %s', oldidData);
log('revisionString=' + revisionString);
switch (l.action) {
case 'edit§ion=new':
hint = popupString('newSectionHint');
break;
case 'edit&undo=':
if (l.diff && l.diff != 'prev' && savedOldid) {
l.action += l.diff + '&undoafter=' + savedOldid;
} else if (savedOldid) {
l.action += savedOldid;
}
hint = popupString('undoHint');
break;
case 'raw&ctype=text/css':
hint = popupString('rawHint');
break;
case 'revert':
var p = parseParams(pg.current.link.href);
l.action =
'edit&autoclick=wpSave&actoken=' +
autoClickToken() +
'&autoimpl=' +
popupString('autoedit_version') +
'&autosummary=' +
revertSummary(l.oldid, p.diff);
if (p.diff == 'prev') {
l.action += '&direction=prev';
revisionString = tprintf('the revision prior to revision %s of %s', oldidData);
}
if (getValueOf('popupRevertSummaryPrompt')) {
l.action += '&autosummaryprompt=true';
}
if (getValueOf('popupMinorReverts')) {
l.action += '&autominor=true';
}
log('revisionString is now ' + revisionString);
break;
case 'nullEdit':
l.action =
'edit&autoclick=wpSave&actoken=' +
autoClickToken() +
'&autoimpl=' +
popupString('autoedit_version') +
'&autosummary=null';
break;
case 'historyfeed':
l.action = 'history&feed=rss';
break;
case 'markpatrolled':
l.action = 'markpatrolled&rcid=' + l.rcid;
}
if (hint) {
if (l.oldid) {
hint = simplePrintf(hint, [revisionString]);
} else {
hint = simplePrintf(hint, [safeDecodeURI(l.article)]);
}
} else {
hint = safeDecodeURI(l.article + '&action=' + l.action) + l.oldid ? '&oldid=' + l.oldid : '';
}
return titledWikiLink({
article: l.article,
action: l.action,
text: l.text,
newWin: l.newWin,
title: hint,
oldid: l.oldid,
noPopup: l.noPopup,
onclick: l.onclick,
});
}
function revertSummary(oldid, diff) {
let ret = '';
if (diff == 'prev') {
ret = getValueOf('popupQueriedRevertToPreviousSummary');
} else {
ret = getValueOf('popupQueriedRevertSummary');
}
return ret + '&autorv=' + oldid;
}
function titledWikiLink(l) {
// possible properties of argument:
// article, action, text, title, oldid, actionName, className, noPopup
// oldid = null is fine here
// article and action are mandatory args
if (typeof l.article == 'undefined' || typeof l.action == 'undefined') {
errlog('got undefined article or action in titledWikiLink');
return null;
}
const base = pg.wiki.titlebase + l.article.urlString();
let url = base;
if (typeof l.actionName == 'undefined' || !l.actionName) {
l.actionName = 'action';
}
// no need to add &action=view, and this confuses anchors
if (l.action != 'view') {
url = base + '&' + l.actionName + '=' + l.action;
}
if (typeof l.oldid != 'undefined' && l.oldid) {
url += '&oldid=' + l.oldid;
}
let cssClass = pg.misc.defaultNavlinkClassname;
if (typeof l.className != 'undefined' && l.className) {
cssClass = l.className;
}
return generalNavLink({
url: url,
newWin: l.newWin,
title: typeof l.title != 'undefined' ? l.title : null,
text: typeof l.text != 'undefined' ? l.text : null,
className: cssClass,
noPopup: l.noPopup,
onclick: l.onclick,
});
}
pg.fn.getLastContrib = function getLastContrib(wikipage, newWin) {
getHistoryInfo(wikipage, (x) => {
processLastContribInfo(x, { page: wikipage, newWin: newWin });
});
};
function processLastContribInfo(info, stuff) {
if (!info.edits || !info.edits.length) {
alert('Popups: an odd thing happened. Please retry.');
return;
}
if (!info.firstNewEditor) {
alert(
tprintf('Only found one editor: %s made %s edits', [
info.edits[0].editor,
info.edits.length,
])
);
return;
}
const newUrl =
pg.wiki.titlebase +
new Title(stuff.page).urlString() +
'&diff=cur&oldid=' +
info.firstNewEditor.oldid;
displayUrl(newUrl, stuff.newWin);
}
pg.fn.getDiffSinceMyEdit = function getDiffSinceMyEdit(wikipage, newWin) {
getHistoryInfo(wikipage, (x) => {
processDiffSinceMyEdit(x, { page: wikipage, newWin: newWin });
});
};
function processDiffSinceMyEdit(info, stuff) {
if (!info.edits || !info.edits.length) {
alert('Popups: something fishy happened. Please try again.');
return;
}
const friendlyName = stuff.page.split('_').join(' ');
if (!info.myLastEdit) {
alert(
tprintf("Couldn't find an edit by %s\nin the last %s edits to\n%s", [
info.userName,
getValueOf('popupHistoryLimit'),
friendlyName,
])
);
return;
}
if (info.myLastEdit.index === 0) {
alert(
tprintf('%s seems to be the last editor to the page %s', [info.userName, friendlyName])
);
return;
}
const newUrl =
pg.wiki.titlebase +
new Title(stuff.page).urlString() +
'&diff=cur&oldid=' +
info.myLastEdit.oldid;
displayUrl(newUrl, stuff.newWin);
}
function displayUrl(url, newWin) {
if (newWin) {
window.open(url);
} else {
document.location = url;
}
}
pg.fn.purgePopups = function purgePopups() {
processAllPopups(true);
setupCache(); // deletes all cached items (not browser cached, though...)
pg.option = {};
abortAllDownloads();
};
function processAllPopups(nullify, banish) {
for (let i = 0; pg.current.links && i < pg.current.links.length; ++i) {
if (!pg.current.links[i].navpopup) {
continue;
}
if (nullify || banish) {
pg.current.links[i].navpopup.banish();
}
pg.current.links[i].simpleNoMore = false;
if (nullify) {
pg.current.links[i].navpopup = null;
}
}
}
pg.fn.disablePopups = function disablePopups() {
processAllPopups(false, true);
setupTooltips(null, true);
};
pg.fn.togglePreviews = function togglePreviews() {
processAllPopups(true, true);
pg.option.simplePopups = !pg.option.simplePopups;
abortAllDownloads();
};
function magicWatchLink(l) {
//Yuck!! Would require a thorough redesign to add this as a click event though ...
l.onclick = simplePrintf("pg.fn.modifyWatchlist('%s','%s');return false;", [
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
this.id,
]);
return wikiLink(l);
}
pg.fn.modifyWatchlist = function modifyWatchlist(title, action) {
const reqData = {
action: 'watch',
formatversion: 2,
titles: title,
uselang: mw.config.get('wgUserLanguage'),
};
if (action === 'unwatch') {
reqData.unwatch = true;
}
// Load the Addedwatchtext or Removedwatchtext message and show it
const mwTitle = mw.Title.newFromText(title);
let messageName;
if (mwTitle && mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1) {
messageName = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
} else {
messageName = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
}
$.when(
getMwApi().postWithToken('watch', reqData),
getMwApi().loadMessagesIfMissing([messageName])
).done(() => {
mw.notify(mw.message(messageName, title).parseDom());
});
};
function magicHistoryLink(l) {
// FIXME use onclick change href trick to sort this out instead of window.open
let jsUrl = '',
title = '',
onClick = '';
switch (l.id) {
case 'lastContrib':
onClick = simplePrintf("pg.fn.getLastContrib('%s',%s)", [
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
l.newWin,
]);
title = popupString('lastContribHint');
break;
case 'sinceMe':
onClick = simplePrintf("pg.fn.getDiffSinceMyEdit('%s',%s)", [
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
l.newWin,
]);
title = popupString('sinceMeHint');
break;
}
jsUrl = 'javascript:' + onClick; // jshint ignore:line
onClick += ';return false;';
return generalNavLink({
url: jsUrl,
newWin: false, // can't have new windows with JS links, I think
title: title,
text: l.text,
noPopup: l.noPopup,
onclick: onClick,
});
}
function popupMenuLink(l) {
const jsUrl = simplePrintf('javascript:pg.fn.%s()', [l.id]); // jshint ignore:line
const title = popupString(simplePrintf('%sHint', [l.id]));
const onClick = simplePrintf('pg.fn.%s();return false;', [l.id]);
return generalNavLink({
url: jsUrl,
newWin: false,
title: title,
text: l.text,
noPopup: l.noPopup,
onclick: onClick,
});
}
function specialLink(l) {
// properties: article, specialpage, text, sep
if (typeof l.specialpage == 'undefined' || !l.specialpage) {
return null;
}
const base =
pg.wiki.titlebase +
mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +
':' +
l.specialpage;
if (typeof l.sep == 'undefined' || l.sep === null) {
l.sep = '&target=';
}
let article = l.article.urlString({
keepSpaces: l.specialpage == 'Search',
});
let hint = popupString(l.specialpage + 'Hint');
switch (l.specialpage) {
case 'Log':
switch (l.sep) {
case '&user=':
hint = popupString('userLogHint');
break;
case '&type=block&page=':
hint = popupString('blockLogHint');
break;
case '&page=':
hint = popupString('pageLogHint');
break;
case '&type=protect&page=':
hint = popupString('protectLogHint');
break;
case '&type=delete&page=':
hint = popupString('deleteLogHint');
break;
default:
log('Unknown log type, sep=' + l.sep);
hint = 'Missing hint (FIXME)';
}
break;
case 'PrefixIndex':
article += '/';
break;
}
if (hint) {
hint = simplePrintf(hint, [safeDecodeURI(l.article)]);
} else {
hint = safeDecodeURI(l.specialpage + ':' + l.article);
}
const url = base + l.sep + article;
return generalNavLink({
url: url,
title: hint,
text: l.text,
newWin: l.newWin,
noPopup: l.noPopup,
});
}
/**
* Builds a link from a object representing a link
*
* @param {Object} link
* @param {string} link.url URL
* @param {string} link.text The text to show for a link
* @param {string} link.title Title of the link, this shows up
* when you hover over the link
* @param {boolean} link.newWin Should open in a new Window
* @param {number} link.noPopup Should nest new popups from link (0 or 1)
* @param {string} link.onclick
* @return {string|null} null if no url is given
*/
function generalLink(link) {
if (typeof link.url == 'undefined') {
return null;
}
const elem = document.createElement( 'a' );
elem.href = link.url;
elem.title = link.title;
// The onclick event adds raw JS in textual form to the HTML.
// TODO: We should look into removing this, and/or auditing what gets sent.
elem.setAttribute( 'onclick', link.onclick );
if ( link.noPopup ) {
elem.setAttribute('noPopup', '1' );
}
let newWin;
if (typeof link.newWin == 'undefined' || link.newWin === null) {
newWin = getValueOf('popupNewWindows');
} else {
newWin = link.newWin;
}
if (newWin) {
elem.target = '_blank';
}
if (link.className) {
elem.className = link.className;
}
elem.innerText = pg.unescapeQuotesHTML(link.text);
return elem.outerHTML;
}
function appendParamsToLink(linkstr, params) {
const sp = linkstr.parenSplit(/(href="[^"]+?)"/i);
if (sp.length < 2) {
return null;
}
let ret = sp.shift() + sp.shift();
ret += '&' + params + '"';
ret += sp.join('');
return ret;
}
function changeLinkTargetLink(x) {
// newTarget, text, hint, summary, clickButton, minor, title (optional), alsoChangeLabel {
if (x.newTarget) {
log('changeLinkTargetLink: newTarget=' + x.newTarget);
}
if (x.oldTarget !== decodeURIComponent(x.oldTarget)) {
log('This might be an input problem: ' + x.oldTarget);
}
// FIXME: first character of page title as well as namespace should be case insensitive
// eg [[:category:X1]] and [[:Category:X1]] are equivalent
// this'll break if charAt(0) is nasty
const cA = mw.util.escapeRegExp(x.oldTarget);
let chs = cA.charAt(0).toUpperCase();
chs = '[' + chs + chs.toLowerCase() + ']';
let currentArticleRegexBit = chs + cA.substring(1);
currentArticleRegexBit = currentArticleRegexBit
.split(/(?:[_ ]+|%20)/g)
.join('(?:[_ ]+|%20)')
.split('\\(')
.join('(?:%28|\\()')
.split('\\)')
.join('(?:%29|\\))'); // why does this need to match encoded strings ? links in the document ?
// leading and trailing space should be ignored, and anchor bits optional:
currentArticleRegexBit = '\\s*(' + currentArticleRegexBit + '(?:#[^\\[\\|]*)?)\\s*';
// e.g. Computer (archaic) -> \s*([Cc]omputer[_ ](?:%2528|\()archaic(?:%2528|\)))\s*
// autoedit=s~\[\[([Cc]ad)\]\]~[[Computer-aided%20design|$1]]~g;s~\[\[([Cc]AD)[|]~[[Computer-aided%20design|~g
const title = x.title || mw.config.get('wgPageName').split('_').join(' ');
const lk = titledWikiLink({
article: new Title(title),
newWin: x.newWin,
action: 'edit',
text: x.text,
title: x.hint,
className: 'popup_change_title_link',
});
let cmd = '';
if (x.newTarget) {
// escape '&' and other nasties
const t = x.newTarget;
const s = mw.util.escapeRegExp(x.newTarget);
if (x.alsoChangeLabel) {
cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + ']]~g;';
cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~g;';
cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + ']]~g';
} else {
cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + '|$1]]~g;';
cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~g;';
cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + ']]~g';
}
} else {
cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~$1~g;';
cmd += 's~\\[\\[' + currentArticleRegexBit + '[|](.*?)\\]\\]~$2~g';
}
// Build query
cmd = 'autoedit=' + encodeURIComponent(cmd);
cmd +=
'&autoclick=' +
encodeURIComponent(x.clickButton) +
'&actoken=' +
encodeURIComponent(autoClickToken());
cmd += x.minor === null ? '' : '&autominor=' + encodeURIComponent(x.minor);
cmd += x.watch === null ? '' : '&autowatch=' + encodeURIComponent(x.watch);
cmd += '&autosummary=' + encodeURIComponent(x.summary);
cmd += '&autoimpl=' + encodeURIComponent(popupString('autoedit_version'));
return appendParamsToLink(lk, cmd);
}
function redirLink(redirMatch, article) {
// NB redirMatch is in wikiText
let ret = '';
if (getValueOf('popupAppendRedirNavLinks') && getValueOf('popupNavLinks')) {
ret += '<hr />';
if (getValueOf('popupFixRedirs') && typeof autoEdit != 'undefined' && autoEdit) {
ret += popupString('Redirects to: (Fix ');
log('redirLink: newTarget=' + redirMatch);
ret += addPopupShortcut(
changeLinkTargetLink({
newTarget: redirMatch,
text: popupString('target'),
hint: popupString('Fix this redirect, changing just the link target'),
summary: simplePrintf(getValueOf('popupFixRedirsSummary'), [
article.toString(),
redirMatch,
]),
oldTarget: article.toString(),
clickButton: getValueOf('popupRedirAutoClick'),
minor: true,
watch: getValueOf('popupWatchRedirredPages'),
}),
'R'
);
ret += popupString(' or ');
ret += addPopupShortcut(
changeLinkTargetLink({
newTarget: redirMatch,
text: popupString('target & label'),
hint: popupString('Fix this redirect, changing the link target and label'),
summary: simplePrintf(getValueOf('popupFixRedirsSummary'), [
article.toString(),
redirMatch,
]),
oldTarget: article.toString(),
clickButton: getValueOf('popupRedirAutoClick'),
minor: true,
watch: getValueOf('popupWatchRedirredPages'),
alsoChangeLabel: true,
}),
'R'
);
ret += popupString(')');
} else {
ret += popupString('Redirects') + popupString(' to ');
}
return ret;
} else {
return (
'<br> ' +
popupString('Redirects') +
popupString(' to ') +
titledWikiLink({
article: new Title().fromWikiText(redirMatch),
action: 'view' /* FIXME: newWin */,
text: safeDecodeURI(redirMatch),
title: popupString('Bypass redirect'),
})
);
}
}
function arinLink(l) {
if (!saneLinkCheck(l)) {
return null;
}
if (!l.article.isIpUser() || !pg.wiki.wikimedia) {
return null;
}
const uN = l.article.userName();
return generalNavLink({
url: 'http://ws.arin.net/cgi-bin/whois.pl?queryinput=' + encodeURIComponent(uN),
newWin: l.newWin,
title: tprintf('Look up %s in ARIN whois database', [uN]),
text: l.text,
noPopup: 1,
});
}
function toolDbName(cookieStyle) {
let ret = mw.config.get('wgDBname');
if (!cookieStyle) {
ret += '_p';
}
return ret;
}
function saneLinkCheck(l) {
if (typeof l.article != typeof {} || typeof l.text != typeof '') {
return false;
}
return true;
}
function editCounterLink(l) {
if (!saneLinkCheck(l)) {
return null;
}
if (!pg.wiki.wikimedia) {
return null;
}
const uN = l.article.userName();
const tool = getValueOf('popupEditCounterTool');
let url;
const defaultToolUrl = 'https://xtools.wmflabs.org/ec?user=$1&project=$2.$3&uselang=' + mw.config.get('wgUserLanguage');
switch (tool) {
case 'custom':
url = simplePrintf(getValueOf('popupEditCounterUrl'), [
encodeURIComponent(uN),
toolDbName(),
]);
break;
case 'soxred': // no longer available
case 'kate': // no longer available
case 'interiot': // no longer available
/* fall through */
case 'supercount':
default:
var theWiki = pg.wiki.hostname.split('.');
url = simplePrintf(defaultToolUrl, [encodeURIComponent(uN), theWiki[0], theWiki[1]]);
}
return generalNavLink({
url: url,
title: tprintf('editCounterLinkHint', [uN]),
newWin: l.newWin,
text: l.text,
noPopup: 1,
});
}
function globalSearchLink(l) {
if (!saneLinkCheck(l)) {
return null;
}
const base = 'https://global-search.toolforge.org/?uselang=' + mw.config.get('wgUserLanguage') + '&q=';
const article = l.article.urlString({ keepSpaces: true });
return generalNavLink({
url: base + article,
newWin: l.newWin,
title: tprintf('globalSearchHint', [safeDecodeURI(l.article)]),
text: l.text,
noPopup: 1,
});
}
function googleLink(l) {
if (!saneLinkCheck(l)) {
return null;
}
const base = 'https://www.google.com/search?q=';
const article = l.article.urlString({ keepSpaces: true });
return generalNavLink({
url: base + '%22' + article + '%22',
newWin: l.newWin,
title: tprintf('googleSearchHint', [safeDecodeURI(l.article)]),
text: l.text,
noPopup: 1,
});
}
function editorListLink(l) {
if (!saneLinkCheck(l)) {
return null;
}
const article = l.article.articleFromTalkPage() || l.article;
const url =
'https://xtools.wmflabs.org/articleinfo/' +
encodeURI(pg.wiki.hostname) +
'/' +
article.urlString() +
'?uselang=' +
mw.config.get('wgUserLanguage');
return generalNavLink({
url: url,
title: tprintf('editorListHint', [article]),
newWin: l.newWin,
text: l.text,
noPopup: 1,
});
}
function generalNavLink(l) {
l.className = l.className === null ? 'popupNavLink' : l.className;
return generalLink(l);
}
//////////////////////////////////////////////////
// magic history links
//
function getHistoryInfo(wikipage, whatNext) {
log('getHistoryInfo');
getHistory(
wikipage,
whatNext ?
(d) => {
whatNext(processHistory(d));
} :
processHistory
);
}
// FIXME eliminate pg.idNumber ... how? :-(
function getHistory(wikipage, onComplete) {
log('getHistory');
const url =
pg.wiki.apiwikibase +
'?format=json&formatversion=2&action=query&prop=revisions&titles=' +
new Title(wikipage).urlString() +
'&rvlimit=' +
getValueOf('popupHistoryLimit');
log('getHistory: url=' + url);
return startDownload(url, pg.idNumber + 'history', onComplete);
}
function processHistory(download) {
const jsobj = getJsObj(download.data);
try {
const revisions = anyChild(jsobj.query.pages).revisions;
const edits = [];
for (let i = 0; i < revisions.length; ++i) {
edits.push({ oldid: revisions[i].revid, editor: revisions[i].user });
}
log('processed ' + edits.length + ' edits');
return finishProcessHistory(edits, mw.config.get('wgUserName'));
} catch (someError) {
log('Something went wrong with JSON business');
return finishProcessHistory([]);
}
}
function finishProcessHistory(edits, userName) {
const histInfo = {};
histInfo.edits = edits;
histInfo.userName = userName;
for (let i = 0; i < edits.length; ++i) {
if (typeof histInfo.myLastEdit === 'undefined' && userName && edits[i].editor == userName) {
histInfo.myLastEdit = {
index: i,
oldid: edits[i].oldid,
previd: i === 0 ? null : edits[i - 1].oldid,
};
}
if (typeof histInfo.firstNewEditor === 'undefined' && edits[i].editor != edits[0].editor) {
histInfo.firstNewEditor = {
index: i,
oldid: edits[i].oldid,
previd: i === 0 ? null : edits[i - 1].oldid,
};
}
}
//pg.misc.historyInfo=histInfo;
return histInfo;
}
// ENDFILE: links.js
// STARTFILE: options.js
//////////////////////////////////////////////////
// options
// check for existing value, else use default
function defaultize(x) {
if (pg.option[x] === null || typeof pg.option[x] == 'undefined') {
if (typeof window[x] != 'undefined') {
pg.option[x] = window[x];
} else {
pg.option[x] = pg.optionDefault[x];
}
}
}
function newOption(x, def) {
pg.optionDefault[x] = def;
}
function setDefault(x, def) {
return newOption(x, def);
}
function getValueOf(varName) {
defaultize(varName);
return pg.option[varName];
}
/*eslint-disable */
function useDefaultOptions() {
// for testing
for (var p in pg.optionDefault) {
pg.option[p] = pg.optionDefault[p];
if (typeof window[p] != 'undefined') {
delete window[p];
}
}
}
/*eslint-enable */
function setOptions() {
// user-settable parameters and defaults
let userIsSysop = false;
if (mw.config.get('wgUserGroups')) {
for (let g = 0; g < mw.config.get('wgUserGroups').length; ++g) {
if (mw.config.get('wgUserGroups')[g] == 'sysop') {
userIsSysop = true;
}
}
}
// Basic options
newOption('popupDelay', 0.5);
newOption('popupHideDelay', 0.5);
newOption('simplePopups', false);
newOption('popupStructure', 'shortmenus'); // see later - default for popupStructure is 'original' if simplePopups is true
newOption('popupActionsMenu', true);
newOption('popupSetupMenu', true);
newOption('popupAdminLinks', userIsSysop);
newOption('popupShortcutKeys', false);
newOption('popupHistoricalLinks', true);
newOption('popupOnlyArticleLinks', true);
newOption('removeTitles', true);
newOption('popupMaxWidth', 350);
newOption('popupSimplifyMainLink', true);
newOption('popupAppendRedirNavLinks', true);
newOption('popupTocLinks', false);
newOption('popupSubpopups', true);
newOption('popupDragHandle', false /* 'popupTopLinks'*/);
newOption('popupLazyPreviews', true);
newOption('popupLazyDownloads', true);
newOption('popupAllDabsStubs', false);
newOption('popupDebugging', false);
newOption('popupActiveNavlinks', true);
newOption('popupModifier', false); // ctrl, shift, alt or meta
newOption('popupModifierAction', 'enable'); // or 'disable'
newOption('popupDraggable', true);
newOption('popupReview', false);
newOption('popupLocale', false);
newOption('popupDateTimeFormatterOptions', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
newOption('popupDateFormatterOptions', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
newOption('popupTimeFormatterOptions', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
// images
newOption('popupImages', true);
newOption('imagePopupsForImages', true);
newOption('popupNeverGetThumbs', false);
//newOption('popupImagesToggleSize', true);
newOption('popupThumbAction', 'imagepage'); //'sizetoggle');
newOption('popupImageSize', 60);
newOption('popupImageSizeLarge', 200);
// redirs, dabs, reversion
newOption('popupFixRedirs', false);
newOption('popupRedirAutoClick', 'wpDiff');
newOption('popupFixDabs', false);
newOption('popupDabsAutoClick', 'wpDiff');
newOption('popupRevertSummaryPrompt', false);
newOption('popupMinorReverts', false);
newOption('popupRedlinkRemoval', false);
newOption('popupRedlinkAutoClick', 'wpDiff');
newOption('popupWatchDisambiggedPages', null);
newOption('popupWatchRedirredPages', null);
newOption('popupDabWiktionary', 'last');
// navlinks
newOption('popupNavLinks', true);
newOption('popupNavLinkSeparator', ' ⋅ ');
newOption('popupLastEditLink', true);
newOption('popupEditCounterTool', 'supercount');
newOption('popupEditCounterUrl', '');
// previews etc
newOption('popupPreviews', true);
newOption('popupSummaryData', true);
newOption('popupMaxPreviewSentences', 5);
newOption('popupMaxPreviewCharacters', 600);
newOption('popupLastModified', true);
newOption('popupPreviewKillTemplates', true);
newOption('popupPreviewRawTemplates', true);
newOption('popupPreviewFirstParOnly', true);
newOption('popupPreviewCutHeadings', true);
newOption('popupPreviewButton', false);
newOption('popupPreviewButtonEvent', 'click');
// diffs
newOption('popupPreviewDiffs', true);
newOption('popupDiffMaxLines', 100);
newOption('popupDiffContextLines', 2);
newOption('popupDiffContextCharacters', 40);
newOption('popupDiffDates', true);
newOption('popupDiffDatePrinter', 'toLocaleString'); // no longer in use
// edit summaries. God, these are ugly.
newOption('popupReviewedSummary', popupString('defaultpopupReviewedSummary'));
newOption('popupFixDabsSummary', popupString('defaultpopupFixDabsSummary'));
newOption('popupExtendedRevertSummary', popupString('defaultpopupExtendedRevertSummary'));
newOption('popupRevertSummary', popupString('defaultpopupRevertSummary'));
newOption('popupRevertToPreviousSummary', popupString('defaultpopupRevertToPreviousSummary'));
newOption('popupQueriedRevertSummary', popupString('defaultpopupQueriedRevertSummary'));
newOption(
'popupQueriedRevertToPreviousSummary',
popupString('defaultpopupQueriedRevertToPreviousSummary')
);
newOption('popupFixRedirsSummary', popupString('defaultpopupFixRedirsSummary'));
newOption('popupRedlinkSummary', popupString('defaultpopupRedlinkSummary'));
newOption('popupRmDabLinkSummary', popupString('defaultpopupRmDabLinkSummary'));
// misc
newOption('popupHistoryLimit', 50);
newOption('popupFilters', [
popupFilterStubDetect,
popupFilterDisambigDetect,
popupFilterPageSize,
popupFilterCountLinks,
popupFilterCountImages,
popupFilterCountCategories,
popupFilterLastModified,
popupFilterWikibaseItem,
]);
newOption('extraPopupFilters', []);
newOption('popupOnEditSelection', 'cursor');
newOption('popupPreviewHistory', true);
newOption('popupImageLinks', true);
newOption('popupCategoryMembers', true);
newOption('popupUserInfo', true);
newOption('popupHistoryPreviewLimit', 25);
newOption('popupContribsPreviewLimit', 25);
newOption('popupRevDelUrl', '//en.wikipedia.org/wiki/Wikipedia:Revision_deletion');
newOption('popupShowGender', true);
// new windows
newOption('popupNewWindows', false);
newOption('popupLinksNewWindow', { lastContrib: true, sinceMe: true });
// regexps
newOption(
'popupDabRegexp',
'disambiguation\\}\\}|\\{\\{\\s*(d(ab|isamb(ig(uation)?)?)|(((geo|hn|road?|school|number)dis)|[234][lc][acw]|(road|ship)index))\\s*(\\|[^}]*)?\\}\\}|is a .*disambiguation.*page'
);
newOption('popupAnchorRegexp', 'anchors?'); //how to identify an anchors template
newOption('popupStubRegexp', '(sect)?stub[}][}]|This .*-related article is a .*stub');
newOption(
'popupImageVarsRegexp',
'image|image_(?:file|skyline|name|flag|seal)|cover|badge|logo'
);
}
// ENDFILE: options.js
// STARTFILE: strings.js
//////////////////////////////////////////////////
// Translatable strings
//////////////////////////////////////////////////
//
// See instructions at
// https://en.wikipedia.org/wiki/Wikipedia:Tools/Navigation_popups/Translation
pg.string = {
/////////////////////////////////////
// summary data, searching etc.
/////////////////////////////////////
article: 'article',
category: 'category',
categories: 'categories',
image: 'image',
images: 'images',
stub: 'stub',
'section stub': 'section stub',
'Empty page': 'Empty page',
kB: 'kB',
bytes: 'bytes',
day: 'day',
days: 'days',
hour: 'hour',
hours: 'hours',
minute: 'minute',
minutes: 'minutes',
second: 'second',
seconds: 'seconds',
week: 'week',
weeks: 'weeks',
search: 'search',
SearchHint: 'Find English Wikipedia articles containing %s',
web: 'web',
global: 'global',
globalSearchHint: 'Search across Wikipedias in different languages for %s',
googleSearchHint: 'Google for %s',
/////////////////////////////////////
// article-related actions and info
// (some actions also apply to user pages)
/////////////////////////////////////
actions: 'actions', ///// view articles and view talk
popupsMenu: 'popups',
togglePreviewsHint: 'Toggle preview generation in popups on this page',
'enable previews': 'enable previews',
'disable previews': 'disable previews',
'toggle previews': 'toggle previews',
'show preview': 'show preview',
reset: 'reset',
'more...': 'more...',
disable: 'disable popups',
disablePopupsHint: 'Disable popups on this page. Reload page to re-enable.',
historyfeedHint: 'RSS feed of recent changes to this page',
purgePopupsHint: 'Reset popups, clearing all cached popup data.',
PopupsHint: 'Reset popups, clearing all cached popup data.',
spacebar: 'space',
view: 'view',
'view article': 'view article',
viewHint: 'Go to %s',
talk: 'talk',
'talk page': 'talk page',
'this revision': 'this revision',
'revision %s of %s': 'revision %s of %s',
'Revision %s of %s': 'Revision %s of %s',
'the revision prior to revision %s of %s': 'the revision prior to revision %s of %s',
'Toggle image size': 'Click to toggle image size',
del: 'del', ///// delete, protect, move
delete: 'delete',
deleteHint: 'Delete %s',
undeleteShort: 'un',
UndeleteHint: 'Show the deletion history for %s',
protect: 'protect',
protectHint: 'Restrict editing rights to %s',
unprotectShort: 'un',
unprotectHint: 'Allow %s to be edited by anyone again',
'send thanks': 'send thanks',
ThanksHint: 'Send a thank you notification to this user',
move: 'move',
'move page': 'move page',
MovepageHint: 'Change the title of %s',
edit: 'edit', ///// edit articles and talk
'edit article': 'edit article',
editHint: 'Change the content of %s',
'edit talk': 'edit talk',
new: 'new',
'new topic': 'new topic',
newSectionHint: 'Start a new section on %s',
'null edit': 'null edit',
nullEditHint: 'Submit an edit to %s, making no changes ',
hist: 'hist', ///// history, diffs, editors, related
history: 'history',
historyHint: 'List the changes made to %s',
'History preview failed': 'History preview failed :-(',
last: 'prev', // For labelling the previous revision in history pages; the key is "last" for backwards compatibility
lastEdit: 'lastEdit',
'mark patrolled': 'mark patrolled',
markpatrolledHint: 'Mark this edit as patrolled',
'Could not marked this edit as patrolled': 'Could not marked this edit as patrolled',
'show last edit': 'most recent edit',
'Show the last edit': 'Show the effects of the most recent change',
lastContrib: 'lastContrib',
'last set of edits': 'latest edits',
lastContribHint: 'Show the net effect of changes made by the last editor',
cur: 'cur',
diffCur: 'diffCur',
'Show changes since revision %s': 'Show changes since revision %s',
'%s old': '%s old', // as in 4 weeks old
oldEdit: 'oldEdit',
purge: 'purge',
purgeHint: 'Demand a fresh copy of %s',
raw: 'source',
rawHint: 'Download the source of %s',
render: 'simple',
renderHint: 'Show a plain HTML version of %s',
'Show the edit made to get revision': 'Show the edit made to get revision',
sinceMe: 'sinceMe',
'changes since mine': 'diff my edit',
sinceMeHint: 'Show changes since my last edit',
"Couldn't find an edit by %s\nin the last %s edits to\n%s":
"Couldn't find an edit by %s\nin the last %s edits to\n%s",
eds: 'eds',
editors: 'editors',
editorListHint: 'List the users who have edited %s',
related: 'related',
relatedChanges: 'relatedChanges',
'related changes': 'related changes',
RecentchangeslinkedHint: 'Show changes in articles related to %s',
editOld: 'editOld', ///// edit old version, or revert
rv: 'rv',
revert: 'revert',
revertHint: 'Revert to %s',
defaultpopupReviewedSummary:
'Accepted by reviewing the [[Special:diff/%s/%s|difference]] between this version and previously accepted version using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupRedlinkSummary:
'Removing link to empty page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupFixDabsSummary:
'Disambiguate [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupFixRedirsSummary:
'Redirect bypass from [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupExtendedRevertSummary:
'Revert to revision dated %s by %s, oldid %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupRevertToPreviousSummary:
'Revert to the revision prior to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupRevertSummary:
'Revert to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupQueriedRevertToPreviousSummary:
'Revert to the revision prior to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupQueriedRevertSummary:
'Revert to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
defaultpopupRmDabLinkSummary:
'Remove link to dab page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
Redirects: 'Redirects', // as in Redirects to ...
' to ': ' to ', // as in Redirects to ...
'Bypass redirect': 'Bypass redirect',
'Fix this redirect': 'Fix this redirect',
disambig: 'disambig', ///// add or remove dab etc.
disambigHint: 'Disambiguate this link to [[%s]]',
'Click to disambiguate this link to:': 'Click to disambiguate this link to:',
'remove this link': 'remove this link',
'remove all links to this page from this article':
'remove all links to this page from this article',
'remove all links to this disambig page from this article':
'remove all links to this disambig page from this article',
mainlink: 'mainlink', ///// links, watch, unwatch
wikiLink: 'wikiLink',
wikiLinks: 'wikiLinks',
'links here': 'links here',
whatLinksHere: 'whatLinksHere',
'what links here': 'what links here',
WhatlinkshereHint: 'List the pages that are hyperlinked to %s',
unwatchShort: 'un',
watchThingy: 'watch', // called watchThingy because {}.watch is a function
watchHint: 'Add %s to my watchlist',
unwatchHint: 'Remove %s from my watchlist',
'Only found one editor: %s made %s edits': 'Only found one editor: %s made %s edits',
'%s seems to be the last editor to the page %s':
'%s seems to be the last editor to the page %s',
rss: 'rss',
/////////////////////////////////////
// diff previews
/////////////////////////////////////
'Diff truncated for performance reasons': 'Diff truncated for performance reasons',
'Old revision': 'Old revision',
'New revision': 'New revision',
'Something went wrong :-(': 'Something went wrong :-(',
'Empty revision, maybe non-existent': 'Empty revision, maybe non-existent',
'Unknown date': 'Unknown date',
/////////////////////////////////////
// other special previews
/////////////////////////////////////
'Empty category': 'Empty category',
'Category members (%s shown)': 'Category members (%s shown)',
'No image links found': 'No image links found',
'File links': 'File links',
'No image found': 'No image found',
'Image from Commons': 'Image from Commons',
'Description page': 'Description page',
'Alt text:': 'Alt text:',
revdel: 'Hidden revision',
/////////////////////////////////////
// user-related actions and info
/////////////////////////////////////
user: 'user', ///// user page, talk, email, space
'user page': 'user page',
'user talk': 'user talk',
'edit user talk': 'edit user talk',
'leave comment': 'leave comment',
email: 'email',
'email user': 'email user',
EmailuserHint: 'Send an email to %s',
space: 'space', // short form for userSpace link
PrefixIndexHint: 'Show pages in the userspace of %s',
count: 'count', ///// contributions, log
'edit counter': 'edit counter',
editCounterLinkHint: 'Count the contributions made by %s',
contribs: 'contribs',
contributions: 'contributions',
deletedContribs: 'deleted contributions',
DeletedcontributionsHint: 'List deleted edits made by %s',
ContributionsHint: 'List the contributions made by %s',
log: 'log',
'user log': 'user log',
userLogHint: "Show %s's user log",
arin: 'ARIN lookup', ///// ARIN lookup, block user or IP
'Look up %s in ARIN whois database': 'Look up %s in the ARIN whois database',
unblockShort: 'un',
block: 'block',
'block user': 'block user',
IpblocklistHint: 'Unblock %s',
BlockipHint: 'Prevent %s from editing',
'block log': 'block log',
blockLogHint: 'Show the block log for %s',
protectLogHint: 'Show the protection log for %s',
pageLogHint: 'Show the page log for %s',
deleteLogHint: 'Show the deletion log for %s',
'Invalid %s %s': 'The option %s is invalid: %s',
'No backlinks found': 'No backlinks found',
' and more': ' and more',
undo: 'undo',
undoHint: 'undo this edit',
'Download preview data': 'Download preview data',
'Invalid or IP user': 'Invalid or IP user',
'Not a registered username': 'Not a registered username',
BLOCKED: 'BLOCKED',
'Has blocks': 'Has blocks',
' edits since: ': ' edits since: ',
'last edit on ': 'last edit on ',
'he/him': 'he/him',
'she/her': 'she/her',
/////////////////////////////////////
// Autoediting
/////////////////////////////////////
'Enter a non-empty edit summary or press cancel to abort':
'Enter a non-empty edit summary or press cancel to abort',
'Failed to get revision information, please edit manually.\n\n':
'Failed to get revision information, please edit manually.\n\n',
'The %s button has been automatically clicked. Please wait for the next page to load.':
'The %s button has been automatically clicked. Please wait for the next page to load.',
'Could not find button %s. Please check the settings in your javascript file.':
'Could not find button %s. Please check the settings in your javascript file.',
/////////////////////////////////////
// Popups setup
/////////////////////////////////////
'Open full-size image': 'Open full-size image',
zxy: 'zxy',
autoedit_version: 'np20140416',
};
function popupString(str) {
if (typeof popupStrings != 'undefined' && popupStrings && popupStrings[str]) {
return popupStrings[str];
}
if (pg.string[str]) {
return pg.string[str];
}
return str;
}
function tprintf(str, subs) {
if (typeof subs != typeof []) {
subs = [subs];
}
return simplePrintf(popupString(str), subs);
}
// ENDFILE: strings.js
// STARTFILE: run.js
////////////////////////////////////////////////////////////////////
// Run things
////////////////////////////////////////////////////////////////////
// For some reason popups requires a fully loaded page jQuery.ready(...) causes problems for some.
// The old addOnloadHook did something similar to the below
if (document.readyState == 'complete') {
autoEdit();
}
//will setup popups
else {
$(window).on('load', autoEdit);
}
// Support for MediaWiki's live preview, VisualEditor's saves and Echo's flyout.
(function () {
let once = true;
function dynamicContentHandler($content) {
// Try to detect the hook fired on initial page load and disregard
// it, we already hook to onload (possibly to different parts of
// page - it's configurable) and running twice might be bad. Ugly…
if ($content.attr('id') == 'mw-content-text') {
if (once) {
once = false;
return;
}
}
function registerHooksForVisibleNavpops() {
for (let i = 0; pg.current.links && i < pg.current.links.length; ++i) {
const navpop = pg.current.links[i].navpopup;
if (!navpop || !navpop.isVisible()) {
continue;
}
Navpopup.tracker.addHook(posCheckerHook(navpop));
}
}
function doIt() {
registerHooksForVisibleNavpops();
$content.each(function () {
this.ranSetupTooltipsAlready = false;
setupTooltips(this);
});
}
setupPopups(doIt);
}
// This hook is also fired after page load.
mw.hook('wikipage.content').add(dynamicContentHandler);
mw.hook('ext.echo.overlay.beforeShowingOverlay').add(($overlay) => {
dynamicContentHandler($overlay.find('.mw-echo-state'));
});
}());
});
// ENDFILE: run.js
0hg4w3d7wuyediu6mitbo1s6up4ghyu
MediaWiki:Gadget-navpop.css
8
29048
326071
2026-04-04T08:50:46Z
Kannotlogin
29153
nieuw blad: .popupMoreLink { display: block; text-align: right; cursor: pointer; } ins.popupDiff { background: #afe; } del.popupDiff { background: #ffe6e6; } /* Dark mode fixes */ html.skin-theme-clientpref-night ins.popupDiff { background: #026c5f; } html.skin-theme-clientpref-night del.popupDiff { background: #89474b; } @media (prefers-color-scheme: dark) { html.skin-theme-clientpref-os ins.popupDiff { background: #026c5f; } html.skin-theme-clientpref-os del.pop…
326071
css
text/css
.popupMoreLink {
display: block;
text-align: right;
cursor: pointer;
}
ins.popupDiff {
background: #afe;
}
del.popupDiff {
background: #ffe6e6;
}
/* Dark mode fixes */
html.skin-theme-clientpref-night ins.popupDiff {
background: #026c5f;
}
html.skin-theme-clientpref-night del.popupDiff {
background: #89474b;
}
@media (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os ins.popupDiff {
background: #026c5f;
}
html.skin-theme-clientpref-os del.popupDiff {
background: #89474b;
}
}
#selectionPreview {
border: 2px solid var(--border-color-subtle, #c8ccd1);
background-color: var(--background-color-progressive-subtle, #eaf3ff);
padding: 6px;
}
.navpopup {
border: 1px solid var(--border-color-base, #a2a9b1);
background-color: var(--background-color-base, #fff);
color: var(--color-base, #202122);
padding: 10px;
padding-bottom: 5px;
font-size: 11px;
box-shadow: 0 3px 8px rgba( 50, 50, 50, 0.35 );
word-wrap: break-word;
}
.navpopup hr {
color: #aaa !important;
background-color: #aaa !important;
}
/* Configure Drag bar color */
.popupDrag {
background-color: #ffbe20;
height: 5px;
margin-top: -5px;
margin-bottom: 5px;
}
.popupDragHandle {
cursor: move;
position: relative;
}
/* menu magic - many thanks to [[User:Zocky]]! */
/* popups */
.popup_menu {
display: none;
position: absolute;
left: 0;
margin: 0;
margin-top: 1.4em;
line-height: 1.25em;
top: 0;
z-index: 2;
width: 10em;
background: var(--background-color-base, #fff);
border: 1px solid grey;
padding: 0 !important;
margin-left: -6px;
border-width: 1px 1px 1px 6px;
}
.popup_menu li { /* both: popup_menu_row and popup_menu_item */
list-style: none;
margin:0;
padding:0;
}
.popup_menu a {
display: block;
padding: 3px;
}
.popup_menu_row a {
display: inline-block;
}
.popup_menu_row {
color: #aaa;
}
.popup_drop {
display: inline;
position: relative;
}
.popup_drop a,
.popup_drop a:visited {
padding: 3px;
margin: 0;
font-weight: bold;
}
.popup_drop:hover .popup_menu,
.popup_drop .popup_menu:hover {
display: inline;
padding: 2px;
}
.popup_drop:hover {
background: #ccf;
color: #44f;
}
/* other colours, styles and so on */
.popup_menu a:hover {
background: grey;
color: #fff;
text-decoration: none;
}
.popup_mainlink {
font-size: 140%;
font-weight: bold;
}
.popup_mainlink a {
color: var(--color-emphasized, #000);
}
a.popup_change_title_link {
color: #152;
}
/* Dark mode fixes */
html.skin-theme-clientpref-night a.popup_change_title_link {
color: #25b84a;
}
@media (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os a.popup_change_title_link {
color: #25b84a;
}
}
.popup_diff_dates {
font-style: italic;
background: none;
}
.popup_menu_item a {
display: block;
}
.popup_history_row_even {
background: var(--background-color-neutral, #eaecf0);
}
.popup_history_date {
font-weight: bold;
font-size: 120%;
}
/*
Important for history preview when summaries are all empty;
e.g. https://en.wikipedia.org/w/index.php?title=User:GWicke/sw.js
The path is:
.popupPreview > table > tbody > tr:is(.popup_history_row_odd, .popup_history_row_even)
*/
.popupPreview > table,
.popupPreview > table > tbody {
display: block;
}
.popup_history_row_odd,
.popup_history_row_even {
display: flex;
}
.popup_history_row_even td:nth-child(3),
.popup_history_row_odd td:nth-child(3) {
flex: 3;
word-break: break-word;
}
.popup_history_row_even td:nth-child(4),
.popup_history_row_odd td:nth-child(4) {
flex: 7;
word-break: break-word;
}
.popup_history_row_even > td:not(:last-child),
.popup_history_row_odd > td:not(:last-child) {
margin-right: 2px;
}
/* disable interwiki styling */
.popupPreview a.extiw,
.popupPreview a.extiw:active {
color: #36b;
background: none;
padding: 0;
}
.popupPreview .external {
color: #36b;
}
/* this can be used in the content area to switch off
special external link styling */
.popupPreview .plainlinks a {
background: none !important;
padding: 0 !important;
}
/*
Mouse safe zones for popup menu.
*/
/* eye guide suggestion */
.popup_menu:hover {
box-shadow: 0 0 5px 5px rgba(179, 179, 255, 0.3);
}
/* Dark mode fixes */
html.skin-theme-clientpref-night .popup_menu:hover {
box-shadow: 0 0 5px 5px rgba(0, 0, 90, 0.3);
}
@media (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .popup_menu:hover {
box-shadow: 0 0 5px 5px rgba(0, 0, 90, 0.3);
}
}
/* trapezoid guide for the cursor */
.popup_menu::before {
content: '';
display: block;
position: absolute;
height: 1.5em;
top: -1.4em;
left: -1.2em;
width: calc(2 * var(--navpop-m-len, 6ch));
transform: perspective(1px) rotateX(1deg);
}
/* make the trapezoid go behing menu items */
.popup_menu::before {
z-index: 1;
}
.popup_menu li {
position: relative;
z-index: 2;
}
/* additional hover margin on the sides */
.popup_menu li::before {
content: '';
display: block;
position: absolute;
top: 0;
height: calc(100% + .5em);
left: -1.7em;
width: 1.7em;
}
.popup_menu li::after {
content: '';
display: block;
position: absolute;
top: 0;
height: calc(100% + .5em);
left: calc(10em - 1px); /* depends on the width of the menu */
width: 1em;
}
tj6m2rtk3wrwc2mvhq84u0lokg7ljj7
MediaWiki:Gadget-ReferenceTooltips.js
8
29049
326072
2026-04-04T08:50:59Z
Kannotlogin
29153
nieuw blad: // See [[mw:Reference Tooltips]] // Source https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js /*eslint space-in-parens: ["error", "always"], array-bracket-spacing: ["error", "always"]*/ ( function () { // If you're loading the script from another wiki and want to set your settings, do that in `window` // properties with `rt_` prefix, e.g. // window.rt_REF_LINK_SELECTOR = '...'; // They will be used instead of enwiki detaults. var REF_LINK_SELECTOR = window.…
326072
javascript
text/javascript
// See [[mw:Reference Tooltips]]
// Source https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js
/*eslint space-in-parens: ["error", "always"], array-bracket-spacing: ["error", "always"]*/
( function () {
// If you're loading the script from another wiki and want to set your settings, do that in `window`
// properties with `rt_` prefix, e.g.
// window.rt_REF_LINK_SELECTOR = '...';
// They will be used instead of enwiki detaults.
var REF_LINK_SELECTOR = window.rt_REF_LINK_SELECTOR || '.reference, a[href^="#CITEREF"]',
COMMENTED_TEXT_CLASS = window.rt_COMMENTED_TEXT_CLASS || 'rt-commentedText',
COMMENTED_TEXT_SELECTOR = (
window.rt_COMMENTED_TEXT_SELECTOR ||
( COMMENTED_TEXT_CLASS ? '.' + COMMENTED_TEXT_CLASS + ', ' : '' ) +
'abbr[title]'
);
if ( mw.messages.get( 'rt-settings' ) === null ) {
mw.messages.set( {
'rt-settings': 'Reference Tooltips settings',
'rt-enable-footer': 'Enable Reference Tooltips',
'rt-settings-title': 'Reference Tooltips',
'rt-save': 'Save',
'rt-enable': 'Enable Reference Tooltips',
'rt-activationMethod': 'Show a tooltip when I\'m',
'rt-hovering': 'hovering a reference',
'rt-clicking': 'clicking a reference',
'rt-delay': 'Delay before the tooltip appears (in milliseconds)',
'rt-tooltipsForComments': 'Show the tooltip over <span title="Tooltip example" class="' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' ) + '" style="border-bottom: 1px dotted; cursor: help;">text with a dotted underline</span> in Reference Tooltips style (allows to see such tooltips on devices with no mouse support)',
'rt-disabledNote': 'You can re-enable Reference Tooltips using a link in the footer of the page.',
'rt-done': 'Done',
'rt-enabled': 'Reference Tooltips are enabled'
} );
}
// "Global" variables
var SECONDS_IN_A_DAY = 60 * 60 * 24,
CLASSES = {
FADE_IN_DOWN: 'rt-fade-in-down',
FADE_IN_UP: 'rt-fade-in-up',
FADE_OUT_DOWN: 'rt-fade-out-down',
FADE_OUT_UP: 'rt-fade-out-up'
},
IS_TOUCHSCREEN = 'ontouchstart' in document.documentElement,
// Quite a rough check for mobile browsers, a mix of what is advised at
// https://stackoverflow.com/a/24600597 (sends to
// https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent)
// and https://stackoverflow.com/a/14301832
IS_MOBILE = /Mobi|Android/i.test( navigator.userAgent ) ||
typeof window.orientation !== 'undefined',
CLIENT_NAME = $.client.profile().name,
settingsString, settings, enabled, delay, activatedByClick, tooltipsForComments, cursorWaitCss,
windowManager, $teleportTarget,
$body = $( document.body ),
$window = $( window ),
$overlay = $( '<div>' )
.addClass( 'rt-overlay' )
.appendTo( $body );
// Can't use before https://phabricator.wikimedia.org/T369880 is resolved
// mw.loader.using( 'mediawiki.page.ready' ).then( function ( require ) {
// $teleportTarget = $( require( 'mediawiki.page.ready' ).teleportTarget );
// $overlay.appendTo( $teleportTarget );
// } );
function rt( $content ) {
// Popups gadget
if ( window.pg ) {
return;
}
var teSelector,
settingsDialogOpening = false;
function setSettingsCookie() {
mw.cookie.set(
'RTsettings',
(
Number( enabled ) +
'|' +
delay +
'|' +
Number( activatedByClick ) +
'|' +
Number( tooltipsForComments )
),
{ path: '/', expires: 90 * SECONDS_IN_A_DAY, prefix: '' }
);
}
function enableRt() {
enabled = true;
setSettingsCookie();
$( '.rt-enableItem' ).remove();
rt( $content );
mw.notify( mw.msg( 'rt-enabled' ) );
}
function disableRt() {
$content.find( teSelector ).removeClass( 'rt-commentedText' ).off( '.rt' );
$body.off( '.rt' );
$window.off( '.rt' );
}
function addEnableLink() {
// #footer-places – Vector
// #f-list – Timeless, Monobook, Modern
// parent of #footer li – Cologne Blue
var $footer = $( '#footer-places, #f-list' );
if ( !$footer.length ) {
$footer = $( '#footer li' ).parent();
}
if ( !$footer.find( '.rt-enableItem' ).length ) {
$footer.append(
$( '<li>' )
.addClass( 'rt-enableItem' )
.append(
$( '<a>' )
.text( mw.msg( 'rt-enable-footer' ) )
.attr( 'href', '#' )
.click( function ( e ) {
e.preventDefault();
enableRt();
} )
)
);
}
}
function TooltippedElement( $element ) {
var events,
te = this;
function onStartEvent( e ) {
var showRefArgs;
if ( activatedByClick && te.type !== 'commentedText' && e.type !== 'contextmenu' ) {
e.preventDefault();
}
if ( !te.noRef ) {
showRefArgs = [ $( this ) ];
if ( te.type !== 'supRef' ) {
showRefArgs.push( e.pageX, e.pageY );
}
te.showRef.apply( te, showRefArgs );
}
}
function onEndEvent() {
if ( !te.noRef ) {
te.hideRef();
}
}
if ( !$element ) {
return;
}
// TooltippedElement.$element and TooltippedElement.$originalElement will be different when
// the first is changed after its cloned version is hovered in a tooltip
this.$element = $element;
this.$originalElement = $element;
if ( this.$element.is( REF_LINK_SELECTOR ) ) {
if ( this.$element.prop( 'tagName' ) === 'SUP' ) {
this.type = 'supRef';
} else {
this.type = 'harvardRef';
}
} else {
this.type = 'commentedText';
this.comment = this.$element.attr( 'title' );
if ( !this.comment ) {
return;
}
this.$element.addClass( 'rt-commentedText' );
}
if ( activatedByClick ) {
events = {
'click.rt': onStartEvent
};
// Adds an ability to see tooltips for links
if (
this.type === 'commentedText' &&
( this.$element.closest( 'a' ).length || this.$element.has( 'a' ).length )
) {
events[ 'contextmenu.rt' ] = onStartEvent;
}
} else {
events = {
'mouseenter.rt': onStartEvent,
'mouseleave.rt': onEndEvent
};
}
this.$element.on( events );
this.hideRef = function ( immediately ) {
clearTimeout( te.showTimer );
if ( this.type === 'commentedText' ) {
this.$element.attr( 'title', this.comment );
}
if ( this.tooltip && this.tooltip.isPresent ) {
if ( activatedByClick || immediately ) {
this.tooltip.hide();
} else {
this.hideTimer = setTimeout( function () {
te.tooltip.hide();
}, 200 );
}
} else if ( this.$ref && this.$ref.hasClass( 'rt-target' ) ) {
this.$ref.removeClass( 'rt-target' );
if ( activatedByClick ) {
$body.off( 'click.rt touchstart.rt', this.onBodyClick );
}
}
};
this.showRef = function ( $element, ePageX, ePageY ) {
// Popups gadget
if ( window.pg ) {
disableRt();
return;
}
if ( this.tooltip && !this.tooltip.$content.length ) {
return;
}
var tooltipInitiallyPresent = this.tooltip && this.tooltip.isPresent;
function reallyShow() {
var viewportTop, refOffsetTop, teHref;
if ( !te.$ref && !te.comment ) {
teHref = te.type === 'supRef' ?
te.$element.find( 'a' ).attr( 'href' ) :
te.$element.attr( 'href' ); // harvardRef
te.$ref = teHref &&
$( '#' + $.escapeSelector( teHref.slice( 1 ) ) );
if ( !te.$ref || !te.$ref.length || !te.$ref.text() ) {
te.noRef = true;
return;
}
}
if ( !tooltipInitiallyPresent && !te.comment ) {
viewportTop = $window.scrollTop();
refOffsetTop = te.$ref.offset().top;
if (
!activatedByClick &&
viewportTop < refOffsetTop &&
viewportTop + $window.height() > refOffsetTop + te.$ref.height() &&
// There can be gadgets/scripts that make references horizontally scrollable.
$window.width() > te.$ref.offset().left + te.$ref.width()
) {
// Highlight the reference itself
te.$ref.addClass( 'rt-target' );
return;
}
}
if ( !te.tooltip ) {
te.tooltip = new Tooltip( te );
if ( !te.tooltip.$content.length ) {
return;
}
}
// If this tooltip is called from inside another tooltip. We can't define it
// in the constructor since a ref can be cloned but have the same Tooltip object;
// so, Tooltip.parent is a floating value.
te.tooltip.parent = te.$element.closest( '.rt-tooltip' ).data( 'tooltip' );
if ( te.tooltip.parent && te.tooltip.parent.disappearing ) {
return;
}
te.tooltip.show();
if ( tooltipInitiallyPresent ) {
if ( te.tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
te.tooltip.$element.addClass( CLASSES.FADE_IN_DOWN );
} else {
te.tooltip.$element.addClass( CLASSES.FADE_IN_UP );
}
return;
}
te.tooltip.calculatePosition( ePageX, ePageY );
$window.on( 'resize.rt', te.onWindowResize );
}
// We redefine this.$element here because e.target can be a reference link inside
// a reference tooltip, not a link that was initially assigned to this.$element
this.$element = $element;
if ( this.type === 'commentedText' ) {
this.$element.attr( 'title', '' );
}
if ( activatedByClick ) {
if (
tooltipInitiallyPresent ||
( this.$ref && this.$ref.hasClass( 'rt-target' ) )
) {
return;
} else {
setTimeout( function () {
$body.on( 'click.rt touchstart.rt', te.onBodyClick );
}, 0 );
}
}
if ( activatedByClick || tooltipInitiallyPresent ) {
reallyShow();
} else {
this.showTimer = setTimeout( reallyShow, delay );
}
};
this.onBodyClick = function ( e ) {
if ( !te.tooltip && !( te.$ref && te.$ref.hasClass( 'rt-target' ) ) ) {
return;
}
var $current = $( e.target );
function contextMatchesParameter( parameter ) {
return this === parameter;
}
// The last condition is used to determine cases when a clicked tooltip is the current
// element's tooltip or one of its descendants
while (
$current.length &&
(
!$current.hasClass( 'rt-tooltip' ) ||
!$current.data( 'tooltip' ) ||
!$current.data( 'tooltip' ).upToTopParent(
contextMatchesParameter, [ te.tooltip ],
true
)
)
) {
$current = $current.parent();
}
if ( !$current.length ) {
te.hideRef();
}
};
this.onWindowResize = function () {
te.tooltip.calculatePosition();
};
}
function Tooltip( te ) {
function openSettingsDialog() {
var settingsDialog, settingsWindow;
if ( cursorWaitCss ) {
cursorWaitCss.disabled = true;
}
function SettingsDialog() {
SettingsDialog.parent.call( this );
}
OO.inheritClass( SettingsDialog, OO.ui.ProcessDialog );
SettingsDialog.static.name = 'settingsDialog';
SettingsDialog.static.title = mw.msg( 'rt-settings-title' );
SettingsDialog.static.actions = [
{
modes: 'main',
action: 'save',
label: mw.msg( 'rt-save' ),
flags: [ 'primary', 'progressive' ]
},
{
modes: 'main',
flags: [ 'safe', 'close' ]
},
{
modes: 'disabled',
action: 'deactivated',
label: mw.msg( 'rt-done' ),
flags: [ 'primary', 'progressive' ]
}
];
SettingsDialog.prototype.initialize = function () {
var dialog = this;
SettingsDialog.parent.prototype.initialize.apply( this, arguments );
this.enableCheckbox = new OO.ui.CheckboxInputWidget( {
selected: true
} );
this.enableCheckbox.on( 'change', function ( selected ) {
dialog.activationMethodSelect.setDisabled( !selected );
dialog.delayInput.setDisabled( !selected || dialog.clickOption.isSelected() );
dialog.tooltipsForCommentsCheckbox.setDisabled( !selected );
} );
this.enableField = new OO.ui.FieldLayout( this.enableCheckbox, {
label: mw.msg( 'rt-enable' ),
align: 'inline',
classes: [ 'rt-enableField' ]
} );
this.hoverOption = new OO.ui.RadioOptionWidget( {
label: mw.msg( 'rt-hovering' )
} );
this.clickOption = new OO.ui.RadioOptionWidget( {
label: mw.msg( 'rt-clicking' )
} );
this.activationMethodSelect = new OO.ui.RadioSelectWidget( {
items: [ this.hoverOption, this.clickOption ]
} );
this.activationMethodSelect.selectItem(
activatedByClick ? this.clickOption : this.hoverOption
);
this.activationMethodSelect.on( 'choose', function ( item ) {
dialog.delayInput.setDisabled( item === dialog.clickOption );
} );
this.activationMethodField = new OO.ui.FieldLayout( this.activationMethodSelect, {
label: mw.msg( 'rt-activationMethod' ),
align: 'top'
} );
this.delayInput = new OO.ui.NumberInputWidget( {
input: { value: delay },
step: 50,
min: 0,
max: 5000,
disabled: activatedByClick,
classes: [ 'rt-numberInput' ]
} );
this.delayField = new OO.ui.FieldLayout( this.delayInput, {
label: mw.msg( 'rt-delay' ),
align: 'top'
} );
this.tooltipsForCommentsCheckbox = new OO.ui.CheckboxInputWidget( {
selected: tooltipsForComments
} );
this.tooltipsForCommentsField = new OO.ui.FieldLayout(
this.tooltipsForCommentsCheckbox,
{
label: new OO.ui.HtmlSnippet( mw.msg( 'rt-tooltipsForComments' ) ),
align: 'inline',
classes: [ 'rt-tooltipsForCommentsField' ]
}
);
new TooltippedElement(
this.tooltipsForCommentsField.$element.find(
'.' + ( COMMENTED_TEXT_CLASS || 'rt-commentedText' )
)
);
this.fieldset = new OO.ui.FieldsetLayout();
this.fieldset.addItems( [
this.enableField,
this.activationMethodField,
this.delayField,
this.tooltipsForCommentsField
] );
this.panelSettings = new OO.ui.PanelLayout( {
padded: true,
expanded: false
} );
this.panelSettings.$element.append( this.fieldset.$element );
this.panelDisabled = new OO.ui.PanelLayout( {
padded: true,
expanded: false
} );
this.panelDisabled.$element.append(
$( '<table>' )
.addClass( 'rt-disabledHelp' )
.append(
$( '<tr>' ).append(
$( '<td>' ).append(
$( '<img>' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/c/c0/MediaWiki_footer_link_ltr.svg' )
),
$( '<td>' )
.addClass( 'rt-disabledNote' )
.text( mw.msg( 'rt-disabledNote' ) )
)
)
);
this.stackLayout = new OO.ui.StackLayout( {
items: [ this.panelSettings, this.panelDisabled ]
} );
this.$body.append( this.stackLayout.$element );
};
SettingsDialog.prototype.getSetupProcess = function ( data ) {
return SettingsDialog.parent.prototype.getSetupProcess.call( this, data )
.next( function () {
this.stackLayout.setItem( this.panelSettings );
this.actions.setMode( 'main' );
}, this );
};
SettingsDialog.prototype.getActionProcess = function ( action ) {
var dialog = this;
if ( action === 'save' ) {
return new OO.ui.Process( function () {
var newDelay = Number( dialog.delayInput.getValue() );
enabled = dialog.enableCheckbox.isSelected();
if ( newDelay >= 0 && newDelay <= 5000 ) {
delay = newDelay;
}
activatedByClick = dialog.clickOption.isSelected();
tooltipsForComments = dialog.tooltipsForCommentsCheckbox.isSelected();
setSettingsCookie();
if ( enabled ) {
dialog.close();
disableRt();
rt( $content );
} else {
dialog.actions.setMode( 'disabled' );
dialog.stackLayout.setItem( dialog.panelDisabled );
disableRt();
addEnableLink();
}
} );
} else if ( action === 'deactivated' ) {
dialog.close();
}
return SettingsDialog.parent.prototype.getActionProcess.call( this, action );
};
SettingsDialog.prototype.getBodyHeight = function () {
return this.stackLayout.getCurrentItem().$element.outerHeight( true );
};
tooltip.upToTopParent( function adjustRightAndHide() {
if ( this.isPresent ) {
if ( this.$element[ 0 ].style.right ) {
this.$element.css(
'right',
'+=' + ( window.innerWidth - $window.width() )
);
}
this.te.hideRef( true );
}
} );
if ( !windowManager ) {
windowManager = new OO.ui.WindowManager();
$body.append( windowManager.$element );
}
settingsDialog = new SettingsDialog();
windowManager.addWindows( [ settingsDialog ] );
settingsWindow = windowManager.openWindow( settingsDialog );
settingsWindow.opened.then( function () {
settingsDialogOpening = false;
} );
settingsWindow.closed.then( function () {
windowManager.clearWindows();
} );
}
var tooltip = this;
// This variable can change: one tooltip can be called from a harvard-style reference link
// that is put into different tooltips
this.te = te;
switch ( this.te.type ) {
case 'supRef':
this.id = 'rt-' + this.te.$originalElement.attr( 'id' );
this.$content = this.te.$ref
.contents()
.filter( function ( i ) {
var $this = $( this );
if ( $this.hasClass( 'mw-subreference-list' ) ) {
return false;
}
return (
this.nodeType === Node.TEXT_NODE ||
!(
// `a[href^="#cite_ref-"]` is for Wiktionary and possibly other
// sites (not English Wikipedia) where the output of the Cite
// extension is slightly different
$this.is( '.mw-cite-backlink, a[href^="#cite_ref-"]' ) ||
(
i === 0 &&
// Template:Cnote, Template:Note
( $this.is( 'b' ) ||
// Template:Note_label
$this.is( 'a' ) &&
$this.attr( 'href' ).indexOf( '#ref' ) === 0
)
)
)
);
} )
.clone( true );
const $ol = this.te.$ref.closest( 'ol' );
if ( $ol.hasClass( 'mw-subreference-list' ) ) {
this.$content = $( '<div>' ).append(
$ol.siblings( '.reference-text' ).clone( true )
.css( { display: 'block', 'margin-bottom': '0.7em' } ),
this.$content
);
}
break;
case 'harvardRef':
this.id = 'rt-' + this.te.$originalElement.closest( 'li' ).attr( 'id' );
this.$content = this.te.$ref
.clone( true )
.removeAttr( 'id' );
break;
case 'commentedText':
this.id = 'rt-' + String( Math.random() ).slice( 2 );
this.$content = $( document.createTextNode( this.te.comment ) );
break;
}
if ( !this.$content.length ) {
return;
}
this.isInsideWindow = Boolean( this.te.$element.closest( '.oo-ui-window' ).length );
this.$element = $( '<div>' )
.addClass( 'rt-tooltip' )
.attr( 'id', this.id )
.attr( 'role', 'tooltip' )
.data( 'tooltip', this );
var $hoverArea = $( '<div>' )
.addClass( 'rt-hoverArea' )
.appendTo( this.$element );
var $scroll = $( '<div>' )
.addClass( 'rt-scroll' )
.appendTo( $hoverArea );
this.$content = this.$content
.wrapAll( '<div>' )
.parent()
.addClass( 'rt-content' )
.addClass( 'mw-parser-output' )
.appendTo( $scroll );
if ( !activatedByClick ) {
this.$element
.on( 'mouseenter linkPopupHover', function ( e ) {
if ( !tooltip.disappearing || e.type === 'linkPopupHover' ) {
tooltip.upToTopParent( function () {
this.show();
} );
}
} )
.on( 'mouseleave', function ( e ) {
// https://stackoverflow.com/q/47649442 workaround. Relying on relatedTarget
// alone has pitfalls: when alt-tabbing, relatedTarget is empty too
if (
CLIENT_NAME !== 'chrome' ||
(
!e.originalEvent ||
e.originalEvent.relatedTarget !== null ||
!tooltip.clickedTime ||
$.now() - tooltip.clickedTime > 50
)
) {
tooltip.upToTopParent( function () {
this.te.hideRef();
} );
}
} )
.click( function () {
tooltip.clickedTime = $.now();
} );
}
if ( !this.isInsideWindow ) {
$( '<a>' )
.addClass( 'rt-settingsLink' )
.attr( 'role', 'button' )
.attr( 'href', '#' )
.attr( 'title', mw.msg( 'rt-settings' ) )
.click( function ( e ) {
e.preventDefault();
if ( settingsDialogOpening ) {
return;
}
settingsDialogOpening = true;
if ( mw.loader.getState( 'oojs-ui' ) !== 'ready' ) {
if ( cursorWaitCss ) {
cursorWaitCss.disabled = false;
} else {
cursorWaitCss = mw.util.addCSS( 'body { cursor: wait; }' );
}
}
mw.loader.using( [ 'oojs', 'oojs-ui' ], openSettingsDialog );
} )
.prependTo( this.$content );
}
// Tooltip tail element is inside tooltip content element in order for the tooltip
// not to disappear when the mouse is above the tail
this.$tail = $( '<div>' )
.addClass( 'rt-tail' )
.prependTo( this.$element );
this.disappearing = false;
this.show = function () {
this.disappearing = false;
clearTimeout( this.te.hideTimer );
clearTimeout( this.te.removeTimer );
this.$element
.removeClass( CLASSES.FADE_OUT_DOWN )
.removeClass( CLASSES.FADE_OUT_UP );
if ( !this.isPresent ) {
$overlay.append( this.$element );
}
this.isPresent = true;
};
this.hide = function () {
var tooltip = this;
tooltip.disappearing = true;
if ( tooltip.$element.hasClass( 'rt-tooltip-above' ) ) {
tooltip.$element
.removeClass( CLASSES.FADE_IN_DOWN )
.addClass( CLASSES.FADE_OUT_UP );
} else {
tooltip.$element
.removeClass( CLASSES.FADE_IN_UP )
.addClass( CLASSES.FADE_OUT_DOWN );
}
tooltip.te.removeTimer = setTimeout( function () {
if ( tooltip.isPresent ) {
tooltip.$element.detach();
tooltip.$tail.css( 'left', '' );
if ( activatedByClick ) {
$body.off( 'click.rt touchstart.rt', tooltip.te.onBodyClick );
}
$window.off( 'resize.rt', tooltip.te.onWindowResize );
tooltip.isPresent = false;
}
}, 200 );
};
this.calculatePosition = function ( ePageX, ePageY ) {
var teElement, teOffsets, teOffset, targetTailOffsetX, tailLeft;
this.$tail.css( 'left', '' );
teElement = this.te.$element.get( 0 );
if ( ePageX !== undefined ) {
targetTailOffsetX = ePageX;
teOffsets = ( teElement.getClientRects && teElement.getClientRects() ) ||
teElement.getBoundingClientRect();
if ( teOffsets.length > 1 ) {
for ( var i = teOffsets.length - 1; i >= 0; i-- ) {
if (
ePageY >= Math.round( $window.scrollTop() + teOffsets[ i ].top ) &&
ePageY <= Math.round(
$window.scrollTop() + teOffsets[i].top + teOffsets[ i ].height
)
) {
teOffset = teOffsets[ i ];
}
}
}
}
if ( !teOffset ) {
teOffset = ( teElement.getClientRects && teElement.getClientRects()[ 0 ] ) ||
teElement.getBoundingClientRect();
}
teOffset = {
top: $window.scrollTop() + teOffset.top,
left: $window.scrollLeft() + teOffset.left,
width: teOffset.width,
height: teOffset.height
};
if ( !targetTailOffsetX ) {
targetTailOffsetX = teOffset.left + ( teOffset.width / 2 );
}
// Value of `left` in `.rt-tooltip-above .rt-tail`
var defaultTailLeft = 19;
// Value of `width` in `.rt-tail`
var tailSideWidth = 13;
// We tilt the square 45 degrees, so we need square root to calculate the distance.
var tailWidth = tailSideWidth * Math.SQRT2;
var tailHeight = tailWidth / 2;
var tailCenterDelta = tailSideWidth + 1 - ( tailWidth / 2 );
var tooltip = this;
var getTop = function ( isBelow ) {
var delta = isBelow ?
teOffset.height + tailHeight :
-tooltip.$element.outerHeight() - tailHeight + 1;
return teOffset.top + delta;
};
this.$element.css( {
top: getTop(),
left: targetTailOffsetX - defaultTailLeft - tailCenterDelta,
right: ''
} );
// Is it squished against the right side of the page?
if ( this.$element.offset().left + this.$element.outerWidth() > $window.width() - 1 ) {
this.$element.css( {
left: '',
right: 0
} );
tailLeft = targetTailOffsetX - this.$element.offset().left - tailCenterDelta;
}
// Is a part of it above the top of the screen?
if ( teOffset.top < this.$element.outerHeight() + $window.scrollTop() + tailHeight ) {
this.$element
.removeClass( 'rt-tooltip-above' )
.addClass( 'rt-tooltip-below' )
.addClass( CLASSES.FADE_IN_UP )
.css( {
top: getTop( true )
} );
if ( tailLeft ) {
this.$tail.css( 'left', ( tailLeft + tailSideWidth ) + 'px' );
}
} else {
this.$element
.removeClass( 'rt-tooltip-below' )
.addClass( 'rt-tooltip-above' )
.addClass( CLASSES.FADE_IN_DOWN )
// A fix for cases when a tooltip shown once is then wrongly positioned when it
// is shown again after a window resize.
.css( {
top: getTop()
} );
if ( tailLeft ) {
this.$tail.css( 'left', tailLeft + 'px' );
}
}
};
// Run some function for all the tooltips up to the top one in a tree. Its context will be
// the tooltip, while its parameters may be passed to Tooltip.upToTopParent as an array
// in the second parameter. If the third parameter passed to ToolTip.upToTopParent is true,
// the execution stops when the function in question returns true for the first time,
// and ToolTip.upToTopParent returns true as well.
this.upToTopParent = function ( func, parameters, stopAtTrue ) {
var returnValue,
currentTooltip = this;
do {
returnValue = func.apply( currentTooltip, parameters );
if ( stopAtTrue && returnValue ) {
break;
}
} while ( ( currentTooltip = currentTooltip.parent ) );
if ( stopAtTrue ) {
return returnValue;
}
};
}
if ( !enabled ) {
addEnableLink();
return;
}
teSelector = REF_LINK_SELECTOR;
if ( tooltipsForComments ) {
teSelector += ', ' + COMMENTED_TEXT_SELECTOR;
}
$content.find( teSelector ).each( function () {
new TooltippedElement( $( this ) );
} );
}
settingsString = mw.cookie.get( 'RTsettings', '' );
if ( settingsString ) {
settings = settingsString.split( '|' );
enabled = Boolean( Number( settings[ 0 ] ) );
delay = Number( settings[ 1 ] );
activatedByClick = Boolean( Number( settings[ 2 ] ) );
// The forth value was added later, so we provide for a default value. See comments below
// for why we use "IS_TOUCHSCREEN && IS_MOBILE".
tooltipsForComments = settings[ 3 ] === undefined ?
IS_TOUCHSCREEN && IS_MOBILE :
Boolean( Number( settings[ 3 ] ) );
} else {
enabled = true;
delay = 200;
// Since the mobile browser check is error-prone, adding IS_MOBILE condition here would probably
// leave cases where a user interacting with the browser using touches doesn't know how to call
// a tooltip in order to switch to activation by click. Some touch-supporting laptop users
// interacting by touch (though probably not the most popular use case) would not be happy too.
activatedByClick = IS_TOUCHSCREEN;
// Arguably we shouldn't convert native tooltips into gadget tooltips for devices that have
// mouse support, even if they have touchscreens (there are laptops with touchscreens).
// IS_TOUCHSCREEN check here is for reliability, since the mobile check is prone to false
// positives.
tooltipsForComments = IS_TOUCHSCREEN && IS_MOBILE;
}
mw.hook( 'wikipage.content' ).add( rt );
}() );
a43nofchz87jsl0z00dy2djz6ctocgi
MediaWiki:Gadget-ReferenceTooltips.css
8
29050
326073
2026-04-04T08:51:20Z
Kannotlogin
29153
nieuw blad: /* See [[mw:Reference Tooltips]] */ .rt-overlay { position: absolute; width: 100%; font-size: calc(var(--font-size-medium, 1rem) * (13 / 14)); line-height: 1.5em; /* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */ z-index: 800; /* match z-index-tooltip in https://doc.wikimedia.org/codex/latest/design-tokens/z-index.html */ top: 0; } /* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportT…
326073
css
text/css
/* See [[mw:Reference Tooltips]] */
.rt-overlay {
position: absolute;
width: 100%;
font-size: calc(var(--font-size-medium, 1rem) * (13 / 14));
line-height: 1.5em;
/* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */
z-index: 800; /* match z-index-tooltip in https://doc.wikimedia.org/codex/latest/design-tokens/z-index.html */
top: 0;
}
/* Remove after https://phabricator.wikimedia.org/T369880 is resolved and $teleportTarget is assigned */
.skin-vector-legacy .rt-overlay {
font-size: 13px;
}
.skin-monobook .rt-overlay {
font-size: 12.7px;
}
.rt-tooltip {
position: absolute;
max-width: 27em;
background: var(--background-color-base, #fff);
color: var(--color-base, #202122);
border: 1px solid var(--border-color-subtle, #c8ccd1);
border-radius: 2px;
box-shadow: 0 20px 48px 0 rgba(0, 0, 0, 0.2);
}
html.skin-theme-clientpref-night .rt-tooltip {
box-shadow: 0 20px 48px 0 rgba(0, 0, 0, 1);
}
/* Extend the tooltip vertically to make sure it doesn't disappear while the user moves the mouse to it */
.rt-tooltip-above .rt-hoverArea {
margin-bottom: -0.6em;
padding-bottom: 0.6em;
}
.rt-tooltip-below .rt-hoverArea {
margin-top: -0.7em;
padding-top: 0.7em;
}
.rt-scroll {
overflow-x: auto;
}
.rt-content {
padding: 0.7em 0.9em;
overflow-wrap: break-word;
}
.rt-tail {
/* Use 48%, not 50%, to make the tail start at a right place in Blink browsers in Windows on bigger system font sizes */
background: linear-gradient(to top right, var(--border-color-subtle, #c8ccd1) 48%, rgba(0, 0, 0, 0) 48%);
--tail-left: 19px;
--tail-side-width: 13px;
}
.rt-tail,
.rt-tail:after {
position: absolute;
/* Make sure the tail is behind the scrollbar, e.g. [73] at
https://en.wikipedia.org/w/index.php?title=Lemniscate_elliptic_functions&oldid=1231701944#cite_ref-73
if .rt-tooltip has width of 25em */
z-index: -1;
width: var(--tail-side-width);
height: var(--tail-side-width);
}
.rt-tail:after {
content: '';
background: var(--background-color-base, #fff);
bottom: 1px;
left: 1px;
}
.rt-tooltip-above .rt-tail {
transform: rotate(-45deg);
transform-origin: 100% 100%;
bottom: 0;
left: var(--tail-left);
}
.rt-tooltip-below .rt-tail {
transform: rotate(135deg);
transform-origin: 0 0;
top: 0;
left: calc(var(--tail-left) + var(--tail-side-width));
}
.rt-settingsLink {
background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%0D%0A%20%20%20%20%3Cpath%20fill%3D%22%2354595d%22%20d%3D%22M20%2014.5v-2.9l-1.8-.3c-.1-.4-.3-.8-.6-1.4l1.1-1.5-2.1-2.1-1.5%201.1c-.5-.3-1-.5-1.4-.6L13.5%205h-2.9l-.3%201.8c-.5.1-.9.3-1.4.6L7.4%206.3%205.3%208.4l1%201.5c-.3.5-.4.9-.6%201.4l-1.7.2v2.9l1.8.3c.1.5.3.9.6%201.4l-1%201.5%202.1%202.1%201.5-1c.4.2.9.4%201.4.6l.3%201.8h3l.3-1.8c.5-.1.9-.3%201.4-.6l1.5%201.1%202.1-2.1-1.1-1.5c.3-.5.5-1%20.6-1.4l1.5-.3zM12%2016c-1.7%200-3-1.3-3-3s1.3-3%203-3%203%201.3%203%203-1.3%203-3%203z%22%2F%3E%0D%0A%3C%2Fsvg%3E);
float: right;
margin: -0.5em -0.5em 0 0.5em;
box-sizing: border-box;
height: 32px;
width: 32px;
border: 1px solid transparent;
border-radius: 2px;
background-position: center center;
background-repeat: no-repeat;
background-size: 24px 24px;
}
html.skin-theme-clientpref-night .rt-settingsLink {
background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%0D%0A%20%20%20%20%3Cpath%20fill%3D%22%23c8ccd1%22%20d%3D%22M20%2014.5v-2.9l-1.8-.3c-.1-.4-.3-.8-.6-1.4l1.1-1.5-2.1-2.1-1.5%201.1c-.5-.3-1-.5-1.4-.6L13.5%205h-2.9l-.3%201.8c-.5.1-.9.3-1.4.6L7.4%206.3%205.3%208.4l1%201.5c-.3.5-.4.9-.6%201.4l-1.7.2v2.9l1.8.3c.1.5.3.9.6%201.4l-1%201.5%202.1%202.1%201.5-1c.4.2.9.4%201.4.6l.3%201.8h3l.3-1.8c.5-.1.9-.3%201.4-.6l1.5%201.1%202.1-2.1-1.1-1.5c.3-.5.5-1%20.6-1.4l1.5-.3zM12%2016c-1.7%200-3-1.3-3-3s1.3-3%203-3%203%201.3%203%203-1.3%203-3%203z%22%2F%3E%0D%0A%3C%2Fsvg%3E);
}
.rt-settingsLink:hover,
.rt-settingsLink:active {
background-color: var(--background-color-interactive, #eaecf0);
}
.rt-settingsLink:active {
border-color: var(--border-color-interactive, #72777d);
}
.rt-settingsLink:focus {
outline: 1px solid transparent;
}
.rt-settingsLink:focus:not(:active) {
border-color: var(--border-color-progressive--focus, #36c);
box-shadow: inset 0 0 0 1px var(--box-shadow-color-progressive--focus, #36c);
}
.rt-target {
background-color: var(--background-color-progressive-subtle, #eaf3ff);
}
.rt-enableField {
font-weight: bold;
margin-bottom: 1.25em;
}
.rt-numberInput.rt-numberInput {
width: 10em;
}
.rt-tooltipsForCommentsField.rt-tooltipsForCommentsField.rt-tooltipsForCommentsField {
margin-top: 1.25em;
}
.rt-disabledHelp {
border-collapse: collapse;
}
.rt-disabledHelp td {
padding: 0;
}
.rt-disabledNote.rt-disabledNote {
vertical-align: bottom;
padding-left: 0.36em;
font-weight: bold;
}
@keyframes rt-fade-in-up {
0% {
opacity: 0;
transform: translate(0, 20px);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes rt-fade-in-down {
0% {
opacity: 0;
transform: translate(0, -20px);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes rt-fade-out-down {
0% {
opacity: 1;
transform: translate(0, 0);
}
100% {
opacity: 0;
transform: translate(0, 20px);
}
}
@keyframes rt-fade-out-up {
0% {
opacity: 1;
transform: translate(0, 0);
}
100% {
opacity: 0;
transform: translate(0, -20px);
}
}
.rt-fade-in-up {
animation: rt-fade-in-up 0.2s ease forwards;
}
.rt-fade-in-down {
animation: rt-fade-in-down 0.2s ease forwards;
}
.rt-fade-out-down {
animation: rt-fade-out-down 0.2s ease forwards;
}
.rt-fade-out-up {
animation: rt-fade-out-up 0.2s ease forwards;
}
8zpwcq3vyh7m42hvp1h7840jq6sb25g
MediaWiki:Gadget-ImageAnnotator.js
8
29051
326074
2026-04-04T08:51:31Z
Kannotlogin
29153
nieuw blad: /* ImageAnnotator v2.3.2 Image annotations. Draw rectangles onto image thumbnail displayed on image description page and associate them with textual descriptions that will be displayed when the mouse moves over the rectangles. If an image has annotations, display the rectangles. Add a button to create new annotations. Note: if an image that has annotations is overwritten by a new version, only display the annotations if the size of the top image matches the store…
326074
javascript
text/javascript
/*
ImageAnnotator v2.3.2
Image annotations. Draw rectangles onto image thumbnail displayed on image description
page and associate them with textual descriptions that will be displayed when the mouse
moves over the rectangles. If an image has annotations, display the rectangles. Add a
button to create new annotations.
Note: if an image that has annotations is overwritten by a new version, only display the
annotations if the size of the top image matches the stored size exactly. To recover
annotations, one will need to edit the image description page manually, adjusting image
sizes and rectangle coordinates, or re-enter annotations.
Author: [[User:Lupo]], June 2009 - March 2010
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation.
*/
/* global importScript, importScriptURI,
LAPI, Tooltip, Tooltips, TextCleaner, UIElements, Buttons,
ImageAnnotator, ImageAnnotator_disable */
/* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define,
eqeqeq, no-alert, no-loop-func, no-inner-declarations */
if ( typeof ImageAnnotator === 'undefined' ) { // Guard against multiple inclusions
importScript( 'MediaWiki:LAPI.js' );
importScript( 'MediaWiki:Tooltips.js' );
importScript( 'MediaWiki:TextCleaner.js' );
importScript( 'MediaWiki:UIElements.js' );
( function () { // Local scope
var ImageAnnotator_config = null;
var ImageAnnotation = function () {
this.initialize.apply( this, arguments );
};
ImageAnnotation.compare = function ( a, b ) {
var result = b.area() - a.area();
if ( result !== 0 ) { return result; }
// Just to make sure the order is complete
return a.model.id - b.model.id;
};
ImageAnnotation.prototype = {
// Rectangle to be displayed on image: a div with pos and size
view: null,
// Internal representation of the annotation
model: null,
// Tooltip to display the annotation
tooltip: null,
// Content of the tooltip
content: null,
// Reference to the viewer this note belongs to
viewer: null,
initialize: function ( node, viewer, id ) {
var is_new = false;
var view_w = 0, view_h = 0, view_x = 0, view_y = 0;
this.viewer = viewer;
if ( LAPI.DOM.hasClass( node, IA.annotation_class ) ) {
// Extract the info we need
var x = IA.getIntItem( 'view_x_' + id, viewer.scope );
var y = IA.getIntItem( 'view_y_' + id, viewer.scope );
var w = IA.getIntItem( 'view_w_' + id, viewer.scope );
var h = IA.getIntItem( 'view_h_' + id, viewer.scope );
var html = IA.getRawItem( 'content_' + id, viewer.scope );
if ( x === null || y === null || w === null || h === null || html === null ) { throw new Error( 'Invalid note' ); }
if ( x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height ) { throw new Error( 'Invalid note: origin invalid on note ' + id ); }
if (
x + w > viewer.full_img.width + 10 ||
y + h > viewer.full_img.height + 10
) {
throw new Error( 'Invalid note: size extends beyond image on note ' + id );
}
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
if ( x + w > viewer.full_img.width ) { w = viewer.full_img.width - x; }
if ( y + h > viewer.full_img.height ) { h = viewer.full_img.height - y; }
view_w = Math.floor( w / viewer.factors.dx );
view_h = Math.floor( h / viewer.factors.dy );
view_x = Math.floor( x / viewer.factors.dx );
view_y = Math.floor( y / viewer.factors.dy );
this.view = LAPI.make(
'div', null,
{
position: 'absolute',
display: 'none',
lineHeight: '0px', // IE
fontSize: '0px', // IE
top: String( view_y ) + 'px',
left: String( view_x ) + 'px',
width: String( view_w ) + 'px',
height: String( view_h ) + 'px'
}
);
// We'll add the view to the DOM once we've loaded all notes
this.model = {
id: id,
dimension: { x: x, y: y, w: w, h: h },
wiki: '',
html: html.cloneNode( true )
};
} else {
is_new = true;
this.view = node;
this.model = {
id: -1,
dimension: null,
wiki: '',
html: null
};
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
view_h = this.view.offsetHeight - 2;
view_x = this.view.offsetLeft;
view_y = this.view.offsetTop;
}
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
if ( view_w < 6 ) { view_x = Math.floor( view_x + view_w / 2 - 3 ); view_w = 6; }
if ( view_h < 6 ) { view_y = Math.floor( view_y + view_h / 2 - 3 ); view_h = 6; }
Object.merge(
{
left: String( view_x ) + 'px',
top: String( view_y ) + 'px',
width: String( view_w ) + 'px',
height: String( view_h ) + 'px'
},
this.view.style
);
this.view.style.zIndex = 500; // Below tooltips
try {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.outer_border;
}
this.view.appendChild(
LAPI.make(
'div', null
, { lineHeight: '0px', // IE
fontSize: '0px', // IE
width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border
height: String( Math.max( view_h - 2, 0 ) ) + 'px'
}
)
// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
);
try {
this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border;
} catch ( ex ) {
this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
}
if ( is_new ) { viewer.adjustRectangleSize( this.view ); }
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
// display raw rectangles without any tooltips attached while drawing. Yuck.
this.dummy = this.view.cloneNode( true );
viewer.img_div.appendChild( this.dummy );
if ( !is_new ) {
// New notes get their tooltip only once the editor has saved, otherwise IE may try to
// open them if the mouse moves onto the view even though there is the cover above them!
this.setTooltip();
}
},
setTooltip: function () {
if ( this.tooltip || !this.view ) { return; } // Already set, or corrupt
// Note: on IE, don't have tooltips appear automatically. IE doesn't do it right for transparent
// targets and we have to show and hide them ourselves through a mousemove listener in the viewer
// anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering.
this.tooltip = new Tooltip(
this.view.firstChild,
this.display.bind( this ),
{
activate: ( LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER ),
deactivate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ),
close_button: null,
mode: Tooltip.MOUSE,
mouse_offset: { x: -5, y: -5, dx: ( IA.is_rtl ? -1 : 1 ), dy: 1 },
open_delay: 0,
hide_delay: 0,
onclose: ( function ( tooltip, evt ) {
if ( this.view ) {
try {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.outer_border;
}
}
if ( this.viewer.tip == tooltip ) { this.viewer.tip = null; }
// Hide all boxes if we're outside the image. Relies on hide checking the
// coordinates! (Otherwise, we'd always hide...)
if ( evt ) { this.viewer.hide( evt ); }
} ).bind( this ),
onopen: ( function ( tooltip ) {
if ( this.view ) {
try {
this.view.style.border = '1px solid ' + this.viewer.active_border;
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.active_border;
}
}
this.viewer.tip = tooltip;
} ).bind( this )
},
IA.tooltip_styles
);
},
display: function ( evt ) {
if ( !this.content ) {
this.content = LAPI.make( 'div' );
var main = LAPI.make( 'div' );
this.content.appendChild( main );
this.content.main = main;
if ( this.model.html ) { main.appendChild( this.model.html.cloneNode( true ) ); }
// Make sure that the popup encompasses all floats
this.content.appendChild( LAPI.make( 'div', null, { clear: 'both' } ) );
if ( this.viewer.may_edit ) {
this.content.button_section = LAPI.make(
'div',
null,
{
fontSize: 'smaller',
textAlign: ( IA.is_rtl ? 'left' : 'right' ),
borderTop: IA.tooltip_styles.border
}
);
this.content.appendChild( this.content.button_section );
this.content.button_section.appendChild( LAPI.DOM.makeLink(
'#',
ImageAnnotator.UI.get( 'wpImageAnnotatorEdit', true ),
null,
LAPI.Evt.makeListener( this, this.edit )
) );
if ( ImageAnnotator_config.mayDelete() ) {
this.content.button_section.appendChild( document.createTextNode( '\xa0' ) );
this.content.button_section.appendChild( LAPI.DOM.makeLink(
'#',
ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ),
null,
LAPI.Evt.makeListener( this, this.remove_event )
) );
}
}
}
return this.content;
},
edit: function ( evt ) {
if ( IA.canEdit() ) { IA.editor.editNote( this ); }
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
},
remove_event: function ( evt ) {
if ( IA.canEdit() ) { this.remove(); }
return LAPI.Evt.kill( evt );
},
remove: function () {
if ( !this.content ) { // New note: just destroy it.
this.destroy();
return true;
}
if ( !ImageAnnotator_config.mayDelete() ) { return false; }
// Close and remove tooltip only if edit succeeded! Where and how to display error messages?
var reason = '';
if ( !ImageAnnotator_config.mayBypassDeletionPrompt() || !window.ImageAnnotator_noDeletionPrompt ) {
// Prompt for a removal reson
reason = prompt( ImageAnnotator.UI.get( 'wpImageAnnotatorDeleteReason', true ), '' );
if ( reason === null ) { return false; } // Cancelled
reason = reason.trim();
if ( !reason.length ) {
if ( !ImageAnnotator_config.emptyDeletionReasonAllowed() ) { return false; }
}
// Re-show tooltip (without re-positioning it, we have no mouse coordinates here) in case
// it was hidden because of the alert. If possible, we want the user to see the spinner.
this.tooltip.show_now( this.tooltip );
}
var self = this;
var spinnerId = 'image_annotation_delete_' + this.model.id;
LAPI.Ajax.injectSpinner( this.content.button_section.lastChild, spinnerId );
if ( this.tooltip ) { this.tooltip.size_change(); }
LAPI.Ajax.editPage(
mw.config.get( 'wgPageName' ),
function ( doc, editForm, failureFunc, revision_id ) {
try {
if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) { throw new Error( '#Page version (revision ID) mismatch: edit conflict.' ); }
var textbox = editForm.wpTextbox1;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
var pagetext = textbox.value.replace( /\r\n/g, '\n' );
// Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other
// browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added.
// We normally don't care, but here we need this to make sure we don't leave extra line
// breaks when we remove the note.
IA.setWikitext( pagetext );
var span = IA.findNote( pagetext, self.model.id );
if ( !span ) { // Hmmm? Doesn't seem to exist
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
self.destroy();
return;
}
var char_before = 0;
var char_after = 0;
if ( span.start > 0 ) { char_before = pagetext.charCodeAt( span.start - 1 ); }
if ( span.end < pagetext.length ) { char_after = pagetext.charCodeAt( span.end ); }
if (
String.fromCharCode( char_before ) == '\n' &&
String.fromCharCode( char_after ) == '\n'
) {
span.start = span.start - 1;
}
pagetext = pagetext.substring( 0, span.start ) + pagetext.substring( span.end );
textbox.value = pagetext;
var summary = editForm.wpSummary;
if ( !summary ) {
throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' );
}
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorRemoveSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1',
( reason.length ? reason + ': ' : '' ) + self.model.wiki
);
} catch ( ex ) {
failure( null, ex );
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm,
function ( request ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
if ( !revision_id ) {
failureFunc( request, new Error( 'Revision ID not found. Please reload the page.' ) );
return;
}
mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
self.destroy();
},
function ( request, ex ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
failureFunc( request, ex );
}
);
},
function ( request, ex ) {
// Failure. What now? TODO: Implement some kind of user feedback.
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
}
);
return true;
},
destroy: function () {
if ( this.view ) { LAPI.DOM.removeNode( this.view ); }
if ( this.dummy ) { LAPI.DOM.removeNode( this.dummy ); }
if ( this.tooltip ) { this.tooltip.hide_now(); }
if ( this.model && this.model.id > 0 && this.viewer ) { this.viewer.deregister( this ); }
this.model = null;
this.view = null;
this.content = null;
this.tooltip = null;
this.viewer = null;
},
area: function () {
if ( !this.model || !this.model.dimension ) { return 0; }
return ( this.model.dimension.w * this.model.dimension.h );
},
cannotEdit: function () {
if ( this.content && this.content.button_section ) {
LAPI.DOM.removeNode( this.content.button_section );
this.content.button_section = null;
if ( this.tooltip ) { this.tooltip.size_change(); }
}
}
}; // end ImageAnnotation
var ImageAnnotationEditor = function () { this.initialize.apply( this, arguments ); };
ImageAnnotationEditor.prototype = {
initialize: function () {
var editor_width = 50;
// Respect potential user-defined width setting
if ( window.ImageAnnotationEditor_columns &&
!isNaN( window.ImageAnnotationEditor_columns ) &&
window.ImageAnnotationEditor_columns >= 30 &&
window.ImageAnnotationEditor_columns <= 100 ) {
editor_width = window.ImageAnnotationEditor_columns;
}
this.editor = new LAPI.Edit(
'', editor_width, 6,
{
box: ImageAnnotator.UI.get( 'wpImageAnnotatorEditorLabel', false ),
preview: ImageAnnotator.UI.get( 'wpImageAnnotatorPreview', true ).capitalizeFirst(),
save: ImageAnnotator.UI.get( 'wpImageAnnotatorSave', true ).capitalizeFirst(),
revert: ImageAnnotator.UI.get( 'wpImageAnnotatorRevert', true ).capitalizeFirst(),
cancel: ImageAnnotator.UI.get( 'wpImageAnnotatorCancel', true ).capitalizeFirst(),
nullsave: ImageAnnotator_config.mayDelete() ?
ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ).capitalizeFirst() :
null,
post: ImageAnnotator.UI.get( 'wpImageAnnotatorCopyright', false )
},
{
onsave: this.save.bind( this ),
onpreview: this.onpreview.bind( this ),
oncancel: this.cancel.bind( this ),
ongettext: function ( text ) {
if ( text == null ) { return ''; }
text = text.trim()
.replace( /\{\{(\s*ImageNote(End)?\s*\|)/g, '{{$1' );
// Guard against people trying to break notes on purpose
if ( text.length && typeof TextCleaner !== 'undefined' ) { text = TextCleaner.sanitizeWikiText( text, true ); }
return text;
}
}
);
this.box = LAPI.make( 'div' );
this.box.appendChild( this.editor.getView() );
// Limit the width of the bounding box to the size of the textarea, taking into account the
// tooltip styles. Do *not* simply append this.box or the editor view, Opera behaves strangely
// if textboxes were ever hidden through a visibility setting! Use a second throw-away textbox
// instead.
var temp = LAPI.make( 'div', null, IA.tooltip_styles );
temp.appendChild( LAPI.make( 'textarea', { cols: editor_width, rows: 6 } ) );
Object.merge(
{
position: 'absolute',
top: '0px',
left: '-10000px',
visibility: 'hidden'
},
temp.style
);
document.body.appendChild( temp );
// Now we know how wide this textbox will be
var box_width = temp.offsetWidth;
LAPI.DOM.removeNode( temp );
// Note: we need to use a tooltip with a dynamic content creator function here because
// static content is cloned inside the Tooltip. Cloning on IE loses all attached handlers,
// and thus the editor's controls wouldn't work anymore. (This is not a problem on FF3,
// where cloning preserves the handlers.)
this.tooltip = new Tooltip(
IA.get_cover(),
this.get_editor.bind( this ),
{
activate: Tooltip.NONE, // We'll always show it explicitly
deactivate: Tooltip.ESCAPE,
close_button: null, // We have a cancel button anyway
mode: Tooltip.FIXED,
anchor: Tooltip.TOP_LEFT,
mouse_offset: { x: 10, y: 10, dx: 1, dy: 1 }, // Misuse this: fixed offset from view
max_pixels: ( box_width ? box_width + 20 : 0 ), // + 20 gives some slack
z_index: 2010, // Above the cover.
open_delay: 0,
hide_delay: 0,
onclose: this.close_tooltip.bind( this )
},
IA.tooltip_styles
);
this.note = null;
this.visible = false;
LAPI.Evt.listenTo( this, this.tooltip.popup, IA.mouse_in,
function ( evt ) {
Array.forEach( IA.viewers, ( function ( viewer ) {
if ( viewer != this.viewer && viewer.visible ) { viewer.hide(); }
} ).bind( this ) );
}
);
},
get_editor: function () {
return this.box;
},
editNote: function ( note ) {
var same_note = ( note == this.note );
this.note = note;
this.viewer = this.note.viewer;
var cover = IA.get_cover();
cover.style.cursor = 'auto';
IA.show_cover();
if ( note.tooltip ) { note.tooltip.hide_now(); }
IA.is_editing = true;
if ( note.content && !IA.wiki_read ) {
// Existing note, and we don't have the wikitext yet: go get it
var self = this;
LAPI.Ajax.apiGet(
'query',
{
prop: 'revisions',
titles: mw.config.get( 'wgPageName' ),
rvlimit: 1,
rvstartid: mw.config.get( 'wgCurRevisionId' ),
rvprop: 'ids|content'
},
function ( request, json_result ) {
if ( json_result && json_result.query && json_result.query.pages ) {
// Should have only one page here
for ( var page in json_result.query.pages ) {
var p = json_result.query.pages[ page ];
if ( p && p.revisions && p.revisions.length ) {
var rev = p.revisions[ 0 ];
if ( rev.revid == mw.config.get( 'wgCurRevisionId' ) && rev[ '*' ] && rev[ '*' ].length ) { IA.setWikitext( rev[ '*' ] ); }
}
break;
}
}
// TODO: What upon a failure?
self.open_editor( same_note, cover );
},
function ( request ) {
// TODO: What upon a failure?
self.open_editor( same_note, cover );
}
);
} else {
this.open_editor( same_note, cover );
}
},
open_editor: function ( same_note, cover ) {
this.editor.hidePreview();
if ( !same_note || this.editor.textarea.readOnly ) {
// Different note, or save error last time
this.editor.setText( this.note.model.wiki );
}
this.editor.enable( LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL );
this.editor.textarea.readOnly = false;
this.editor.textarea.style.backgroundColor = 'white';
// Set the position relative to the note's view.
var view_pos = LAPI.Pos.position( this.note.view );
var origin = LAPI.Pos.position( cover );
this.tooltip.options.fixed_offset.x =
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
this.tooltip.options.fixed_offset.y =
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
this.tooltip.options.fixed_offset.dx = 1;
this.tooltip.options.fixed_offset.dy = 1;
// Make sure mouse event listeners are removed, especially on IE.
this.dim = { x: this.note.view.offsetLeft, y: this.note.view.offsetTop,
w: this.note.view.offsetWidth, h: this.note.view.offsetHeight };
this.viewer.setShowHideEvents( false );
this.viewer.hide(); // Make sure notes are hidden
this.viewer.toggle( true ); // Show all note rectangles (but only the dummies)
// Now show the editor
this.tooltip.show_tip( null, false );
var tpos = LAPI.Pos.position( this.editor.textarea );
var ppos = LAPI.Pos.position( this.tooltip.popup );
tpos = tpos.x - ppos.x;
if ( tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth ) { this.editor.textarea.style.width = ( this.tooltip.popup.offsetWidth - 2 * tpos ) + 'px'; }
if ( LAPI.Browser.is_ie ) {
// Fixate textarea width to prevent ugly flicker on each keypress in IE6...
this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
}
this.visible = true;
},
hide_editor: function ( evt ) {
if ( !this.visible ) { return; }
this.visible = false;
IA.is_editing = false;
this.tooltip.hide_now( evt );
if ( evt && evt.type == 'keydown' && !this.saving ) {
// ESC pressed on new note before a save attempt
this.cancel();
}
IA.hide_cover();
this.viewer.setDefaultMsg();
this.viewer.setShowHideEvents( true );
this.viewer.hide();
this.viewer.show(); // Make sure we get the real views again.
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
// may show the note rectangles even though the mouse is now outside the image. (It was
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
// pressed, it may actually be anywhere.)
},
save: function ( editor ) {
var data = editor.getText();
if ( !data || !data.length ) {
// Empty text
if ( this.note.remove() ) {
this.hide_editor();
this.cancel();
this.note = null;
} else {
this.hide_editor();
this.cancel();
}
return;
} else if ( data == this.note.model.wiki ) {
// Text unchanged
this.hide_editor();
this.cancel();
return;
}
// Construct what to insert
var dim = Object.clone( this.note.model.dimension );
if ( !dim ) {
dim = {
x: Math.round( this.dim.x * this.viewer.factors.dx ),
y: Math.round( this.dim.y * this.viewer.factors.dy ),
w: Math.round( this.dim.w * this.viewer.factors.dx ),
h: Math.round( this.dim.h * this.viewer.factors.dy )
};
// Make sure everything is within bounds
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( dim.w > this.dim.w * this.viewer.factors.dx ) {
dim.w--;
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( dim.x > 0 ) { dim.x--; } else { dim.w = this.viewer.full_img.width; }
}
} else {
// Width already was rounded down
if ( dim.x > 0 ) { dim.x--; }
}
}
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( dim.h > this.dim.h * this.viewer.factors.dy ) {
dim.h--;
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( dim.y > 0 ) { dim.y--; } else { dim.h = this.viewer.full_img.height; }
}
} else {
// Height already was rounded down
if ( dim.y > 0 ) { dim.y--; }
}
}
// If still too large, adjust width and height
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( this.viewer.full_img.width > dim.x ) {
dim.w = this.viewer.full_img.width - dim.x;
} else {
dim.x = this.viewer.full_img.width - 1;
dim.w = 1;
}
}
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( this.viewer.full_img.height > dim.y ) {
dim.h = this.viewer.full_img.height - dim.y;
} else {
dim.y = this.viewer.full_img.height - 1;
dim.h = 1;
}
}
}
this.to_insert =
'{{ImageNote' +
'|id=' + this.note.model.id +
'|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h +
'|dimx=' + this.viewer.full_img.width +
'|dimy=' + this.viewer.full_img.height +
'|style=2' +
'}}\n' +
data + ( data.endsWith( '\n' ) ? '' : '\n' ) +
'{{ImageNoteEnd|id=' + this.note.model.id + '}}';
// Now edit the page
var self = this;
this.editor.busy( true );
this.editor.enable( 0 ); // Disable all buttons
this.saving = true;
LAPI.Ajax.editPage(
mw.config.get( 'wgPageName' ),
function ( doc, editForm, failureFunc, revision_id ) {
try {
if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) {
// Page was edited since the user loaded it.
throw new Error( '#Page version (revision ID) mismatch: edit conflict.' );
}
// Modify the page
var textbox = editForm.wpTextbox1;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
var pagetext = textbox.value;
IA.setWikitext( pagetext );
var span = null;
if ( self.note.content ) { // Otherwise it's a new note!
span = IA.findNote( pagetext, self.note.model.id );
}
if ( span ) { // Replace
pagetext =
pagetext.substring( 0, span.start ) +
self.to_insert +
pagetext.substring( span.end );
} else { // If not found, append
// Try to append right after existing notes
var lastNote = pagetext.lastIndexOf( '{{ImageNoteEnd|id=' );
if ( lastNote >= 0 ) {
var endLastNote = pagetext.substring( lastNote ).indexOf( '}}' );
if ( endLastNote < 0 ) {
endLastNote = pagetext.substring( lastNote ).indexOf( '\n' );
if ( endLastNote < 0 ) { lastNote = -1; } else { lastNote += endLastNote; }
} else { lastNote += endLastNote + 2; }
}
if ( lastNote >= 0 ) {
pagetext =
pagetext.substring( 0, lastNote ) +
'\n' + self.to_insert +
pagetext.substring( lastNote );
} else { pagetext = pagetext.trimRight() + '\n' + self.to_insert; }
}
textbox.value = pagetext;
var summary = editForm.wpSummary;
if ( !summary ) { throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' ); }
// If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary!
if ( self.note.content != null ) {
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorChangeSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1',
data
);
} else {
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorAddSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1',
data
);
}
} catch ( ex ) {
failureFunc( null, ex );
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm,
function ( request ) {
// After a successful submit.
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
// TODO: Actually, the edit got through here, so calling failureFunc on
// inconsistencies isn't quite right. Should we reload the page?
var id = 'image_annotation_content_' + self.note.model.id;
var doc = LAPI.Ajax.getHTML( request, failureFunc, id );
if ( !doc ) { return; }
var html = LAPI.$( id, doc );
if ( !html ) {
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
failureFunc( request, new Error( '#Note not found after saving. Please reload the page.' ) );
return;
}
var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
if ( !revision_id ) {
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
failureFunc( request, new Error( '#Version inconsistency after saving. Please reload the page.' ) );
return;
}
mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!
self.note.model.html = LAPI.DOM.importNode( document, html, true );
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
self.note.model.dimension = dim; // record dimension
self.note.model.html.style.display = '';
self.note.model.wiki = data;
self.editor.busy( false );
if ( self.note.content ) {
LAPI.DOM.removeChildren( self.note.content.main );
self.note.content.main.appendChild( self.note.model.html );
} else {
// New note.
self.note.display(); // Actually a misnomer. Just creates 'content'.
if ( self.viewer.annotations.length > 1 ) {
self.viewer.annotations.sort( ImageAnnotation.compare );
var idxOfNote = Array.indexOf( self.viewer.annotations, self.note );
if ( idxOfNote + 1 < self.viewer.annotations.length ) {
LAPI.DOM.insertNode( self.note.view, self.viewer.annotations[ idxOfNote + 1 ].view );
}
}
}
self.to_insert = null;
self.saving = false;
if ( !self.note.tooltip ) { self.note.setTooltip(); }
self.hide_editor();
IA.is_editing = false;
self.editor.setText( data ); // In case the same note is re-opened: start new undo cycle
}
, function ( request, ex ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
failureFunc( request, ex );
}
);
},
function ( request, ex ) {
self.editor.busy( false );
self.saving = false;
// TODO: How and where to display error if user closed editor through ESC (or through
// opening another tooltip) in the meantime?
if ( !self.visible ) { return; }
// Change the tooltip to show the error.
self.editor.setText( self.to_insert );
// Error message. Use preview field for this.
var error_msg = ImageAnnotator.UI.get( 'wpImageAnnotatorSaveError', false );
var lk = getElementsByClassName( error_msg, 'span', 'wpImageAnnotatorOwnPageLink' );
if ( lk && lk.length && lk[ 0 ].firstChild.nodeName.toLowerCase() === 'a' ) {
lk = lk[ 0 ].firstChild;
lk.href = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( mw.config.get( 'wgPageName' ) ) ) + '?action=edit';
}
if ( ex ) {
var ex_msg = LAPI.formatException( ex, true );
if ( ex_msg ) {
ex_msg.style.borderBottom = '1px solid red';
var tmp = LAPI.make( 'div' );
tmp.appendChild( ex_msg );
tmp.appendChild( error_msg );
error_msg = tmp;
}
}
self.editor.setPreview( error_msg );
self.editor.showPreview();
self.editor.textarea.readOnly = true;
// Force a light gray background, since IE has no visual readonly indication.
self.editor.textarea.style.backgroundColor = '#EEEEEE';
self.editor.enable( LAPI.Edit.CANCEL ); // Disable all other buttons
}
);
},
onpreview: function ( editor ) {
if ( this.tooltip ) { this.tooltip.size_change(); }
},
cancel: function ( editor ) {
if ( !this.note ) { return; }
if ( !this.note.content ) {
// No content: Cancel and remove this note!
this.note.destroy();
this.note = null;
}
if ( editor ) { this.hide_editor(); }
},
close_tooltip: function ( tooltip, evt ) {
this.hide_editor( evt );
this.cancel();
}
};
var ImageNotesViewer = function () { this.initialize.apply( this, arguments ); };
ImageNotesViewer.prototype = {
initialize: function ( descriptor, may_edit ) {
Object.merge( descriptor, this );
this.annotations = [];
this.max_id = 0;
this.main_div = null;
this.msg = null;
this.may_edit = may_edit;
this.setup_done = false;
this.tip = null;
this.icon = null;
this.factors = {
dx: this.full_img.width / this.thumb.width,
dy: this.full_img.height / this.thumb.height
};
if ( !this.isThumbnail && !this.isOther ) {
this.setup();
} else {
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
// a foreign repository (the Commons). Otherwise a later information load might fail because
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
// also locally.
this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 );
}
},
setup: function ( onlyIcon ) {
this.setup_done = true;
var name = this.realName;
if ( this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax ) {
this.imgName = this.realName;
this.realName = '';
} else {
name = getElementsByClassName( this.scope, '*', 'wpImageAnnotatorFullName' );
this.realName = ( ( name && name.length ) ? LAPI.DOM.getInnerText( name[ 0 ] ) : '' );
this.imgName = this.realName;
}
var annotations = getElementsByClassName( this.scope, 'div', IA.annotation_class );
if ( !this.may_edit && ( !annotations || annotations.length === 0 ) ) { return; } // Nothing to do
// A div inserted around the image. It ensures that everything we add is positioned properly
// over the image, even if the browser window size changes and re-layouts occur.
var isEnabledImage = LAPI.DOM.hasClass( this.scope, 'wpImageAnnotatorEnable' );
if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } );
var floater = LAPI.make(
'div', null,
{
cssFloat: ( IA.is_rtl ? 'right' : 'left' ),
styleFloat: ( IA.is_rtl ? 'right' : 'left' ), // For IE...
width: String( this.thumb.width ) + 'px',
position: 'relative' // Fixes IE layout bugs...
}
);
floater.appendChild( this.img_div );
this.img.parentNode.parentNode.insertBefore( floater, this.img.parentNode );
this.img_div.appendChild( this.img.parentNode );
// And now a clear:left to make the rest appear below the image, as usual.
var breaker = LAPI.make( 'div', null, { clear: ( IA.is_rtl ? 'right' : 'left' ) } );
LAPI.DOM.insertAfter( breaker, floater );
// Remove spurious br tag.
if ( breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase() == 'br' ) { LAPI.DOM.removeNode( breaker.nextSibling ); }
} else if ( this.isOther || isEnabledImage ) {
this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } );
this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
this.img_div.appendChild( this.img.parentNode );
// Insert one more to have a file_div, so that we can align the message text correctly
this.file_div = LAPI.make( 'div', null, { width: String( this.thumb.width ) + 'px' } );
this.img_div.parentNode.insertBefore( this.file_div, this.img_div );
this.file_div.appendChild( this.img_div );
} else { // Thumbnail
this.img_div = LAPI.make(
'div',
{ className: 'thumbimage' },
{ position: 'relative', width: String( this.thumb.width ) + 'px' }
);
this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
this.img.style.border = 'none';
this.img_div.appendChild( this.img.parentNode );
}
if (
( this.isThumbnail || this.isOther ) && !this.may_edit &&
(
onlyIcon ||
this.iconOnly ||
ImageAnnotator_config.inlineImageUsesIndicator(
name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
)
)
) {
// Use an onclick handler instead of a link around the image. The link may have a default white
// background, but we want to be sure to have transparency. The image should be an 8-bit indexed
// PNG or a GIF and have a transparent background.
this.icon = ImageAnnotator.UI.get( 'wpImageAnnotatorIndicatorIcon', false );
if ( this.icon ) { this.icon = this.icon.firstChild; } // Skip the message container span or div
// Guard against misconfigurations
if (
this.icon &&
this.icon.nodeName.toLowerCase() == 'a' &&
this.icon.firstChild.nodeName.toLowerCase() == 'img'
) {
// Make sure we use the right protocol:
var srcFixed = this.icon.firstChild.getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol );
this.icon.firstChild.src = srcFixed;
this.icon.firstChild.title = this.icon.title;
this.icon = this.icon.firstChild;
} else if ( !this.icon || this.icon.nodeName.toLowerCase() !== 'img' ) {
this.icon = LAPI.DOM.makeImage(
IA.indication_icon,
14, 14,
ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', true ) || ''
);
}
Object.merge(
{ position: 'absolute', zIndex: 1000, top: '0px', cursor: 'pointer' }
, this.icon.style
);
this.icon.onclick = ( function () { location.href = this.img.parentNode.href; } ).bind( this );
if ( IA.is_rtl ) { this.icon.style.right = '0px'; } else { this.icon.style.left = '0px'; }
this.img_div.appendChild( this.icon );
// And done. We just show the icon, no fancy event handling needed.
return;
}
// Set colors
var colors = IA.getRawItem( 'colors', this.scope );
this.outer_border =
colors && IA.getItem( 'outer', colors ) || IA.outer_border;
this.inner_border =
colors && IA.getItem( 'inner', colors ) || IA.inner_border;
this.active_border =
colors && IA.getItem( 'active', colors ) || IA.active_border;
if ( annotations ) {
for ( var i = 0; i < annotations.length; i++ ) {
var id = annotations[ i ].id;
if ( id && /^image_annotation_note_(\d+)$/.test( id ) ) {
id = parseInt( id.substring( 'image_annotation_note_'.length ) );
} else { id = null; }
if ( id ) {
if ( id > this.max_id ) { this.max_id = id; }
var w = IA.getIntItem( 'full_width_' + id, this.scope );
var h = IA.getIntItem( 'full_height_' + id, this.scope );
if (
w == this.full_img.width && h == this.full_img.height &&
!Array.exists( this.annotations, function ( note ) { return note.model.id == id; } )
) {
try {
this.register( new ImageAnnotation( annotations[ i ], this, id ) );
} catch ( ex ) {
// Swallow.
}
}
}
}
}
if ( this.annotations.length > 1 ) { this.annotations.sort( ImageAnnotation.compare ); }
// Add the rectangles of existing notes to the DOM now that they are sorted.
Array.forEach( this.annotations, ( function ( note ) { this.img_div.appendChild( note.view ); } ).bind( this ) );
if ( this.isThumbnail ) {
this.main_div = getElementsByClassName( this.file_div, 'div', 'thumbcaption' );
if ( !this.main_div || this.main_div.length == 0 ) { this.main_div = null; } else { this.main_div = this.main_div[ 0 ]; }
}
if ( !this.main_div ) {
this.main_div = LAPI.make( 'div' );
if ( IA.is_rtl ) {
this.main_div.style.direction = 'rtl';
this.main_div.style.textAlign = 'right';
this.main_div.className = 'rtl';
} else {
this.main_div.style.textAlign = 'left';
}
if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
LAPI.DOM.insertAfter( this.main_div, this.file_div );
} else {
LAPI.DOM.insertAfter( this.main_div, this.img_div );
}
}
if (
!( this.isThumbnail || this.isOther ) ||
!this.noCaption &&
!IA.hideCaptions &&
ImageAnnotator_config.displayCaptionInArticles(
name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
)
) {
this.msg = LAPI.make( 'div', null, { display: 'none' } );
if ( IA.is_rtl ) {
this.msg.style.direction = 'rtl';
this.msg.className = 'rtl';
}
if ( this.isThumbnail ) { this.msg.style.fontSize = '90%'; }
this.main_div.appendChild( this.msg );
}
// Set overflow parents, if any
var simple = !!window.getComputedStyle;
var checks = ( simple ? [ 'overflow', 'overflow-x', 'overflow-y' ] :
[ 'overflow', 'overflowX', 'overflowY' ]
);
var curStyle = null;
for ( var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode ) {
curStyle = ( simple ? window.getComputedStyle( up, null ) : ( up.currentStyle || up.style ) );
// "up.style" is actually incorrect, but a best-effort fallback.
var overflow = Array.any( checks, function ( t ) {
var o = curStyle[ t ];
return ( o && o != 'visible' ) ? o : null;
} );
if ( overflow ) {
if ( !this.overflowParents ) { this.overflowParents = [ up ]; } else { this.overflowParents[ this.overflowParents.length ] = up; }
}
}
if ( this.overflowParents && this.may_edit ) {
// Forbid editing if we have such a crazy layout.
this.may_edit = false;
IA.may_edit = false;
}
this.show_evt = LAPI.Evt.makeListener( this, this.show );
if ( this.overflowParents || LAPI.Browser.is_ie ) {
// If we have overflowParents, also use a mousemove listener to show/hide the whole
// view (FF doesn't send mouseout events if the visible border is still within the image, i.e.,
// if not the whole image is visible). On IE, also use this handler to show/hide the notes
// if we're still within the visible area of the image. IE passes through mouse_over events to
// the img even if the mouse is within a note's rectangle. Apparently is doesn't handle
// transparent divs correctly. As a result, the notes will pop up and disappear only when the
// mouse crosses the border, and if one moves the mouse a little fast across the border, we
// don't get any event at all. That's no good.
this.move_evt = LAPI.Evt.makeListener( this, this.check_hide );
} else { this.hide_evt = LAPI.Evt.makeListener( this, this.hide ); }
this.move_listening = false;
this.setShowHideEvents( true );
this.visible = false;
this.setDefaultMsg();
},
cannotEdit: function () {
if ( !this.may_edit ) { return; }
this.may_edit = false;
Array.forEach( this.annotations, function ( note ) { note.cannotEdit(); } );
},
setShowHideEvents: function ( set ) {
if ( this.icon ) { return; }
if ( set ) {
LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt );
if ( this.hide_evt ) { LAPI.Evt.attach( this.img, IA.mouse_out, this.hide_evt ); }
} else {
LAPI.Evt.remove( this.img, IA.mouse_in, this.show_evt );
if ( this.hide_evt ) {
LAPI.Evt.remove( this.img, IA.mouse_out, this.hide_evt );
} else if ( this.move_listening ) { this.removeMoveListener(); }
}
},
removeMoveListener: function () {
if ( this.icon ) { return; }
this.move_listening = false;
if ( this.move_evt ) {
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
LAPI.Evt.remove( document, 'mousemove', this.move_evt, true );
}
},
adjustRectangleSize: function ( node ) {
if ( this.icon ) { return; }
// Make sure the note boxes don't overlap the image boundary; we might get an event
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
var view_x = node.offsetLeft;
var view_y = node.offsetTop;
var view_w = node.offsetWidth;
var view_h = node.offsetHeight;
if ( view_x === 0 ) { view_x = 1; }
if ( view_y === 0 ) { view_y = 1; }
if ( view_x + view_w >= this.thumb.width ) {
view_w = this.thumb.width - view_x - 1;
if ( view_w <= 4 ) { view_w = 4; view_x = this.thumb.width - view_w - 1; }
}
if ( view_y + view_h >= this.thumb.height ) {
view_h = this.thumb.height - view_y - 1;
if ( view_h <= 4 ) { view_h = 4; view_y = this.thumb.height - view_h - 1; }
}
// Now set position and width and height, subtracting cumulated border widths
if (
view_x != node.offsetLeft || view_y != node.offsetTop ||
view_w != node.offsetWidth || view_h != node.offsetHeight
) {
node.style.top = String( view_y ) + 'px';
node.style.left = String( view_x ) + 'px';
node.style.width = String( view_w - 2 ) + 'px';
node.style.height = String( view_h - 2 ) + 'px';
node.firstChild.style.width = String( view_w - 4 ) + 'px';
node.firstChild.style.height = String( view_h - 4 ) + 'px';
}
},
toggle: function ( dummies ) {
var i;
if ( !this.annotations || this.annotations.length === 0 || this.icon ) { return; }
if ( dummies ) {
for ( i = 0; i < this.annotations.length; i++ ) {
this.annotations[ i ].view.style.display = 'none';
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' );
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].dummy ); }
}
} else {
for ( i = 0; i < this.annotations.length; i++ ) {
this.annotations[ i ].dummy.style.display = 'none';
this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' );
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].view ); }
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
}
}
this.visible = !this.visible;
},
show: function ( evt ) {
if ( this.visible || this.icon ) { return; }
this.toggle( IA.is_adding || IA.is_editing );
if ( this.move_evt && !this.move_listening ) {
LAPI.Evt.attach( document, 'mousemove', this.move_evt, true );
this.move_listening = true;
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
}
},
hide: function ( evt ) {
if ( this.icon ) { return true; }
if ( !this.visible ) {
// Huh?
if ( this.move_listening ) { this.removeMoveListener(); }
return true;
}
if ( evt ) {
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( mouse_pos ) {
if ( this.tip ) {
// Check whether we're within the visible note.
if ( LAPI.Pos.isWithin( this.tip.popup, mouse_pos.x, mouse_pos.y ) ) { return true; }
}
var is_within = true;
var img_pos = LAPI.Pos.position( this.img );
var rect = {
x: img_pos.x,
y: img_pos.y,
r: ( img_pos.x + this.img.offsetWidth ),
b: ( img_pos.y + this.img.offsetHeight )
};
var i;
if ( this.overflowParents ) {
// We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set.
// Compute the actually visible region by intersecting the rectangle given by img_pos and
// this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents.
function intersect_rectangles( a, b ) {
if ( b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y ) { return { x: 0, y: 0, r: 0, b: 0 }; }
return {
x: Math.max( a.x, b.x ),
y: Math.max( a.y, b.y ),
r: Math.min( a.r, b.r ),
b: Math.min( a.b, b.b )
};
}
for ( i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++ ) {
img_pos = LAPI.Pos.position( this.overflowParents[ i ] );
img_pos.r = img_pos.x + this.overflowParents[ i ].clientWidth;
img_pos.b = img_pos.y + this.overflowParents[ i ].clientHeight;
rect = intersect_rectangles( rect, img_pos );
}
}
is_within = !( rect.x >= rect.r || rect.y >= rect.b || // Empty rectangle
rect.x >= mouse_pos.x || rect.r <= mouse_pos.x ||
rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
);
if ( is_within ) {
if ( LAPI.Browser.is_ie && evt.type === 'mousemove' ) {
var display;
// Loop in reverse order to properly display top rectangle's note!
for ( i = this.annotations.length - 1; i >= 0; i-- ) {
display = this.annotations[ i ].view.style.display;
if (
display !== 'none' && display != null &&
LAPI.Pos.isWithin( this.annotations[ i ].view.firstChild, mouse_pos.x, mouse_pos.y )
) {
if ( !this.annotations[ i ].tooltip.visible ) { this.annotations[ i ].tooltip.show( evt ); }
return true;
}
}
if ( this.tip ) { this.tip.hide_now(); } // Inside the image, but not within any note rectangle
}
return true;
}
}
}
// Not within the image, or forced hiding (no event)
if ( this.move_listening ) { this.removeMoveListener(); }
this.toggle( IA.is_adding || IA.is_editing );
return true;
},
check_hide: function ( evt ) {
if ( this.icon ) { return true; }
if ( this.visible ) { this.hide( evt ); }
return true;
},
register: function ( new_note ) {
this.annotations[ this.annotations.length ] = new_note;
if ( new_note.model.id > 0 ) {
if ( new_note.model.id > this.max_id ) { this.max_id = new_note.model.id; }
} else {
new_note.model.id = ++this.max_id;
}
},
deregister: function ( note ) {
Array.remove( this.annotations, note );
if ( note.model.id == this.max_id ) { this.max_id--; }
if ( this.annotations.length === 0 ) { this.setDefaultMsg(); } // If we removed the last one, clear the msg
},
setDefaultMsg: function () {
if ( this.annotations && this.annotations.length && this.msg ) {
LAPI.DOM.removeChildren( this.msg );
this.msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', false ) );
if ( this.realName && typeof this.realName === 'string' && this.realName.length ) {
var otherPageMsg = ImageAnnotator.UI.get( 'wpImageAnnotatorEditNotesMsg', false );
if ( otherPageMsg ) {
var lk = otherPageMsg.getElementsByTagName( 'a' );
if ( lk && lk.length ) {
lk = lk[ 0 ];
lk.parentNode.replaceChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( this.realName ) ),
this.realName,
this.realName
),
lk
);
this.msg.appendChild( otherPageMsg );
}
}
}
this.msg.style.display = '';
} else {
if ( this.msg ) { this.msg.style.display = 'none'; }
}
if ( IA.button_div && this.may_edit ) { IA.button_div.style.display = ''; }
}
};
var IA = {
// This object is responsible for setting up annotations when a page is loaded. It loads all
// annotations in the page source, and adds an "Annotate this image" button plus the support
// for drawing rectangles onto the image if there is only one image and editing is allowed.
haveAjax: false,
button_div: null,
add_button: null,
cover: null,
border: null,
definer: null,
mouse_in: ( window.ActiveXObject ? 'mouseenter' : 'mouseover' ),
mouse_out: ( window.ActiveXObject ? 'mouseleave' : 'mouseout' ),
annotation_class: 'image_annotation',
// Format of notes in Wikitext. Note: there are two formats, an old one and a new one.
// We only write the newest (last) one, but we can read also the older formats. Order is
// important, because the old format also used the ImageNote template, but for a different
// purpose.
note_delim: [
{
start: '<div id="image_annotation_note_$1"',
end: '</div><!-- End of annotation $1-->',
content_start: '<div id="image_annotation_content_$1">\n',
content_end: '</div>\n<span id="image_annotation_wikitext_$1"'
},
{
start: '{{ImageNote|id=$1',
end: '{{ImageNoteEnd|id=$1}}',
content_start: '}}\n',
content_end: '{{ImageNoteEnd|id=$1}}'
}
],
tooltip_styles: { // The style for all our tooltips
border: '1px solid #8888aa',
backgroundColor: '#ffffe0',
padding: '0.3em',
fontSize: ( ( mw.config.get( 'skin' ) == 'monobook' || mw.config.get( 'skin' ) == 'modern' ) ? '127%' : '100%' )
// Scale up to default text size
},
editor: null,
wiki_read: false,
is_rtl: false,
move_listening: false,
is_tracking: false,
is_adding: false,
is_editing: false,
zoom_threshold: 8.0,
zoom_factor: 4.0,
install_attempts: 0,
max_install_attempts: 10, // Maximum 5 seconds
imgs_with_notes: [],
thumbs: [],
other_images: [],
// Fallback
indication_icon: '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',
install: function ( config ) {
if ( typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable ) { return; }
if ( !config || ImageAnnotator_config != null ) { return; }
// Double check.
if ( !config.viewingEnabled() ) { return; }
var self = IA;
ImageAnnotator_config = config;
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
// doing too much work.
if (
window.XMLHttpRequest &&
typeof LAPI !== 'undefined' &&
typeof LAPI.Ajax !== 'undefined' &&
typeof LAPI.Ajax.getRequest !== 'undefined'
) {
self.haveAjax = ( LAPI.Ajax.getRequest() != null );
self.ajaxQueried = true;
} else {
self.haveAjax = true; // A pity. May occur on IE. We'll check again later on.
self.ajaxQueried = false;
}
// We'll include self.haveAjax later on once more, to catch the !ajaxQueried case.
self.may_edit = mw.config.get( 'wgNamespaceNumber' ) >= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled();
function namespaceCheck( list ) {
if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) { return false; }
var namespaceIds = mw.config.get( 'wgNamespaceIds' );
if ( !namespaceIds ) { return false; }
var namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
for ( var i = 0; i < list.length; i++ ) {
if (
typeof list[ i ] === 'string' &&
(
list[ i ] === '*' ||
namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber
)
) {
return true;
}
}
return false;
}
self.rules = { inline: {}, thumbs: {}, shared: {} };
// Now set the default rules. Undefined means default setting (true for show, false for icon),
// but overrideable by per-image rules. If set, it's not overrideable by per-image rules.
//
if (
!self.haveAjax ||
!config.generalImagesEnabled() ||
namespaceCheck( window.ImageAnnotator_no_images || null )
) {
self.rules.inline.show = false;
self.rules.thumbs.show = false;
self.rules.shared.show = false;
} else {
if (
!self.haveAjax ||
!config.thumbsEnabled() ||
namespaceCheck( window.ImageAnnotator_no_thumbs || null )
) {
self.rules.thumbs.show = false;
}
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
self.rules.shared.show = true;
} else if ( !config.sharedImagesEnabled() ||
namespaceCheck( window.ImageAnnotator_no_shared || null )
) {
self.rules.shared.show = false;
}
if ( namespaceCheck( window.ImageAnnotator_icon_images || null ) ) { self.rules.inline.icon = true; }
if ( namespaceCheck( window.ImageAnnotator_icon_thumbs || null ) ) { self.rules.thumbs.icon = true; }
}
// User rule for displaying captions on images in articles
self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null );
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
if ( do_images ) {
// Per-article switching off of note display on inline images and thumbnails
var rules = document.getElementById( 'wpImageAnnotatorImageRules' );
if ( rules ) {
if ( rules.className.indexOf( 'wpImageAnnotatorNone' ) >= 0 ) {
self.rules.inline.show = false;
self.rules.thumbs.show = false;
self.rules.shared.show = false;
}
if (
typeof self.rules.inline.show === 'undefined' &&
rules.className.indexOf( 'wpImageAnnotatorDisplay' ) >= 0
) {
self.rules.inline.show = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorNoThumbDisplay' ) >= 0 ) {
self.rules.thumbs.show = false;
}
if (
typeof self.rules.thumbs.show === 'undefined' &&
rules.className.indexOf( 'wpImageAnnotatorThumbDisplay' ) >= 0
) {
self.rules.thumbs.show = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorInlineDisplayIcons' ) >= 0 ) {
self.rules.inline.icon = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorThumbDisplayIcons' ) >= 0 ) {
self.rules.thumbs.icon = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorOnlyLocal' ) >= 0 ) {
self.rules.shared.show = false;
}
}
}
// Make sure the shared value is set
self.rules.shared.show = typeof self.rules.shared.show === 'undefined' || self.rules.shared.show;
var do_thumbs = typeof self.rules.thumbs.show === 'undefined' || self.rules.thumbs.show;
if ( do_images ) {
var bodyContent = document.getElementById( 'bodyContent' ) || // monobook, vector
document.getElementById( 'mw_contentholder' ) || // modern
document.getElementById( 'article' ); // old skins
if ( bodyContent ) {
var all_imgs = bodyContent.getElementsByTagName( 'img' );
// This prevents traversing a page with more than 400 images
// There are extreme cases like [[Emoji]] that high number of images can cause
// huge lag specially on Chrome
if ( all_imgs.length > 400 ) {
// purging the array, simply a hack to avoid more indention
all_imgs = [];
}
for ( var i = 0; i < all_imgs.length; i++ ) {
// Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries.
var up = all_imgs[ i ].parentNode;
if ( up.nodeName.toLowerCase() !== 'a' ) { continue; }
up = up.parentNode;
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' thumbinner ' ) >= 0 ) {
if ( do_thumbs ) { self.thumbs[ self.thumbs.length ] = up; }
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
// Other images not in galleries
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
} else {
// Guard against other scripts adding aribtrary numbers of divs (dshuf for instance!)
var is_other = true;
while ( up && up.nodeName.toLowerCase() == 'div' && is_other ) {
up = up.parentNode;
if ( up ) { is_other = ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) < 0; }
}
if ( is_other ) { self.other_images[ self.other_images.length ] = all_imgs[ i ]; }
}
} // end loop
}
} else {
self.imgs_with_notes = getElementsByClassName( document, '*', 'wpImageAnnotatorEnable' );
if ( do_thumbs ) { self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' ); } // No galleries!
}
if (
mw.config.get( 'wgNamespaceNumber' ) == 6 ||
( self.imgs_with_notes.length ) ||
( self.thumbs.length ) ||
( self.other_images.length )
) {
// Publish parts of config.
ImageAnnotator.UI = config.UI;
self.outer_border = config.outer_border;
self.inner_border = config.inner_border;
self.active_border = config.active_border;
self.new_border = config.new_border;
self.wait_for_required_libraries();
}
},
wait_for_required_libraries: function () {
if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' ) {
if ( IA.install_attempts++ < IA.max_install_attempts ) {
setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
}
return;
}
if ( LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) { return; } // Opera 8 has severe problems
// Get the UI. We're likely to need it if we made it to here.
IA.setup_ui();
IA.setup();
},
setup: function () {
var self = IA;
self.imgs = [];
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
self.is_rtl =
LAPI.DOM.hasClass( document.body, 'rtl' ) ||
(
LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
);
var stylepath = mw.config.get( 'stylepath' ) || '/skin';
// Use this to temporarily display an image off-screen to get its dimensions
var testImgDiv = LAPI.make(
'div', null,
{
display: 'none', position: 'absolute', width: '300px',
overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
}
);
document.body.insertBefore( testImgDiv, document.body.firstChild );
function img_check( img, is_other ) {
var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 );
var srcH = parseInt( img.getAttribute( 'height', 2 ), 10 );
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
// rectangles after all...
if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) { return null; }
// For non-thumbnail images, the size limit is larger.
if ( is_other && ( srcW < 60 || srcH < 60 ) ) { return null; }
var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary...
var h = img.clientHeight;
// If the image is currently hidden, its clientWidth and clientHeight are not set. Try
// harder to get the true width and height:
if ( ( !w || !h ) && img.style.display != 'none' ) {
var copied = img.cloneNode( true );
copied.style.display = '';
testImgDiv.appendChild( copied );
testImgDiv.style.display = '';
w = copied.clientWidth;
h = copied.clientHeight;
testImgDiv.style.display = 'none';
LAPI.DOM.removeNode( copied );
}
// Quit if the image wasn't loaded properly for some reason:
if ( w != srcW || h != srcH ) { return null; }
// Exclude system images
if ( img.src.contains( stylepath ) ) { return null; }
// Only if within a link
if ( img.parentNode.nodeName.toLowerCase() != 'a' ) { return null; }
if ( is_other ) {
// Only if the img-within-link construction is within some element that may contain a div!
if ( /^(p|span)$/i.test( img.parentNode.parentNode.nodeName ) ) {
// Special case: a paragraph may contain only inline elements, but we want to be able to handle
// files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image
// in a div, but for now we assume that all browsers can handle a div within a paragraph or
// a span in a meaningful way, even if that is not really allowed.
} else if ( !/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test( img.parentNode.parentNode.nodeName ) ) { return null; }
}
// Exclude any that are within an image note!
var up = img.parentNode.parentNode;
while ( up != document.body ) {
if ( LAPI.DOM.hasClass( up, IA.annotation_class ) ) { return null; }
up = up.parentNode;
}
return { width: w, height: h };
}
function setup_one( scope ) {
var file_div = scope;
var is_thumb =
scope != document &&
scope.nodeName.toLowerCase() == 'div' &&
LAPI.DOM.hasClass( scope, 'thumbinner' );
var is_other = scope.nodeName.toLowerCase() == 'img';
if ( is_other && self.imgs.length && scope == self.imgs[ 0 ] ) { return null; }
if ( scope == document ) {
file_div = LAPI.$( 'file' );
} else if ( !is_thumb && !is_other ) {
file_div = getElementsByClassName( scope, 'div', 'wpImageAnnotatorFile' );
if ( !file_div || file_div.length != 1 ) { return null; }
file_div = file_div[ 0 ];
}
if ( !file_div ) { return null; }
var img = null;
if ( scope == document ) {
img = LAPI.WP.getPreviewImage( mw.config.get( 'wgTitle' ) );
// TIFFs may be multi-paged: allow only for single-page TIFFs
if ( document.pageselector ) { img = null; }
} else if ( is_other ) {
img = scope;
} else {
img = file_div.getElementsByTagName( 'img' );
if ( !img || img.length === 0 ) { return null; }
img = img[ 0 ];
}
if ( !img ) { return null; }
var dim = img_check( img, is_other );
if ( !dim ) { return null; }
// Conditionally exclude shared images.
if (
scope != document &&
!self.rules.shared.show &&
ImageAnnotator_config.imageIsFromSharedRepository( img.src )
) { return null; }
var name = null;
if ( scope == document ) {
name = mw.config.get( 'wgPageName' );
} else {
name = LAPI.WP.pageFromLink( img.parentNode );
if ( !name ) { return null; }
name = name.replace( / /g, '_' );
if ( is_thumb || is_other ) {
var img_src = decodeURIComponent( img.getAttribute( 'src', 2 ) ).replace( / /g, '_' );
// img_src should have a component "/name" in there somewhere
var colon = name.indexOf( ':' );
if ( colon <= 0 ) { return null; }
var img_name = name.substring( colon + 1 );
if ( img_src.search( new RegExp( '/' + img_name.escapeRE() + '(/.*)?$' ) ) < 0 ) { return null; }
// If the link is not going to file namespace, we won't find the full size later on and
// thus we won't do anything with it.
}
}
if ( name.search( /\.(jpe?g|png|gif|svg|tiff?|webp)$/i ) < 0 ) { return null; } // Only PNG, JPE?G, GIF, SVG, TIFF?, and WebP
// Finally check for wpImageAnnotatorControl
var icon_only = false;
var no_caption = false;
if ( is_thumb || is_other ) {
var up = img.parentNode.parentNode;
// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
for ( var i = 0; ++i <= 3 && up; up = up.parentNode ) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControl' ) ) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOff' ) ) { return null; }
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorIconOnly' ) ) { icon_only = true; }
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorCaptionOff' ) ) { no_caption = true; }
break;
}
}
}
return { scope: scope,
file_div: file_div,
img: img,
realName: name,
isThumbnail: is_thumb,
isOther: is_other,
thumb: { width: dim.width, height: dim.height },
iconOnly: icon_only,
noCaption: no_caption
};
}
function setup_images( list ) {
Array.forEach( list,
function ( elem ) {
var desc = setup_one( elem );
if ( desc ) { self.imgs[ self.imgs.length ] = desc; }
}
);
}
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
setup_images( [ document ] );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
setup_images( self.imgs_with_notes );
} else {
setup_images( self.imgs_with_notes );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
}
self.may_edit = self.may_edit && location.href.search( /[?&]oldid=/ ) < 0;
if ( self.haveAjax ) {
setup_images( self.thumbs );
setup_images( self.other_images );
}
// Remove the test div
LAPI.DOM.removeNode( testImgDiv );
if ( self.imgs.length === 0 ) { return; }
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
// need them. This has in particular a benefit if we do have to query for the file sizes below.
if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) {
// Try to get the full size without Ajax.
self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) {
self.setup_step_two();
return;
}
}
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
// Also avoid using Ajax on IE6...
var cache = {};
var names = [];
Array.forEach( self.imgs, function ( img, idx ) {
if ( cache[ img.realName ] ) {
cache[ img.realName ][ cache[ img.realName ].length ] = idx;
} else {
cache[ img.realName ] = [ idx ];
names[ names.length ] = img.realName;
}
} );
var to_do = names.length;
var done = 0;
function check_done( length ) {
done += length;
if ( done >= names.length ) {
if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) { ImageAnnotator.info_callbacks = null; }
self.setup_step_two();
}
}
function make_calls( execute_call, url_limit ) {
function build_titles( from, length, url_limit ) {
var done = 0;
var text = '';
for ( var i = from; i < from + length; i++ ) {
var new_text = names[ i ];
if ( url_limit ) {
new_text = encodeURIComponent( new_text );
if ( text.length && ( text.length + new_text.length + 1 > url_limit ) ) { break; }
}
text += ( text.length ? '|' : '' ) + new_text;
done++;
}
return { text: text, n: done };
}
var start = 0, chunk = 0, params;
while ( to_do > 0 ) {
params = build_titles( start, Math.min( 50, to_do ), url_limit );
execute_call( params.n, params.text );
to_do -= params.n;
start += params.n;
}
}
function set_info( json ) {
try {
if ( json && json.query && json.query.pages ) {
function get_size( info ) {
if ( !info.imageinfo || info.imageinfo.length === 0 ) { return; }
var title = info.title.replace( / /g, '_' );
var indices = cache[ title ];
if ( !indices ) { return; }
Array.forEach(
indices
, function ( i ) {
self.imgs[ i ].full_img = { width: info.imageinfo[ 0 ].width,
height: info.imageinfo[ 0 ].height };
self.imgs[ i ].has_page = ( typeof info.missing === 'undefined' );
self.imgs[ i ].isLocal = !info.imagerepository || info.imagerepository == 'local';
if ( i != 0 || !self.may_edit || !info.protection || mw.config.get( 'wgNamespaceNumber' ) != 6 ) { return; }
// Care about the protection settings
var protection = Array.any( info.protection, function ( e ) {
return ( e.type == 'edit' ? e : null );
} );
self.may_edit =
!protection ||
( mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( protection.level ) );
}
);
}
for ( var page in json.query.pages ) {
get_size( json.query.pages[ page ] );
}
} // end if
} catch ( ex ) {
}
}
if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case.
ImageAnnotator.info_callbacks = [];
var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' +
'&prop=info|imageinfo&inprop=protection&iiprop=size' +
'&titles=&callback=ImageAnnotator.info_callbacks[].callback';
if ( template.startsWith( '//' ) ) { template = document.location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
make_calls(
function ( length, titles ) {
var idx = ImageAnnotator.info_callbacks.length;
ImageAnnotator.info_callbacks[ idx ] = {
callback: function ( json ) {
set_info( json );
ImageAnnotator.info_callbacks[ idx ].done = true;
if ( ImageAnnotator.info_callbacks[ idx ].script ) {
LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
ImageAnnotator.info_callbacks[ idx ].script = null;
}
check_done( length );
},
done: false
};
ImageAnnotator.info_callbacks[ idx ].script = IA.getScript(
template.replace( 'info_callbacks[].callback', 'info_callbacks[' + idx + '].callback' )
.replace( '&titles=&', '&titles=' + titles + '&' ),
true // No local caching!
);
// We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may
// get the script from the browser's cache, and if that happens, IE may execute the
// script (and call the callback) synchronously before the assignment is done. Clean
// up in that case.
if (
ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[ idx ] &&
ImageAnnotator.info_callbacks[ idx ].done && ImageAnnotator.info_callbacks[ idx ].script
) {
LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
ImageAnnotator.info_callbacks[ idx ].script = null;
}
},
( LAPI.Browser.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
);
} else {
make_calls(
function ( length, titles ) {
LAPI.Ajax.apiGet(
'query'
, { titles: titles,
prop: 'info|imageinfo',
inprop: 'protection',
iiprop: 'size'
}
, function ( request, json_result ) {
set_info( json_result );
check_done( length );
}
, function () { check_done( length ); }
);
}
);
} // end if can use Ajax
},
setup_ui: function () {
// Complete the UI object we've gotten from config.
ImageAnnotator.UI.ready = false;
ImageAnnotator.UI.repo = null;
ImageAnnotator.UI.needs_plea = false;
var readyEvent = [];
ImageAnnotator.UI.fireReadyEvent = function () {
if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do.
ImageAnnotator.UI.ready = true;
// Call all registered handlers, and clear the array.
Array.forEach( readyEvent, function ( f, idx ) {
try { f(); } catch ( ex ) {}
readyEvent[ idx ] = null;
} );
readyEvent = null;
};
ImageAnnotator.UI.addReadyEventHandler = function ( f ) {
if ( ImageAnnotator.UI.ready ) {
f(); // Already fired: call directly
} else {
readyEvent[ readyEvent.length ] = f;
}
};
ImageAnnotator.UI.setup = function () {
if ( ImageAnnotator.UI.repo ) { return; }
var self = ImageAnnotator.UI;
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
if ( typeof UIElements === 'undefined' ) {
self.basic = true;
self.repo = {};
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
self.repo[ item ] = node.firstChild;
LAPI.DOM.removeChildren( node );
}
} else {
self.basic = false;
self.repo = UIElements.emptyRepository( self.defaultLanguage );
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
UIElements.setEntry( item, self.repo, node.firstChild );
LAPI.DOM.removeChildren( node );
}
UIElements.load( 'wpImageAnnotatorTexts', null, null, self.repo );
}
LAPI.DOM.removeNode( node );
};
ImageAnnotator.UI.get = function ( id, basic, no_plea ) {
var self = ImageAnnotator.UI;
if ( !self.repo ) { self.setup(); }
var result = null;
var add_plea = false;
if ( self.basic ) {
result = self.repo[ id ];
} else {
result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null );
add_plea = !result;
if ( !result ) { result = UIElements.getEntry( id, self.repo ); }
}
self.needs_plea = add_plea;
if ( !result ) { return null; } // Hmmm... what happened here? We normally have defaults...
if ( basic ) { return LAPI.DOM.getInnerText( result ).trim(); }
result = result.cloneNode( true );
if ( mw.config.get( 'wgServer' ).contains( '/commons' ) && add_plea && !no_plea ) {
// Add a translation plea.
if ( result.nodeName.toLowerCase() == 'div' ) {
result.appendChild( self.get_plea() );
} else {
var span = LAPI.make( 'span' );
span.appendChild( result );
span.appendChild( self.get_plea() );
result = span;
}
}
return result;
};
ImageAnnotator.UI.get_plea = function () {
var self = ImageAnnotator.UI;
var translate = self.get( 'wpTranslate', false, true ) || 'translate';
var span = LAPI.make( 'small' );
span.appendChild( document.createTextNode( '\xa0(' ) );
span.appendChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
'&action=edit§ion=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
'&language=' + mw.config.get( 'wgUserLanguage' ),
translate,
( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
)
);
span.appendChild( document.createTextNode( ')' ) );
return span;
};
ImageAnnotator.UI.init = function ( html_text_or_json ) {
var text;
if ( typeof html_text_or_json === 'string' ) {
text = html_text_or_json;
} else if (
typeof html_text_or_json !== 'undefined' &&
typeof html_text_or_json.parse !== 'undefined' &&
typeof html_text_or_json.parse.text !== 'undefined' &&
typeof html_text_or_json.parse.text[ '*' ] !== 'undefined'
) {
text = html_text_or_json.parse.text[ '*' ];
} else {
text = null;
}
if ( !text ) {
ImageAnnotator.UI.fireReadyEvent();
return;
}
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
try {
node.innerHTML = text;
} catch ( ex ) {
LAPI.DOM.removeNode( node );
node = null;
// Swallow. We'll just work with the default UI
}
if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); }
ImageAnnotator.UI.fireReadyEvent();
};
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
'|live=1}}';
function get_ui_no_ajax() {
var url =
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' +
'&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400';
// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
// anyway.)
IA.getScript( url, true ); // No local caching!
}
function get_ui() {
IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
IA.ajaxQueried = true;
// Works only with Ajax (but then, most of this script doesn't work without).
// Check what this does to load times... If lots of people used this, it might be better to
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
// would get the texts, even people not using this, but the texts are small anyway...
if ( !IA.haveAjax ) {
get_ui_no_ajax(); // Fallback.
return;
}
LAPI.Ajax.parseWikitext(
ui_page,
ImageAnnotator.UI.init,
ImageAnnotator.UI.fireReadyEvent,
false,
null,
'API', // A fixed string to enable caching at all.
14400 // 4 hour caching.
);
} // end get_ui
if ( !window.XMLHttpRequest && !!window.ActiveXObject ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case. The disadvantage
// is that we don't do anything if this fails for some reason.
get_ui_no_ajax();
} else {
get_ui();
}
},
setup_step_two: function () {
var self = IA;
// Throw out any images for which we miss either the thumbnail or the full image size.
// Also throws out thumbnails that are larger than the full image.
self.imgs = Array.select( self.imgs, function ( elem, idx ) {
var result =
elem.thumb.width > 0 && elem.thumb.height > 0 &&
typeof elem.full_img !== 'undefined' &&
elem.full_img.width > 0 && elem.full_img.height > 0 &&
elem.full_img.width >= elem.thumb.width &&
elem.full_img.height >= elem.thumb.height;
if ( self.may_edit && idx === 0 && !result ) { self.may_edit = false; }
return result;
} );
if ( self.imgs.length === 0 ) { return; }
ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
},
complete_setup: function () {
// We can be sure to have the UI here because this is called only when the ready event of the
// UI object is fired.
var self = IA;
// Check edit permissions
if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
self.may_edit = (
( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
(
mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' &&
mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
)
);
}
if ( self.may_edit ) {
// Check whether the image is local. Don't allow editing if the file is remote.
var sharedUpload = getElementsByClassName( document, 'div', 'sharedUploadNotice' );
self.may_edit = ( !sharedUpload || sharedUpload.length === 0 );
}
if ( self.may_edit && mw.config.get( 'wgNamespaceNumber' ) != 6 ) {
// Only allow edits if the stored page name matches the current one.
var img_page_name = getElementsByClassName( self.imgs[ 0 ].scope, '*', 'wpImageAnnotatorPageName' );
if ( img_page_name && img_page_name.length ) { img_page_name = LAPI.DOM.getInnerText( img_page_name[ 0 ] ); } else { img_page_name = ''; }
self.may_edit = ( img_page_name.replace( / /g, '_' ) == mw.config.get( 'wgTitle' ).replace( / /g, '_' ) );
}
if ( self.may_edit && self.ajaxQueried ) { self.may_edit = self.haveAjax; }
// Now create viewers for all images
self.viewers = new Array( self.imgs.length );
for ( var i = 0; i < self.imgs.length; i++ ) {
self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit );
}
if ( self.may_edit ) {
// Respect user override for zoom, if any
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
if (
typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
!isNaN( window.ImageAnnotator_zoom_threshold ) &&
window.ImageAnnotator_zoom_threshold >= 0.0
) {
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
// zooming.
self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
}
// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
// but only if we *can* zoom at least twice
if (
self.viewers[ 0 ].full_img.width > 300 &&
Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0
) {
if (
self.viewers[ 0 ].thumb.width < 400 ||
self.viewers[ 0 ].thumb.width / self.viewers[ 0 ].thumb.height > 2.0 ||
self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0
) {
self.zoom_threshold = 0; // Force zooming
}
}
self.editor = new ImageAnnotationEditor();
function track( evt ) {
evt = evt || window.event;
if ( self.is_adding ) { self.update_zoom( evt ); }
if ( !self.is_tracking ) { return LAPI.Evt.kill( evt ); }
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
var origin = LAPI.Pos.position( self.cover );
// Make mouse pos relative to cover
mouse_pos.x = mouse_pos.x - origin.x;
mouse_pos.y = mouse_pos.y - origin.y;
if ( mouse_pos.x >= self.base_x ) {
self.definer.style.width = String( mouse_pos.x - self.base_x ) + 'px';
self.definer.style.left = String( self.base_x ) + 'px';
} else {
self.definer.style.width = String( self.base_x - mouse_pos.x ) + 'px';
self.definer.style.left = String( mouse_pos.x ) + 'px';
}
if ( mouse_pos.y >= self.base_y ) {
self.definer.style.height = String( mouse_pos.y - self.base_y ) + 'px';
self.definer.style.top = String( self.base_y ) + 'px';
} else {
self.definer.style.height = String( self.base_y - mouse_pos.y ) + 'px';
self.definer.style.top = String( mouse_pos.y ) + 'px';
}
return LAPI.Evt.kill( evt );
}
function pause( evt ) {
LAPI.Evt.remove( document, 'mousemove', track, true );
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
self.move_listening = false;
}
function resume( evt ) {
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
// addEventListener only.
if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) {
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
LAPI.Evt.attach( document, 'mousemove', track, true );
self.move_listening = true;
}
}
function stop_tracking( evt ) {
evt = evt || window.event;
// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
// browsers, we attach the handler on self.cover and thus we don't even get events outside
// that range.
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
if ( self.is_tracking ) {
self.is_tracking = false;
self.is_adding = false;
// Done.
pause();
if ( LAPI.Browser.is_ie ) {
// Trust Microsoft to get everything wrong!
LAPI.Evt.remove( document, 'mouseup', stop_tracking );
} else {
LAPI.Evt.remove( self.cover, 'mouseup', stop_tracking );
}
LAPI.Evt.remove( window, 'blur', pause );
LAPI.Evt.remove( window, 'focus', resume );
self.cover.style.cursor = 'auto';
LAPI.DOM.removeNode( self.border );
LAPI.Evt.remove( self.cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.remove( self.cover, self.mouse_out, self.hide_zoom_evt );
self.hide_zoom();
self.viewers[ 0 ].hide(); // Hide all existing boxes
if ( !self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0 ) {
// Nothing: just remove the definer:
if ( self.definer ) { LAPI.DOM.removeNode( self.definer ); }
// Re-attach event handlers
self.viewers[ 0 ].setShowHideEvents( true );
self.hide_cover();
self.viewers[ 0 ].setDefaultMsg();
// And make sure we get the real view again
self.viewers[ 0 ].show();
} else {
// We have a div with some extent: remove event capturing and create a new annotation
var new_note = new ImageAnnotation( self.definer, self.viewers[ 0 ], -1 );
self.viewers[ 0 ].register( new_note );
self.editor.editNote( new_note );
}
self.definer = null;
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
function start_tracking( evt ) {
if ( !self.is_tracking ) {
self.is_tracking = true;
evt = evt || window.event;
// Set the position, size 1
var mouse_pos = LAPI.Pos.mousePosition( evt );
var origin = LAPI.Pos.position( self.cover );
self.base_x = mouse_pos.x - origin.x;
self.base_y = mouse_pos.y - origin.y;
Object.merge(
{ left: String( self.base_x ) + 'px',
top: String( self.base_y ) + 'px',
width: '0px',
height: '0px',
display: ''
}
, self.definer.style
);
// Set mouse handlers
LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
if ( LAPI.Browser.is_ie ) {
LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
} else {
LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
}
resume();
LAPI.Evt.attach( window, 'blur', pause );
LAPI.Evt.attach( window, 'focus', resume );
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
function add_new( evt ) {
if ( !self.canEdit() ) { return; }
self.editor.hide_editor();
Tooltips.close();
var cover = self.get_cover();
cover.style.cursor = 'crosshair';
self.definer = LAPI.make(
'div', null,
{
border: '1px solid ' + IA.new_border,
display: 'none',
position: 'absolute',
top: '0px',
left: '0px',
width: '0px',
height: '0px',
padding: '0',
lineHeight: '0px', // IE needs this, even though there are no lines within
fontSize: '0px', // IE
zIndex: cover.style.zIndex - 2 // Below the mouse capture div
}
);
self.viewers[ 0 ].img_div.appendChild( self.definer );
// Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image,
// hence none of our tooltips are visible.
self.viewers[ 0 ].img_div.appendChild( self.border );
self.show_cover();
self.is_tracking = false;
self.is_adding = true;
LAPI.Evt.attach( cover, 'mousedown', start_tracking );
resume();
self.button_div.style.display = 'none';
// Remove the event listeners on the image: IE sometimes invokes them even when the image is covered
self.viewers[ 0 ].setShowHideEvents( false );
self.viewers[ 0 ].hide(); // Make sure notes are hidden
self.viewers[ 0 ].toggle( true ); // Show all note rectangles (but only the dummies)
self.update_zoom_evt = LAPI.Evt.makeListener( self, self.update_zoom );
self.hide_zoom_evt = LAPI.Evt.makeListener( self, self.hide_zoom );
self.show_zoom();
LAPI.Evt.attach( cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.attach( cover, self.mouse_out, self.hide_zoom_evt );
LAPI.DOM.removeChildren( self.viewers[ 0 ].msg );
self.viewers[ 0 ].msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorDrawRectMsg', false ) );
self.viewers[ 0 ].msg.style.display = '';
}
self.button_div = LAPI.make( 'div' );
self.viewers[ 0 ].main_div.appendChild( self.button_div );
self.add_button = LAPI.DOM.makeButton(
'ImageAnnotationAddButton',
ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
add_new
);
var add_plea = ImageAnnotator.UI.needs_plea;
self.button_div.appendChild( self.add_button );
self.help_link = self.createHelpLink();
if ( self.help_link ) {
self.button_div.appendChild( document.createTextNode( '\xa0' ) );
self.button_div.appendChild( self.help_link );
}
if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); }
} // end if may_edit
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
var cache = {};
var get_local = [];
var get_foreign = [];
Array.forEach( self.viewers, function ( viewer, idx ) {
if ( viewer.setup_done || viewer.isLocal && !viewer.has_page ) { return; }
// Handle only images that either are foreign or local and do have a page.
if ( cache[ viewer.realName ] ) {
cache[ viewer.realName ][ cache[ viewer.realName ].length ] = idx;
} else {
cache[ viewer.realName ] = [ idx ];
if ( !viewer.has_page ) {
get_foreign[ get_foreign.length ] = viewer.realName;
} else {
get_local[ get_local.length ] = viewer.realName;
}
}
} );
if ( get_local.length === 0 && get_foreign.length === 0 ) { return; }
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
// API limits and to keep the URL length below the limit for the foreign_repo calls.
function make_calls( list, execute_call, url_limit ) {
function composer( list, from, length, url_limit ) {
function compose( list, from, length, url_limit ) {
var text = '';
var done = 0;
for ( var i = from; i < from + length; i++ ) {
var new_text =
'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">' +
'<span class="image_annotation_inline_name">' + list[ i ] + '</span>' +
'{{:' + list[ i ] + '}}' + // Leading dot to avoid getting links to the full images if we hit a parser limit
'</div>';
if ( url_limit ) {
new_text = encodeURIComponent( new_text );
if ( text.length && ( text.length + new_text.length > url_limit ) ) { break; }
}
text = text + new_text;
done++;
// Additionally, limit the number of image pages to get: these can be large, and the server
// may refuse to actually do the transclusions but may in that case include the full images
// in the result, which would make us load the full images, which is desastrous if there are
// many thumbs to large images on the page.
if ( done == 5 ) { break; }
}
return { text: text, n: done };
}
var param = compose( list, from, length, url_limit );
execute_call( param.text );
return param.n;
}
var start = 0, chunk = 0, to_do = list.length;
while ( to_do > 0 ) {
chunk = composer( list, start, Math.min( 50, to_do ), url_limit );
to_do -= chunk;
start += chunk;
}
}
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
var inlineNameEnd = '</span>';
var noteStart = '<div id="image_annotation_note_';
var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;
// Our parse request returns the full html of the description pages' contents, including any
// license or information or other templates that we don't care about, and which may contain
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
// strip out everything but the notes.
function strip_noise( html ) {
var result = '';
var m;
// First, get rid of HTML comments and scripts
html = html.replace( /<!--(.|\s)*?-->/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' );
var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length;
// Now collect pages
while ( idx < l && i >= idx ) {
var j = html.indexOf( inlineNameEnd, i + blockStart.length );
if ( j < i + blockStart.length ) { break; }
result += html.substring( i, j + inlineNameEnd.length );
idx = j + inlineNameEnd.length;
// Now collect all image image notes for that page
var note_begin = 0, next_block = html.indexOf( blockStart, idx );
// Do we have image note control or color templates?
j = idx;
for ( ;; ) {
noteControlRE.lastIndex = j;
m = noteControlRE.exec( html );
if ( !m || next_block >= idx && m.index > next_block ) { break; }
result += m[ 0 ];
j = m.index + m[ 0 ].length;
}
// Collect notes
for ( ;; ) {
note_begin = html.indexOf( noteStart, idx );
// Check whether next wrapper comes first
if ( note_begin < idx || ( next_block >= idx && note_begin > next_block ) ) { break; }
// Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags.
var level = 1, k = note_begin + noteStart.length;
while ( level > 0 && k < l ) {
divRE.lastIndex = k;
m = divRE.exec( html );
if ( !m || m.length < 2 ) {
k = l; // Nothing found at all?
} else {
if ( m[ 1 ] ) {
level++; k = m.index + m[ 1 ].length; // divStart found first
} else if ( m.length > 2 && m[ 2 ] ) {
level--; k = m.index + m[ 2 ].length; // divEnd found first
} else {
k = l; // Huh?
}
}
} // end loop for nested divs
result += html.substring( note_begin, k );
while ( level-- > 0 ) { result += '</div>'; } // Missing ends.
idx = k;
} // end loop collecting notes
result += '</div>'; // Close the wrapper
i = next_block;
} // end loop collecting pages
return result;
}
function setup_thumb_viewers( html_text ) {
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
try {
node.innerHTML = strip_noise( html_text );
var pages = getElementsByClassName( node, 'div', 'wpImageAnnotatorInlineImageWrapper' );
for ( var i = 0; pages && i < pages.length; i++ ) {
var notes = getElementsByClassName( pages[ i ], 'div', self.annotation_class );
if ( !notes || notes.length === 0 ) {
continue;
}
var page = self.getItem( 'inline_name', pages[ i ] );
if ( !page ) { continue; }
page = page.replace( / /g, '_' );
var viewers = cache[ page ] || cache[ 'File:' + page.substring( page.indexOf( ':' ) + 1 ) ];
if ( !viewers || viewers.length === 0 ) {
continue;
}
// Update rules.
var rules = getElementsByClassName( pages[ i ], 'div', 'wpImageAnnotatorInlinedRules' );
var local_rules = {
inline: Object.clone( IA.rules.inline ),
thumbs: Object.clone( IA.rules.thumbs )
};
if ( rules && rules.length ) {
rules = rules[ 0 ];
if (
typeof local_rules.inline.show === 'undefined' &&
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoInlineDisplay' )
) {
local_rules.inline.show = false;
}
if (
typeof local_rules.inline.icon === 'undefined' &&
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorInlineDisplayIcon' )
) {
local_rules.inline.icon = true;
}
if (
typeof local_rules.thumbs.show === 'undefined' &&
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoThumbs' )
) {
local_rules.thumbs.show = false;
}
if (
typeof local_rules.thumbs.icon === 'undefined' &&
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorThumbDisplayIcon' )
) {
local_rules.thumbs.icon = true;
}
}
// Make sure all are set
local_rules.inline.show =
typeof local_rules.inline.show === 'undefined' || local_rules.inline.show;
local_rules.thumbs.show =
typeof local_rules.thumbs.show === 'undefined' || local_rules.thumbs.show;
local_rules.inline.icon =
typeof local_rules.inline.icon !== 'undefined' && local_rules.inline.icon;
local_rules.thumbs.icon =
typeof local_rules.thumbs.icon !== 'undefined' && local_rules.thumbs.icon;
if ( !local_rules.inline.show ) { continue; }
// Now use pages[i] as a scope shared by all the viewers using it. Since we clone note
// contents for note display, this works. Otherwise, we'd have to copy the notes into
// each viewer's scope.
document.body.appendChild( pages[ i ] ); // Move it out of 'node'
// Set viewers' scopes and finish their setup.
Array.forEach( viewers, function ( v ) {
if ( !self.viewers[ v ].isThumbnail || local_rules.thumbs.show ) {
self.viewers[ v ].scope = pages[ i ];
self.viewers[ v ].setup( self.viewers[ v ].isThumbnail && local_rules.thumbs.icon ||
self.viewers[ v ].isOther && local_rules.inline.icon );
}
} );
}
} catch ( ex ) {}
LAPI.DOM.removeNode( node );
}
ImageAnnotator.script_callbacks = [];
function make_script_calls( list, api ) {
var template = api + '?action=parse&pst&text=&prop=text&format=json' +
'&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764
'&callback=ImageAnnotator.script_callbacks[].callback';
if ( template.startsWith( '//' ) ) { template = document.location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
make_calls(
list,
function ( text ) {
var idx = ImageAnnotator.script_callbacks.length;
ImageAnnotator.script_callbacks[ idx ] = {
callback: function ( json ) {
if ( json && json.parse && json.parse.text && json.parse.text[ '*' ] ) {
setup_thumb_viewers( json.parse.text[ '*' ] );
}
ImageAnnotator.script_callbacks[ idx ].done = true;
if ( ImageAnnotator.script_callbacks[ idx ].script ) {
LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script );
ImageAnnotator.script_callbacks[ idx ].script = null;
}
},
done: false
};
ImageAnnotator.script_callbacks[ idx ].script =
IA.getScript(
template.replace( 'script_callbacks[].callback', 'script_callbacks[' + idx + '].callback' )
.replace( '&text=&', '&text=' + text + '&' ),
true // No local caching!
);
if (
ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[ idx ] &&
ImageAnnotator.script_callbacks[ idx ].done && ImageAnnotator.script_callbacks[ idx ].script
) {
LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script );
ImageAnnotator.script_callbacks[ idx ].script = null;
}
},
( LAPI.DOM.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
);
}
if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' );
} else {
make_calls(
get_local,
function ( text ) {
LAPI.Ajax.parseWikitext(
text,
function ( html_text ) { if ( html_text ) { setup_thumb_viewers( html_text ); } },
function () {},
false,
null,
'API', // Fixed string to enable caching at all
1800 // 30 minutes caching.
);
}
);
}
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
// to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083
// character (2048 in the path part), and servers also may impose limits (on the WMF servers,
// the limit appears to be 8kB).
make_script_calls( get_foreign, ImageAnnotator_config.sharedRepositoryAPI() );
},
show_zoom: function () {
var self = IA;
if (
(
self.viewers[ 0 ].factors.dx < self.zoom_threshold &&
self.viewers[ 0 ].factors.dy < self.zoom_threshold
) ||
Math.max( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) < 2.0
) {
// Below zoom threshold, or full image not even twice the size of the preview
return;
}
if ( !self.zoom ) {
self.zoom = LAPI.make(
'div',
{ id: 'image_annotator_zoom' },
{
overflow: 'hidden',
width: '200px',
height: '200px',
position: 'absolute',
display: 'none',
top: '0px',
left: '0px',
border: '2px solid #666666',
backgroundColor: 'white',
zIndex: 2050 // On top of everything
}
);
var src = self.viewers[ 0 ].img.getAttribute( 'src', 2 );
// Adjust zoom_factor
if ( self.zoom_factor > self.viewers[ 0 ].factors.dx || self.zoom_factor > self.viewers[ 0 ].factors.dy ) { self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ); }
self.zoom.appendChild( LAPI.make( 'div', null, { position: 'relative' } ) );
// Calculate zoom size and source link
var zoom_width = Math.floor( self.viewers[ 0 ].thumb.width * self.zoom_factor );
var zoom_height = Math.floor( self.viewers[ 0 ].thumb.height * self.zoom_factor );
// For SVGs, always use a scaled PNG for the zoom.
if ( zoom_width > 0.9 * self.viewers[ 0 ].full_img.width && src.search( /\.svg\.png$/i ) < 0 ) {
// If the thumb we'd be loading was within about 80% of the full image size, we may just as
// well get the full image instead of a scaled version.
self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy );
zoom_width = self.viewers[ 0 ].full_img.width;
zoom_height = self.viewers[ 0 ].full_img.height;
}
// Construct the initial zoomed image. We need to clone; if we create a completely
// new DOM node ourselves, it may not work on IE6...
var zoomed = self.viewers[ 0 ].img.cloneNode( true );
zoomed.width = String( zoom_width );
zoomed.height = String( zoom_height );
Object.merge( { position: 'absolute', top: '0px', left: '0px' }, zoomed.style );
self.zoom.firstChild.appendChild( zoomed );
// Crosshair
self.zoom.firstChild.appendChild(
LAPI.make(
'div', null,
{
width: '1px',
height: '200px',
borderLeft: '1px solid red',
position: 'absolute',
top: '0px',
left: '100px'
}
)
);
self.zoom.firstChild.appendChild(
LAPI.make(
'div', null,
{
width: '200px',
height: '1px',
borderTop: '1px solid red',
position: 'absolute',
top: '100px',
left: '0px'
}
)
);
document.body.appendChild( self.zoom );
LAPI.DOM.loadImage(
self.viewers[ 0 ].imgName,
src,
zoom_width,
zoom_height,
ImageAnnotator_config.thumbnailsGeneratedAutomatically(),
function ( img ) {
// Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets
img.style.position = 'absolute';
img.style.top = self.zoom.firstChild.firstChild.style.top;
img.style.left = self.zoom.firstChild.firstChild.style.left;
img.style.display = '';
self.zoom.firstChild.replaceChild( img, self.zoom.firstChild.firstChild );
}
);
}
self.zoom.style.display = 'none'; // Will be shown in update
},
update_zoom: function ( evt ) {
if ( !evt ) { return; } // We need an event to calculate positions!
var self = IA;
if ( !self.zoom ) { return; }
var mouse_pos = LAPI.Pos.mousePosition( evt );
var origin = LAPI.Pos.position( self.cover );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
IA.hide_zoom();
return;
}
var dx = mouse_pos.x - origin.x;
var dy = mouse_pos.y - origin.y;
// dx, dy is the offset within the preview image. Align the zoom image accordingly.
var top = -dy * self.zoom_factor + 100;
var left = -dx * self.zoom_factor + 100;
self.zoom.firstChild.firstChild.style.top = String( top ) + 'px';
self.zoom.firstChild.firstChild.style.left = String( left ) + 'px';
self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer
// Horizontally keep it in view.
var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 );
if ( x < 0 ) { x = 0; }
self.zoom.style.left = x + 'px';
self.zoom.style.display = '';
// Now that we have offsetWidth, correct the position.
if ( self.is_rtl ) {
x = mouse_pos.x - 10 - self.zoom.offsetWidth;
if ( x < 0 ) { x = 0; }
} else {
var off = LAPI.Pos.scrollOffset();
var view = LAPI.Pos.viewport();
if ( x + self.zoom.offsetWidth > off.x + view.x ) { x = off.x + view.x - self.zoom.offsetWidth; }
if ( x < 0 ) { x = 0; }
}
self.zoom.style.left = x + 'px';
},
hide_zoom: function ( evt ) {
if ( !IA.zoom ) { return; }
if ( evt ) {
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
}
IA.zoom.style.display = 'none';
},
createHelpLink: function () {
var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true );
if ( !msg || !msg.lastChild ) { return null; }
// Make sure we use the right protocol for all images:
var imgs = msg.getElementsByTagName( 'img' );
var text;
var tgt;
if ( imgs ) {
for ( var i = 0; i < imgs.length; i++ ) {
var srcFixed = imgs[ i ].getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol );
imgs[ i ].src = srcFixed;
}
}
if (
msg.childNodes.length == 1 &&
msg.firstChild.nodeName.toLowerCase() == 'a' &&
!LAPI.DOM.hasClass( msg.firstChild, 'image' )
) {
msg.firstChild.id = 'ImageAnnotationHelpButton';
return msg.firstChild; // Single link
}
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
// link.
tgt = msg.lastChild;
if ( tgt.nodeName.toLowerCase() != 'a' ) { tgt = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', 'Help:Gadget-ImageAnnotator' ); } else { tgt = tgt.href; }
function make_handler( tgt ) {
var target = tgt;
return function ( evt ) {
var e = evt || window.event;
location.href = target;
if ( e ) { return LAPI.Evt.kill( e ); }
return false;
};
}
imgs = msg.getElementsByTagName( 'img' );
if ( !imgs || !imgs.length ) {
// We're supposed to have a spans giving the button text
text = msg.firstChild;
if ( text.nodeName.toLowerCase() === 'span' ) { text = LAPI.DOM.getInnerText( text ); } else { text = 'Help'; }
return LAPI.DOM.makeButton(
'ImageAnnotationHelpButton'
, text
, make_handler( tgt )
);
} else {
return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) );
}
},
get_cover: function () {
var self = IA;
var shim;
if ( !self.cover ) {
var pos = { position: 'absolute',
left: '0px',
top: '0px',
width: self.viewers[ 0 ].thumb.width + 'px',
height: self.viewers[ 0 ].thumb.height + 'px'
};
self.cover = LAPI.make( 'div', null, pos );
self.border = self.cover.cloneNode( false );
Object.merge(
{ border: '3px solid green', top: '-3px', left: '-3px' }, self.border.style );
self.cover.style.zIndex = 2000; // Above the tooltips
if ( LAPI.Browser.is_ie ) {
shim = LAPI.make( 'iframe', { frameBorder: 0, tabIndex: -1 }, pos );
shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency
// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
// behaviors". If that is disabled, filters don't work, and our iframe would
// appear as a white rectangle. Fix this by first placing the iframe just above
// image (to block that windowed control) and then placing *another div* just
// above that shim having the image as its background image.
var imgZ = self.viewers[ 0 ].img.style.zIndex;
if ( isNaN( imgZ ) ) { imgZ = 10; } // Arbitrary, positive, > 1, < 500
shim.style.zIndex = imgZ + 1;
self.ieFix = shim;
// And now the bgImage div...
shim = LAPI.make( 'div', null, pos );
Object.merge(
{ top: '0px',
backgroundImage: 'url(' + self.viewers[ 0 ].img.src + ')',
zIndex: imgZ + 2
}
, shim.style
);
self.ieFix2 = shim;
}
if ( LAPI.Browser.is_opera ) {
// It appears that events just pass through completely transparent divs on Opera.
// Hence we have to ensure that these events are killed even if our cover doesn't
// handle them.
shim = LAPI.make( 'div', null, pos );
shim.style.zIndex = self.cover.style.zIndex - 1;
LAPI.Evt.attach( shim, 'mousemove',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
LAPI.Evt.attach( shim, 'mousedown',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
LAPI.Evt.attach( shim, 'mouseup',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
shim.style.cursor = 'default';
self.eventFix = shim;
}
self.cover_visible = false;
}
return self.cover;
},
show_cover: function () {
var self = IA;
if ( self.cover && !self.cover_visible ) {
if ( self.ieFix ) {
self.viewers[ 0 ].img_div.appendChild( self.ieFix );
self.viewers[ 0 ].img_div.appendChild( self.ieFix2 );
}
if ( self.eventFix ) { self.viewers[ 0 ].img_div.appendChild( self.eventFix ); }
self.viewers[ 0 ].img_div.appendChild( self.cover );
self.cover_visible = true;
}
},
hide_cover: function () {
var self = IA;
if ( self.cover && self.cover_visible ) {
if ( self.ieFix ) {
LAPI.DOM.removeNode( self.ieFix );
LAPI.DOM.removeNode( self.ieFix2 );
}
if ( self.eventFix ) { LAPI.DOM.removeNode( self.eventFix ); }
LAPI.DOM.removeNode( self.cover );
self.cover_visible = false;
}
},
getRawItem: function ( what, scope ) {
var node = null;
if ( !scope || scope == document ) {
node = LAPI.$( 'image_annotation_' + what );
} else {
node = getElementsByClassName( scope, '*', 'image_annotation_' + what );
if ( node && node.length ) { node = node[ 0 ]; } else { node = null; }
}
return node;
},
getItem: function ( what, scope ) {
var node = IA.getRawItem( what, scope );
if ( !node ) { return null; }
return LAPI.DOM.getInnerText( node ).trim();
},
getIntItem: function ( what, scope ) {
var x = IA.getItem( what, scope );
if ( x !== null ) { x = parseInt( x, 10 ); }
return x;
},
findNote: function ( text, id ) {
function find( text, id, delim ) {
var start = delim.start.replace( '$1', id );
var start_match = text.indexOf( start );
if ( start_match < 0 ) { return null; }
var end = delim.end.replace( '$1', id );
var end_match = text.indexOf( end );
if ( end_match < start_match + start.length ) { return null; }
return { start: start_match, end: end_match + end.length };
}
var result = null;
for ( var i = 0; i < IA.note_delim.length && !result; i++ ) {
result = find( text, id, IA.note_delim[ i ] );
}
return result;
},
setWikitext: function ( pagetext ) {
var self = IA;
if ( self.wiki_read ) { return; }
Array.forEach( self.viewers[ 0 ].annotations, function ( note ) {
if ( note.model.id >= 0 ) {
var span = self.findNote( pagetext, note.model.id );
if ( !span ) { return; }
// Now extract the wikitext
var code = pagetext.substring( span.start, span.end );
for ( var i = 0; i < self.note_delim.length; i++ ) {
var start = self.note_delim[ i ].content_start.replace( '$1', note.model.id );
var end = self.note_delim[ i ].content_end.replace( '$1', note.model.id );
var j = code.indexOf( start );
var k = code.indexOf( end );
if ( j >= 0 && k >= 0 && k >= j + start.length ) {
note.model.wiki = code.substring( j + start.length, k ).trim();
return;
}
}
}
} );
self.wiki_read = true;
},
setSummary: function ( summary, initial_text, note_text ) {
if ( initial_text.contains( '$1' ) ) {
var max = ( summary.maxlength || 200 ) - initial_text.length;
if ( note_text ) {
initial_text =
initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) );
} else {
initial_text = initial_text.replace( '$1', '' );
}
}
summary.value = initial_text;
},
getScript: function ( url, bypass_local_cache, bypass_caches ) {
// Don't use LAPI here, it may not yet be available
if ( bypass_caches ) {
url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime();
}
// Avoid protocol-relative URIs (IE7 bug)
if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) { url = document.location.protocol + url; }
if ( bypass_local_cache ) {
var s = document.createElement( 'script' );
s.setAttribute( 'src', url );
s.setAttribute( 'type', 'text/javascript' );
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
return s;
} else {
return mw.loader.load( url );
}
},
canEdit: function () {
var self = IA;
if ( self.may_edit ) {
if ( !self.ajaxQueried ) {
self.haveAjax = ( LAPI.Ajax.getRequest() != null );
self.ajaxQueried = true;
self.may_edit = self.haveAjax;
if ( !self.may_edit && self.button_div ) {
LAPI.DOM.removeChildren( self.button_div );
self.button_div.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorCannotEditMsg', false ) );
self.viewers[ 0 ].msg.style.display = '';
self.viewers[ 0 ].cannotEdit();
}
}
}
return self.may_edit;
}
}; // end IA
// Backwards compatibility
function getElementsByClassName( scope, tag, className ) {
if ( window.jQuery ) {
return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className );
} else {
// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
return getElementsByClassName( scope, tag, className );
}
}
window.ImageAnnotator = {
install: function ( config ) { IA.install( config ); }
};
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
IA.getScript(
mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript',
true // No local caching!
);
}() ); // end local scope
} // end if (guard against double inclusions)
jsh879v26gaodlkrdv2qjwlufpioq7s
MediaWiki:Gadget-exlinks.js
8
29052
326075
2026-04-04T08:51:43Z
Kannotlogin
29153
nieuw blad: // ********************************************************************** // ** ***WARNING GLOBAL GADGET FILE*** ** // ** changes to this file affect many users. ** // ** please discuss on the talk page before editing ** // ** ** // ********************************************************************** /** * @source mediawiki.org/wiki/Snipp…
326075
javascript
text/javascript
// **********************************************************************
// ** ***WARNING GLOBAL GADGET FILE*** **
// ** changes to this file affect many users. **
// ** please discuss on the talk page before editing **
// ** **
// **********************************************************************
/**
* @source mediawiki.org/wiki/Snippets/Open_external_links_in_new_window
* @version 5
*/
mw.hook('wikipage.content').add(function($content) {
// Second selector is for external links in Parsoid HTML+RDFa output (bug 65243).
$content.find('a.external, a[rel="mw:ExtLink"]').each(function () {
// Can't use wgServer because it can be protocol relative
// Use this.href property instead of this.getAttribute('href') because the property
// is converted to a full URL (including protocol)
if (this.href.indexOf(location.protocol + '//' + location.hostname) !== 0) {
this.target = '_blank';
if ( this.rel.indexOf( 'noopener' ) < 0 ) {
this.rel += ' noopener'; // the leading space matters, rel attributes have space-separated tokens
}
if ( this.rel.indexOf( 'noreferrer' ) < 0 ) {
this.rel += ' noreferrer'; // the leading space matters, rel attributes have space-separated tokens
}
}
});
});
la8tkkgxfljayougc48gucmj2telov4
MediaWiki:Gadget-markblocked.js
8
29053
326076
2026-04-04T08:51:55Z
Kannotlogin
29153
nieuw blad: /* You can import this gadget to other wikis by using mw.loader.load and specifying the local alias for Special:Contributions. For example: var markblocked_contributions = 'Special:Contributions'; mw.loader.load('//en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-markblocked.js&bcache=1&maxage=259200&action=raw&ctype=text/javascript'); This gadget will pull the user accounts and IPs from the history page and will strike out the users that are currently blocked. Configuratio…
326076
javascript
text/javascript
/*
You can import this gadget to other wikis by using mw.loader.load and specifying the local alias for Special:Contributions. For example:
var markblocked_contributions = 'Special:Contributions';
mw.loader.load('//en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-markblocked.js&bcache=1&maxage=259200&action=raw&ctype=text/javascript');
This gadget will pull the user accounts and IPs from the history page and will strike out the users that are currently blocked.
Configuration variables:
- window.markblocked_contributions - Let wikis that are importing this gadget specify the local alias of Special:Contributions
- window.mbHidePartialBlockRestrictions - if set to true, do not show partial block restrictions in the tooltip
- window.mbIndefStyle - custom CSS to override default CSS for indefinite blocks
- window.mbNoAutoStart - if set to true, doesn't mark blocked until you click "XX" in the "More" menu
- window.mbPartialStyle - custom CSS to override default CSS for partial blocks
- window.mbTempStyle - custom CSS to override default CSS for short duration blocks
- window.mbTipBox - if set to true, loads a yellow box with a pound sign next to blocked usernames. upon hovering over it, displays a tooltip.
- window.mbTipBoxStyle - custom CSS to override default CSS for the tip box (see above)
- window.mbTooltip - custom pattern to use for tooltips. default is '; blocked ($1) by $2: $3 ($4 ago)'
Forked from https://ru.wikipedia.org/w/index.php?title=MediaWiki:Gadget-markblocked.js&oldid=77732587 on July 13, 2016
*/
( () => {
function execute() {
if ( [ 'edit', 'submit' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) {
return;
}
const maybeAutostart = $.Deferred();
if ( window.mbNoAutoStart ) {
const portletLink = mw.util.addPortletLink( 'p-cactions', '', 'XX', 'ca-showblocks' );
$( portletLink ).on( 'click', ( e ) => {
e.preventDefault();
maybeAutostart.resolve();
} );
} else {
maybeAutostart.resolve();
}
$.when(
$.ready,
// keep mw.loader.using in case folks are loading this as a user script
mw.loader.using( [ 'mediawiki.util', 'mediawiki.page.ready', 'mediawiki.Title' ] ),
maybeAutostart
).then( () => {
let firstTime = true;
mw.hook( 'wikipage.content' ).add( ( $container ) => {
// On the first call after initial page load, container is mw.util.$content
// Limit mainspace activity to just the diff definitions
if ( mw.config.get( 'wgAction' ) === 'view' && mw.config.get( 'wgNamespaceNumber' ) === 0 ) {
$container = $container.find( '.diff-title' );
}
if ( firstTime ) {
firstTime = false;
// On page load, also update the namespace tab
$container = $container.add( '#ca-nstab-user' );
mw.util.addCSS( '\
.markblocked-loading a.userlink {opacity:' + ( window.mbLoadingOpacity || 0.85 ) + '}\
a.user-blocked-partial {' + ( window.mbPartialStyle || 'text-decoration: underline; text-decoration-style: dotted' ) + '}\
a.user-blocked-temp {' + ( window.mbTempStyle || 'opacity: 0.7; text-decoration: line-through' ) + '}\
a.user-blocked-indef {' + ( window.mbIndefStyle || 'opacity: 0.4; font-style: italic; text-decoration: line-through' ) + '}\
.user-blocked-tipbox {' + ( window.mbTipBoxStyle || 'font-size:smaller; background:#FFFFF0; border:1px solid #FEA; padding:0 0.3em; color:#AAA' ) + '}\
' );
}
markBlocked( $container );
} );
} );
}
function markBlocked( $container ) {
// Get all aliases for user: & user_talk:
const userNS = [];
for ( const ns in mw.config.get( 'wgNamespaceIds' ) ) {
if ( mw.config.get( 'wgNamespaceIds' )[ ns ] === 2 || mw.config.get( 'wgNamespaceIds' )[ ns ] === 3 ) {
userNS.push( mw.util.escapeRegExp( ns.replace( /_/g, ' ' ) ) + ':' );
}
}
// Let wikis that are importing this gadget specify the local alias of Special:Contributions
if ( window.markblocked_contributions === undefined ) {
window.markblocked_contributions = 'Special:Contributions';
}
const userLinks = {};
getUserLinks( userLinks, $container, userNS );
// Convert users into array
const users = [];
for ( const u in userLinks ) {
users.push( u );
}
if ( users.length === 0 ) {
return;
}
// API request
let apiRequests = 0;
$container.addClass( 'markblocked-loading' );
while ( users.length > 0 ) {
apiRequests++;
// TODO: refactor to use mw.Api()
$.post(
mw.util.wikiScript( 'api' ) + '?format=json&action=query',
{
list: 'blocks',
bklimit: 100,
bkusers: users.splice( 0, 50 ).join( '|' ),
bkprop: 'user|by|timestamp|expiry|reason|flags|restrictions'
// no need for 'id'
}
).done( ( resp, status, xhr ) => {
markLinks( resp, xhr, userLinks );
apiRequests--;
if ( apiRequests === 0 ) { // last response
$container.removeClass( 'markblocked-loading' );
$( '#ca-showblocks' ).parent().remove(); // remove added portlet link
}
} );
}
}
/**
* Receive data and mark links
*/
function markLinks( resp, xhr, userLinks ) {
const serverTime = new Date( xhr.getResponseHeader( 'Date' ) );
let list, block, tooltipString, links, $link;
if ( !resp || !( list = resp.query ) || !( list = list.blocks ) ) {
return;
}
for ( let i = 0; i < list.length; i++ ) {
block = list[ i ];
const partial = 'partial' in block; // Partial block
let htmlClass, blockTime;
if ( /^in/.test( block.expiry ) ) {
htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-indef';
blockTime = block.expiry;
} else {
htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-temp';
// Apparently you can subtract date objects in JavaScript. Some kind of
// magic happens and they are automatically converted to milliseconds.
blockTime = inHours( parseTimestamp( block.expiry ) - parseTimestamp( block.timestamp ) );
}
tooltipString = window.mbTooltip || '; blocked ($1) by $2: $3 ($4 ago)';
if ( partial ) {
if ( window.mbHidePartialBlockRestrictions ) {
tooltipString = tooltipString.replace( 'blocked', 'partially blocked' );
} else {
tooltipString = tooltipString.replace( 'blocked', 'partially blocked from $5');
}
}
tooltipString = tooltipString.replace( '$1', blockTime )
.replace( '$2', block.by )
.replace( '$3', block.reason )
.replace( '$4', inHours( serverTime - parseTimestamp( block.timestamp ) ) );
if ( partial && !window.mbHidePartialBlockRestrictions ) {
pbDetails = [];
if ( block.restrictions.pages ) {
pbDetails.push ( ( block.restrictions.pages.length == 1 ? 'page ' : 'pages ' ) + makeCommaSeparatedList( block.restrictions.pages.map( (page) => '[[' + page.title + ']]' ) ) );
}
if ( block.restrictions.namespaces ) {
if ( block.restrictions.namespaces && block.restrictions.namespaces.length == 1 && block.restrictions.namespaces[0] == 0 ) pbDetails.push( 'mainspace' );
else pbDetails.push ( ( block.restrictions.namespaces.length == 1 ? 'namespace ' : 'namespaces ' ) +
makeCommaSeparatedList( block.restrictions.namespaces.map( (ns) => ns == 0 ? '(Article)' : mw.config.get( 'wgFormattedNamespaces' )[ns] ) ) );
}
if ( block.restrictions.actions ) {
if ( block.restrictions.actions.includes( 'upload' ) ) pbDetails.push( "uploading files" );
if ( block.restrictions.actions.includes( 'move' ) ) pbDetails.push( "moving pages" );
if ( block.restrictions.actions.includes( 'create' ) ) pbDetails.push( "creating pages" );
if ( block.restrictions.actions.includes( 'thanks' ) ) pbDetails.push( "thanking users" );
}
if ( !block.restrictions.pages && !block.restrictions.namespaces && !block.restrictions.actions ) {
pbDetails.push("specified non-editing actions");
}
tooltipString = tooltipString.replace ( '$5', makeCommaSeparatedList( pbDetails ) );
}
links = userLinks[ block.user ];
for ( let k = 0; links && k < links.length; k++ ) {
$link = $( links[ k ] );
$link = $link.addClass( htmlClass );
if ( window.mbTipBox ) {
$( '<span class=user-blocked-tipbox>#</span>' ).attr( 'title', tooltipString ).insertBefore( $link );
} else {
$link.attr( 'title', $link.attr( 'title' ) + tooltipString );
}
}
}
}
/**
* Find all "user" links and save them in userLinks : { 'users': [<link1>, <link2>, ...], 'user2': [<link3>, <link3>, ...], ... }
*/
function getUserLinks( userLinks, $container, userNS ) {
// RegExp for all titles that are User:| User_talk: | Special:Contributions/ (for userscripts)
const userTitleRegex = new RegExp( '^(' + userNS.join( '|' ) + '|' + window.markblocked_contributions + '\\/)+([^\\/#]+)$', 'i' );
// RegExp for links
// articleRX also matches external links in order to support the noping template
const articleRegex = new RegExp( mw.config.get( 'wgArticlePath' ).replace( '$1', '' ) + '([^#]+)' );
const scriptRegex = new RegExp( '^' + mw.config.get( 'wgScript' ) + '\\?title=([^#&]+)' );
const ipv6Regex = /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i;
// Collect all the links in the page's content
$container.find( 'a' )
.not( '.mw-changeslist-date, .ext-discussiontools-init-timestamplink, .mw-history-undo > a, .mw-rollback-link > a' )
.each( ( i, link ) => {
// guard clauses and username extraction logic
const $link = $( link );
const url = $link.attr( 'href' );
if ( !url ) {
return;
}
const articleMatch = articleRegex.exec( url ),
scriptMatch = scriptRegex.exec( url );
let pageTitle;
if ( articleMatch ) {
pageTitle = articleMatch[ 1 ];
} else if ( scriptMatch ) {
pageTitle = scriptMatch[ 1 ];
} else {
return;
}
pageTitle = decodeURIComponent( pageTitle ).replace( /_/g, ' ' );
let user = userTitleRegex.exec( pageTitle );
if ( !user ) {
return;
}
const userTitle = mw.Title.newFromText( user[ 2 ] );
if ( !userTitle ) {
return;
}
user = userTitle.getMainText();
if ( ipv6Regex.test( user ) ) {
user = user.toUpperCase();
}
// OK, let's finally do some stuff that has side effects
$link.addClass( 'userlink' );
if ( !userLinks[ user ] ) {
userLinks[ user ] = [];
}
userLinks[ user ].push( link );
} );
}
/**
* @param {string} timestamp 20081226220605 or 2008-01-26T06:34:19Z
* @return {Date}
*/
function parseTimestamp( timestamp ) {
const matches = timestamp.replace( /\D/g, '' ).match( /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ );
return new Date( Date.UTC( matches[ 1 ], matches[ 2 ] - 1, matches[ 3 ], matches[ 4 ], matches[ 5 ], matches[ 6 ] ) );
}
/**
* @param {number} milliseconds 604800000
* @return {string} "2:30" or "5.06d" or "21d"
*/
function inHours( milliseconds ) {
let minutes = Math.floor( milliseconds / 60000 );
if ( !minutes ) {
return Math.floor( milliseconds / 1000 ) + 's';
}
let hours = Math.floor( minutes / 60 );
minutes = minutes % 60;
const days = Math.floor( hours / 24 );
hours = hours % 24;
if ( days ) {
return days + ( days < 10 ? '.' + addLeadingZeroIfNeeded( hours ) : '' ) + 'd';
}
return hours + ':' + addLeadingZeroIfNeeded( minutes );
}
/**
* @param {number} v 9
* @return {string} 09
*/
function addLeadingZeroIfNeeded( v ) {
if ( v <= 9 ) {
v = '0' + v;
}
return v;
}
/** @param {array} strings ["editing pages Test and Wikipedia", "thanking users"]
@return {string} "editing pages Test and Wikipedia and thanking users"
*/
function makeCommaSeparatedList ( strings ) {
if ( strings.length == 0 ) return "";
if ( strings.length == 1 ) return strings[0];
if ( strings.length == 2 ) return strings[0] + ' and ' + strings[1];
return strings.slice( 0, -1 ).join( ', ' ) + ', and ' + strings[strings.length - 1];
}
execute();
} )();
kdjbg0z2qfmyu78919g040x19cfem87
MediaWiki:Gadget-Gebruikersinfo.js
8
29054
326077
2026-04-04T08:52:13Z
Kannotlogin
29153
nieuw blad: // Originele code https://en.wikipedia.org/w/index.php?title=User:PleaseStand/userinfo.js // wat gebaseerd is op http://en.wikipedia.org/wiki/User:Fran Rogers/dimorphism.js // en op http://en.wikipedia.org/wiki/User:Splarka/sysopdectector.js // wat op 31 december 2025 naar het Nederlands vertaald is door IM ItsNyoty function UserinfoJsFormatQty(qty, singular, plural) { return String(qty).replace(/\d{1,3}(?=(\d{3})+(?!\d))/g, "$&,") + "\u00a0" + (qty == 1 ? singular : plura…
326077
javascript
text/javascript
// Originele code https://en.wikipedia.org/w/index.php?title=User:PleaseStand/userinfo.js
// wat gebaseerd is op http://en.wikipedia.org/wiki/User:Fran Rogers/dimorphism.js
// en op http://en.wikipedia.org/wiki/User:Splarka/sysopdectector.js
// wat op 31 december 2025 naar het Nederlands vertaald is door IM ItsNyoty
function UserinfoJsFormatQty(qty, singular, plural) {
return String(qty).replace(/\d{1,3}(?=(\d{3})+(?!\d))/g, "$&,") + "\u00a0" + (qty == 1 ? singular : plural);
}
function UserinfoJsFormatDateRel(old) {
// The code below requires the computer's clock to be set correctly.
var age = new Date().getTime() - old.getTime();
var ageNumber, ageRemainder, ageWords;
if(age < 60000) {
// less than one minute old
ageNumber = Math.floor(age / 1000);
ageWords = UserinfoJsFormatQty(ageNumber, "second", "seconden");
} else if(age < 3600000) {
// less than one hour old
ageNumber = Math.floor(age / 60000);
ageWords = UserinfoJsFormatQty(ageNumber, "minuut", "minuten");
} else if(age < 86400000) {
// less than one day old
ageNumber = Math.floor(age / 3600000);
ageWords = UserinfoJsFormatQty(ageNumber, "uur", "uur");
ageRemainder = Math.floor((age - ageNumber * 3600000) / 60000);
} else if(age < 604800000) {
// less than one week old
ageNumber = Math.floor(age / 86400000);
ageWords = UserinfoJsFormatQty(ageNumber, "dag", "dagen");
} else if(age < 2592000000) {
// less than one month old
ageNumber = Math.floor(age / 604800000);
ageWords = UserinfoJsFormatQty(ageNumber, "week", "weken");
} else if(age < 31536000000) {
// less than one year old
ageNumber = Math.floor(age / 2592000000);
ageWords = UserinfoJsFormatQty(ageNumber, "maand", "maanden");
} else {
// one year or older
ageNumber = Math.floor(age / 31536000000);
ageWords = UserinfoJsFormatQty(ageNumber, "jaar", "jaren");
ageRemainder =
Math.floor((age - ageNumber * 31536000000) / 2592000000);
if(ageRemainder) {
ageWords += " " +
UserinfoJsFormatQty(ageRemainder, "maand", "maanden");
}
}
return ageWords;
}
// If on a user or user talk page, and not a subpage...
if((mw.config.get("wgNamespaceNumber") == 2 || mw.config.get("wgNamespaceNumber") == 3) && !(/\//.test(mw.config.get("wgTitle")))) {
// add a hook to...
mw.loader.using( ['mediawiki.util'], function() { $(function(){
// Request the user's information from the API.
// Note that this is allowed to be up to 5 minutes old.
var et = encodeURIComponent(mw.config.get("wgTitle"));
$.getJSON(mw.config.get("wgScriptPath") + "/api.php?format=json&action=query&list=users|usercontribs&usprop=blockinfo|editcount|gender|registration|groups&uclimit=1&ucprop=timestamp&ususers=" + et + "&ucuser=" + et + "&meta=allmessages&refix=grouppage-&amincludelocal=1")
.done(function(query) {
// When response arrives extract the information we need.
if(!query.query) { return; } // Suggested by Gary King to avoid JS errors --PS 2010-08-25
query = query.query;
var user, invalid, missing, groups, groupPages={}, editcount, registration, blocked, gender, lastEdited;
try {
user = query.users[0];
invalid = typeof user.invalid != "undefined";
missing = typeof user.missing != "undefined";
groups = (typeof user.groups == "object") ? user.groups : [];
editcount = (typeof user.editcount == "number") ? user.editcount : null;
registration = (typeof user.registration == "string") ?
new Date(user.registration) : null;
blocked = typeof user.blockedby != "undefined";
gender = (typeof user.gender == "string") ? user.gender : null;
lastEdited = (typeof query.usercontribs[0] == "object") &&
(typeof query.usercontribs[0].timestamp == "string") ?
new Date(query.usercontribs[0].timestamp) : null;
for (var am=0; am<query.allmessages.length; am++) {
groupPages[query.allmessages[am]["name"].replace("grouppage-","")] = query.allmessages[am]["*"].replace("{{ns:project}}:","Project:");
}
} catch(e) {
return; // Not much to do if the server is returning an error (e.g. if the username is malformed).
}
// Format the information for on-screen display
var statusText = "";
var ipUser = false;
var ipv4User = false;
var ipv6User = false;
// User status
if(blocked) {
statusText += "<a href=\"" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:Log&page=" +
encodeURIComponent(mw.config.get("wgFormattedNamespaces")[2] + ":" + user.name) +
"&type=block\">geblokkeerde</a> ";
}
if (missing) {
statusText += "username not registered";
} else if (invalid) {
ipv4User = mw.util.isIPv4Address(user.name);
ipv6User = mw.util.isIPv6Address(user.name);
ipUser = ipv4User || ipv6User;
if (ipv4User) {
statusText += "anonymous IPv4 user";
} else if (ipv6User) {
statusText += "anonymous IPv6 user";
} else {
statusText += "invalid username";
}
} else {
// User is registered and may be in a privileged group. Below we have a list of user groups.
// Only need the ones different from the software's name (or ones to exclude), though.
var friendlyGroupNames = {
// Exclude implicit user group information provided by MW 1.17 --PS 2010-02-17
'*': false,
'user': false,
'autoconfirmed': false,
sysop: "moderator",
accountcreator: "account aanmaker",
'import': "importeur",
transwiki: "transwiki importeur",
'ipblock-exempt': "uitgezonderd van IP-adresblokkades",
confirmed: "bevestigde gebruiker",
abusefilter: "misbruikfilterredacteur",
'abusefilter-helper': "edit filter helper",
autoreviewer: "autopatrolled user",
filemover: "file mover",
'massmessage-sender': "mass message sender",
templateeditor: "template editor",
extendedconfirmed: "uitgebreid bevestigde gebruiker",
extendedmover: "page mover",
reviewer: "pending changes reviewer",
suppress: "oversighter",
patroller: "new page reviewer",
copyviobot: "copyright violation bot",
eventcoordinator: "organisator van evenementen",
'interface-admin': "Interfacemoderator",
temp: "tijdelijke gebruiker",
'vrt-agent': "medewerker van het contactpunt"
};
var friendlyGroups = [];
for(var i = 0; i < groups.length; ++i) {
var s = groups[i];
var t = friendlyGroupNames.hasOwnProperty(s) ? friendlyGroupNames[s] : s;
if (t) {
if (groupPages.hasOwnProperty(s)) {
friendlyGroups.push("<a href=\"/wiki/" + encodeURIComponent( groupPages[s] ) + "\">" + t + "</a>");
} else {
friendlyGroups.push(t);
}
}
}
switch(friendlyGroups.length) {
case 0:
// User not in a privileged group
// Changed to "registered user" by request of [[User:Svanslyck]]
// --PS 2010-05-16
// statusText += "user";
if(blocked) {
statusText += "user";
} else {
statusText += "registered user";
}
break;
case 1:
statusText += friendlyGroups[0];
break;
case 2:
statusText += friendlyGroups[0] + " en " + friendlyGroups[1];
break;
default:
statusText += friendlyGroups.slice(0, -1).join(", ") +
", en " + friendlyGroups[friendlyGroups.length - 1];
break;
}
}
// Registration date
if(registration) {
var firstLoggedUser = new Date("22:16, 7 September 2005"); // When the [[Special:Log/newusers]] was first activated
if(registration >= firstLoggedUser) {
statusText += ". Sinds <a href='" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:Log&type=newusers&dir=prev&limit=1&user=" +
et + "'>" + UserinfoJsFormatDateRel(registration) + "</a> actief";
} else {
statusText += ". Sinds <a href='" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:ListUsers&limit=1&username=" +
et + "'>" + UserinfoJsFormatDateRel(registration) + "</a> actief";
}
}
// Edit count
if(editcount !== null) {
statusText += ", met in totaal " +
"<a href=\"//tools.wmflabs.org/xtools-ec/?user=" +
encodeURIComponent(user.name) +
"&project=nl.wikipedia.org&uselang=nl\">" +
UserinfoJsFormatQty(editcount, "edit", "edits") + "</a>";
}
// Prefix status text with correct article
if("AEIOaeio".indexOf(statusText.charAt(statusText.indexOf('>')+1)) >= 0) {
statusText = "Een " + statusText;
} else {
statusText = "Een " + statusText;
}
// Add full stop to status text
statusText += ".";
// Last edited --PS 2010-06-27
// Added link to contributions page --PS 2010-07-03
if(lastEdited) {
statusText += " Laatst bewerkt: <a href=\"" + mw.config.get("wgArticlePath").replace("$1", "Special:Contributions/" + encodeURIComponent(user.name)) + "\">" + UserinfoJsFormatDateRel(lastEdited) + " </a> geleden. ";
}
// Show the correct gender symbol
var fh = document.getElementById("firstHeading") ||
document.getElementById("section-0");
if(!fh) return; // e.g. Minerva user talk pages
// Add classes for blocked, registered, and anonymous users
var newClasses = [];
if(blocked) {
newClasses.push("ps-blocked");
}
if(ipUser) {
newClasses.push("ps-anonymous");
} else if(invalid) {
newClasses.push("ps-invalid");
} else {
newClasses.push("ps-registered");
}
fh.className += (fh.className.length ? " " : "") + groups.map(function(s) {
return "ps-group-" + s;
}).concat(newClasses).join(" ");
var genderSpan = document.createElement("span");
genderSpan.id = "ps-gender-" + (gender || "unknown");
genderSpan.style.paddingLeft = "0.25em";
genderSpan.style.fontFamily = '"Lucida Grande", "Lucida Sans Unicode", "sans-serif"';
genderSpan.style.fontSize = "75%";
var genderSymbol;
switch(gender) {
case "male": genderSymbol = "\u2642"; break;
case "female": genderSymbol = "\u2640"; break;
default: genderSymbol = ""; break;
}
genderSpan.appendChild(document.createTextNode(genderSymbol));
fh.appendChild(genderSpan);
// Now show the other information. Non-standard? Yes, but it gets the job done.
// Add a period after the tagline when doing so. --PS 2010-07-03
var ss = document.getElementById("siteSub");
if(!ss) {
ss = document.createElement("div");
ss.id = "siteSub";
ss.innerHTML = "Uit Wikipedia, de vrije encyclopedie.";
var bc = document.getElementById("bodyContent");
bc.insertBefore(ss, bc.firstChild);
}
ss.innerHTML = '<span id="ps-userinfo">' + statusText + '</span> ' + ss.innerHTML + '.';
ss.style.display = "block";
});
}); });
}
noh0n6oszbsh1fikhi6jdx94e5iya72
MediaWiki:Gadget-XTools-ArticleInfo.js
8
29055
326078
2026-04-04T08:52:25Z
Kannotlogin
29153
nieuw blad: /** * XTools PageInfo gadget * Based on meta.wikimedia.org/wiki/User:Hedonil/XTools * Documentation: mediawiki.org/wiki/XTools/PageInfo_gadget * Released under GPL 3.0+ license * For updates, please copy and paste from https://xtools.wmcloud.org/pageinfo-gadget.js */ $(function () { if (mw.config.get('wgArticleId') === 0 || // no deleted articles, no special pages mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId') || // only current revision…
326078
javascript
text/javascript
/**
* XTools PageInfo gadget
* Based on meta.wikimedia.org/wiki/User:Hedonil/XTools
* Documentation: mediawiki.org/wiki/XTools/PageInfo_gadget
* Released under GPL 3.0+ license
* For updates, please copy and paste from https://xtools.wmcloud.org/pageinfo-gadget.js
*/
$(function () {
if (mw.config.get('wgArticleId') === 0 || // no deleted articles, no special pages
mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId') || // only current revision
mw.config.get('wgAction') !== 'view') { // only when viewing a page, not editing
return;
}
var $result,
markup = "<div id='xtools' style='font-size:84%; line-height:1.2em;" +
"width:auto;'><span id='xtools_result'>.</span></div>";
$(markup).insertBefore('#contentSub');
$result = $('#xtools_result');
var loadinganimation = window.setInterval(function () {
if ($result.html() === '. ') {
$result.html(' . ');
} else if ($result.html() === ' . ') {
$result.html(' .');
} else {
$result.html('. ');
}
}, 300);
$.get(
'https://xtools.wmcloud.org/api/page/pageinfo/' +
mw.config.get('wgServerName') + '/' +
mw.config.get('wgPageName').replace(/["?%&+\\]/g, escape) + '?format=html' +
'&uselang=' + mw.config.get('wgUserLanguage')
).done(function (result) {
$result.html(result);
clearInterval(loadinganimation);
});
});
4gl0dz3mnklq1pou1zqhda2l2qfmo0v
MediaWiki:Gadget-HotCat.js
8
29056
326079
2026-04-04T08:52:34Z
Kannotlogin
29153
nieuw blad: /** HotCat V2.46 Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view. Supports multiple category changes, as well as redirect and disambiguation resolution. Also plugs into the upload form. Search engines to use for the suggestion list are configurable, and can be selected interactively. Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Versi…
326079
javascript
text/javascript
/**
HotCat V2.46
Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
Supports multiple category changes, as well as redirect and disambiguation resolution. Also
plugs into the upload form. Search engines to use for the suggestion list are configurable, and
can be selected interactively.
Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
This code should run on any MediaWiki installation >= MW 1.32.
For use with older versions of MediaWiki, use the archived versions below:
<=1.31: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=1001064319
<=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
*/
// <nowiki>
/* eslint-disable vars-on-top, one-var, camelcase, no-alert, curly */
/* global jQuery, mediaWiki, UFUI, JSconfig, UploadForm */
/* jslint strict:false, nonew:false, bitwise:true */
( function ( $, mw ) {
// Don't use mw.config.get() as that takes a copy of the config, and so doesn't
// account for values changing, e.g. wgCurRevisionId after a VE edit
var conf = mw.config.values;
// Expand relative to location.href, so that we have a reliable prefix for other expanded
// links in the current document, regardless of context (e.g. mobile domain)
var expandedScriptPrefix = new URL(conf.wgScript, location.href).href;
var expandedArticlePrefix = new URL(conf.wgArticlePath.replace( '$1', '' ), location.href).href;
// Guard against double inclusions (in old IE/Opera element ids become window properties)
if ( ( window.HotCat && !window.HotCat.nodeName ) ||
conf.wgAction === 'edit' ) // Not on edit mode
return;
// Configuration holder. This is here so that the most common config and interface messages
// can be defined using the same format as translation subpages and /local_defaults.
// The rest is added later.
var HC = HotCat = window.HotCat = {
messages: {},
// Tooltips for the HotCat.links icons
tooltips: {},
engine_names: {}
};
// Common site configuration
//
// DO NOT EDIT
//
// Create the [[MediaWiki:Gadget-HotCat.js/local_defaults]] page
// on your wiki, and copy only the line you want to change there,
// and then change it there.
//
HotCat.messages.cat_removed = 'removed [[Category:$1]]';
HotCat.messages.template_removed = 'removed {{[[Category:$1]]}}';
HotCat.messages.cat_added = 'added [[Category:$1]]';
// * $1 - Category name
// * $2 - New key
HotCat.messages.cat_keychange = 'new key for [[Category:$1]]: "$2"';
HotCat.messages.cat_resolved = ' (redirect [[Category:$1]] resolved)';
HotCat.messages.uncat_removed = 'removed {{uncategorized}}';
HotCat.messages.separator = '; ';
// This text is added to the start of the edit summary.
HotCat.messages.prefix = '';
// This text is added to the end of the edit summary.
// If you prefer to have a marker at the front, use prefix instead
// and set this to the empty string.
HotCat.messages.using = ' using [[Help:Gadget-HotCat|HotCat]]';
// If your language has several plural forms, [[:en:Dual (grammatical form)]],
// you can set HotCat.messages.multi_change to an array of strings,
// which works the same way as {{PLURAL:}} and mw.language.convertPlural().
// https://translatewiki.net/wiki/Plural#Plural_syntax_in_MediaWiki
//
// Example for {{PLURAL:$1|one category|$1 categories}}:
// HotCat.messages.multi_change = ['one category', '$1 categories'];
HotCat.messages.multi_change = '$1 categories';
// Automatic default is '[[' + category_canonical + ':$1]]'
//
// Override if, for example, the short edit summaries should mention
// not the standard category namespace, but a shorter namespace alias.
//
// * $1 - Category name
HotCat.messages.short_catchange = null;
// Any subcategory of this category is deemed a disambiguation category,
// i.e., a category that should not contain any items, but that contains
// links to other categories where stuff should be categorized.
// Use spaces, not underscores.
// If you don't have that concept on your wiki, set it to null.
HotCat.disambig_category = 'Disambiguation';
// Any category in this category is deemed a (soft) redirect to some other category defined by a link
// to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
// If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
// a disambiguation category instead.
HotCat.redir_category = 'Category redirects';
// a list of categories which can be removed by removing a template
// key: the category without namespace
// value: A regexp matching the template name, again without namespace
// If you don't have this at your wiki, or don't want this, leave it as an empty object {}.
HotCat.template_categories = {};
// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
// If not, set it to null.
HotCat.uncat_regexp = /\{\{\s*[Uu]ncategorized\s*[^}]*\}\}\s*(<!--.*?-->\s*)?/g;
// User-interface messages
//
// DO NOT EDIT
//
// These are site-agnostic and must not be translated here.
// HotCat automatically replaces these with translations from here:
// https://commons.wikimedia.org/wiki/Special:Prefixindex/MediaWiki:Gadget-HotCat.js
//
// See also: window.hotcat_translations_from_commons
//
HotCat.messages.cat_notFound = 'Category "$1" not found';
HotCat.messages.cat_exists = 'Category "$1" already exists; not added.';
HotCat.messages.commit = 'Save';
HotCat.messages.ok = 'OK';
HotCat.messages.cancel = 'Cancel';
HotCat.messages.multi_error = 'Could not retrieve the page text from the server. Therefore, your category changes cannot be saved. We apologize for the inconvenience.';
HotCat.categories = 'Categories';
HotCat.tooltips.change = 'Modify';
HotCat.tooltips.remove = 'Remove';
HotCat.tooltips.add = 'Add a new category';
HotCat.tooltips.restore = 'Undo changes';
HotCat.tooltips.undo = 'Undo changes';
HotCat.tooltips.down = 'Open for modifying and display subcategories';
HotCat.tooltips.up = 'Open for modifying and display parent categories';
HotCat.multi_tooltip = 'Modify several categories';
HotCat.engine_names.searchindex = 'Search index';
HotCat.engine_names.pagelist = 'Page list';
HotCat.engine_names.combined = 'Combined search';
HotCat.engine_names.subcat = 'Subcategories';
HotCat.engine_names.parentcat = 'Parent categories';
// Misc site configuration
//
// DO NOT EDIT
//
// Create the [[MediaWiki:Gadget-HotCat.js/local_defaults]] page
// on your wiki, and override only the configuration you want to change.
//
// For example:
//
// HotCat.upload_disabled = false;
//
Object.assign(HC, {
// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
links: {
change: '(±)',
remove: '(\u2212)',
add: '(+)',
restore: '(×)',
undo: '(×)',
down: '(\u2193)',
up: '(\u2191)'
},
changeTag: conf.wgUserName ? 'HotCat' : '', // if tag is missing, edit is rejected
// The HTML content of the "enter multi-mode" link at the front.
addmulti: '<span>+<sup>+</sup></span>',
isSupportedContentModel: function() {
return HC.wikitextContentModels.includes(conf.wgPageContentModel) ||
HC.jsonContentModels.includes(conf.wgPageContentModel);
},
// Return true to disable HotCat.
disable: function () {
var ns = conf.wgNamespaceNumber;
var nsIds = conf.wgNamespaceIds;
return (
ns < 0 || // Special pages; Special:Upload is handled differently
ns === 10 || // Templates
ns === 828 || // Module (Lua)
ns === 8 || // MediaWiki
ns === 6 && !conf.wgArticleId || // Non-existing file pages
ns === 2 && /\.(js|css|json)$/.test( conf.wgTitle ) || // User scripts
nsIds &&
( ns === nsIds.creator ||
ns === nsIds.timedtext ||
ns === nsIds.institution ) );
},
// https://www.mediawiki.org/wiki/Content_handlers
wikitextContentModels: [ 'wikitext', 'proofread-index', 'proofread-page' ],
jsonContentModels: [ 'Tabular.JsonConfig', 'Map.JsonConfig', 'Chart.JsonConfig' ],
// The images used for the little indication icon. Should not need changing.
existsYes: '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png',
existsNo: '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png',
// Override the decision of whether HotCat should help users by automatically
// capitalising the title in the user input text if the wiki has case-sensitive page names.
// Basically, this will make an API query to check the MediaWiki configuration and HotCat then sets
// this to true for most wikis, and to false on Wiktionary.
//
// You can set this directly if there is a problem with it. For example, Georgian Wikipedia (kawiki),
// is known to have different capitalisation logic between MediaWiki PHP and JavaScript. As such, automatic
// case changes in JavaScript by HotCat would be wrong.
capitalizePageNames: null,
// If upload_disabled is true, HotCat will not be used on the Upload form.
upload_disabled: false,
// Single regular expression matching blacklisted categories that cannot be changed or
// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
// word "maintenance" in its title.
blacklist: null,
// Stuff changeable by users:
// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
bg_changed: '#FCA',
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
// the changes; users must always save explicitly.
no_autocommit: false,
// If true, the "category deletion" link "(-)" will never save automatically but always show an
// edit page where the user has to save the edit manually. Is false by default because that's the
// traditional behavior. This setting overrides no_autocommit for "(-)" links.
del_needs_diff: false,
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
// server to get suggestions.
suggest_delay: 100,
// Default width, in characters, of the text input field.
editbox_width: 40,
// One of the engine_names above, to be used as the default suggestion engine.
suggestions: 'combined',
// If true, always use the default engine, and never display a selector.
fixed_search: false,
// If false, do not display the "up" and "down" links
use_up_down: true,
// Default list size
listSize: 10,
// If true, single category changes are marked as minor edits. If false, they're not.
single_minor: true,
// If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
// the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
// options in his or her preferences set.
dont_add_to_watchlist: false,
shortcuts: null,
addShortcuts: function ( map ) {
if ( !map ) return;
window.HotCat.shortcuts = window.HotCat.shortcuts || {};
for ( var k in map ) {
if ( !map.hasOwnProperty( k ) || typeof k !== 'string' ) continue;
var v = map[ k ];
if ( typeof v !== 'string' ) continue;
k = k.replace( /^\s+|\s+$/g, '' );
v = v.replace( /^\s+|\s+$/g, '' );
if ( !k.length || !v.length ) continue;
window.HotCat.shortcuts[ k ] = v;
}
}
});
// More backwards compatibility. We have a few places where we test for the browser: once for
// Safari < 3.0, and twice for WebKit (Chrome or Safari, any versions)
var ua = navigator.userAgent.toLowerCase();
var is_webkit = /applewebkit\/\d+/.test( ua ) && ua.indexOf( 'spoofer' ) < 0;
var cat_prefix = null;
var noSuggestions = false;
function LoadTrigger( needed ) {
// Define methods in a closure so that self reference is available,
// also allows method calls to be detached.
var self = this;
self.queue = [];
self.needed = needed;
self.register = function ( callback ) {
if ( self.needed <= 0 ) callback(); // Execute directly
else self.queue.push( callback );
};
self.loaded = function () {
self.needed--;
if ( self.needed === 0 ) {
// Run queued callbacks once
for ( var i = 0; i < self.queue.length; i++ ) self.queue[ i ]();
self.queue = [];
}
};
}
// Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
var loadTrigger = new LoadTrigger( 2 );
function load( uri, callback ) {
var s = document.createElement( 'script' );
s.src = uri;
var called = false;
s.onload = s.onerror = function () {
if ( !called && callback ) {
called = true;
callback();
}
if ( s.parentNode ) {
s.parentNode.removeChild( s );
}
};
document.head.appendChild( s );
}
function loadJS( page, callback ) {
load( conf.wgScript + '?title=' + encodeURIComponent( page ) + '&action=raw&ctype=text/javascript', callback );
}
function loadURI( href, callback ) {
load( href, callback );
}
// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
// from the wiki where this script is executing, even if this script itself is hotlinked from Commons. This can
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
loadJS( 'MediaWiki:Gadget-HotCat.js/local_defaults', loadTrigger.loaded );
// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
// should be localized in /local_defaults above.
if ( conf.wgUserLanguage !== 'en' ) {
// Lupo: somebody thought it would be a good idea to add this. So the default is true, and you have to set it to false
// explicitly if you're not on Commons and don't want that.
if ( window.hotcat_translations_from_commons === undefined ) window.hotcat_translations_from_commons = true;
// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
if ( window.hotcat_translations_from_commons && conf.wgDBname !== 'commonswiki' ) {
loadURI( '//commons.wikimedia.org/w/index.php?title=' +
'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage +
'&action=raw&ctype=text/javascript', loadTrigger.loaded );
} else {
// Load translations locally
loadJS( 'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage, loadTrigger.loaded );
}
} else {
loadTrigger.loaded();
}
// No further changes should be necessary here.
// The following regular expression strings are used when searching for categories in wikitext.
var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );
// Regexp for handling blanks inside a category title or namespace name.
// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
// MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
// number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
// Therefore, when looking for page titles in wikitext, we must handle all these cases.
// Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
// a link must be on one single line.
// MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
// or adjacent to and inside of "[[" and "]]").
// First auto-localize the regexps for the category and the template namespaces.
var formattedNamespaces = conf.wgFormattedNamespaces;
var namespaceIds = conf.wgNamespaceIds;
function autoLocalize( namespaceNumber, fallback ) {
function createRegexpStr( name ) {
if ( !name || !name.length ) return '';
var regex_name = '';
for ( var i = 0; i < name.length; i++ ) {
var initial = name.charAt( i ),
ll = initial.toLowerCase(),
ul = initial.toUpperCase();
if ( ll === ul ) regex_name += initial; else regex_name += '[' + ll + ul + ']';
}
return regex_name
.replace( /([\\^$.?*+()])/g, '\\$1' )
.replace( wikiTextBlankRE, wikiTextBlank );
}
fallback = fallback.toLowerCase();
var canonical = formattedNamespaces[ String( namespaceNumber ) ].toLowerCase();
var regexp = createRegexpStr( canonical );
if ( fallback && canonical !== fallback ) regexp += '|' + createRegexpStr( fallback );
if ( namespaceIds ) {
for ( var cat_name in namespaceIds ) {
if (
typeof cat_name === 'string' &&
cat_name.toLowerCase() !== canonical &&
cat_name.toLowerCase() !== fallback &&
namespaceIds[ cat_name ] === namespaceNumber
) {
regexp += '|' + createRegexpStr( cat_name );
}
}
}
return regexp;
}
HC.category_canonical = formattedNamespaces[ '14' ];
HC.category_regexp = autoLocalize( 14, 'category' );
if ( formattedNamespaces[ '10' ] ) HC.template_regexp = autoLocalize( 10, 'template' );
// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
// to keep this whole stuff in a single file not depending on any other on-wiki JavaScripts, we re-do
// these few operations here.
function make( arg, literal ) {
if ( !arg ) return null;
return literal ? document.createTextNode( arg ) : document.createElement( arg );
}
function param( name, uri ) {
uri = uri || location.href;
var re = new RegExp( '[&?]' + name + '=([^&#]*)' );
var m = re.exec( uri );
if ( m && m.length > 1 ) return decodeURIComponent( m[ 1 ] );
return null;
}
function title( href ) {
if ( !href ) return null;
// href="/w/index.php?title=..."
if ( href.indexOf( expandedScriptPrefix ) === 0 ) {
return param( 'title', href );
}
// href="/wiki/..."
if ( href.indexOf( expandedArticlePrefix ) === 0 ) {
return decodeURIComponent( href.slice( expandedArticlePrefix.length ) );
}
return null;
}
function hasClass( elem, name ) {
return ( ' ' + elem.className + ' ' ).indexOf( ' ' + name + ' ' ) >= 0;
}
function capitalize( str ) {
if ( !str || !str.length ) return str;
return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 );
}
function wikiPagePath( pageName ) {
// Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
// a query parameter.
return conf.wgArticlePath.replace( '$1', encodeURIComponent( pageName ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' ) );
}
function escapeRE( str ) {
return str.replace( /([\\^$.?*+()[\]])/g, '\\$1' );
}
function substituteFactory( options ) {
options = options || {};
var lead = options.indicator || '$';
var indicator = escapeRE( lead );
var lbrace = escapeRE( options.lbrace || '{' );
var rbrace = escapeRE( options.rbrace || '}' );
var re;
re = new RegExp(
// $$
'(?:' + indicator + '(' + indicator + '))|' +
// $0, $1
'(?:' + indicator + '(\\d+))|' +
// ${key}
'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' +
// $key (only if first char after $ is not $, digit, or { )
'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)',
'g'
);
// Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $.
return function ( str, map ) {
if ( !map ) return str;
return str.replace( re, function ( match, prefix, idx, key, alpha ) {
if ( prefix === lead ) return lead;
var k = alpha || key || idx;
var replacement = typeof map[ k ] === 'function' ? map[ k ]( match, k ) : map[ k ];
return typeof replacement === 'string' ? replacement : ( replacement || match );
} );
};
}
var substitute = substituteFactory();
var replaceShortcuts = ( function () {
var replaceHash = substituteFactory( {
indicator: '#',
lbrace: '[',
rbrace: ']'
} );
return function ( str, map ) {
var s = replaceHash( str, map );
return HC.capitalizePageNames ? capitalize( s ) : s;
};
}() );
// Text modification
var findCatsRE =
new RegExp( '\\[\\[' + wikiTextBlankOrBidi + '(?:' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g' );
function replaceByBlanks( match ) {
return match.replace( /(\s|\S)/g, ' ' ); // /./ doesn't match linebreaks. /(\s|\S)/ does.
}
/**
* find_category() Tests whether a specified category exists in the given text and returns match information.
*
* @param {string} text - The input string to search for category patterns (JSON or wikitext).
* @param {string} category - The target category name (without the "Category:" prefix).
* @param {boolean} once - If true, return only the first match; if false, return all matches.
* @returns {Array<string>|Array<Array<string>>|null}
* - When `once` is true:
* - Returns an Array of strings representing the first regexp match groups.
* - Returns `null` if no match is found.
* - When `once` is false:
* - Returns an Array of Arrays, each inner Array containing regexp match groups for each occurrence.
* - Returns an empty Array if no matches are found.
*/
function find_category( text, category, once ) {
if ( HC.jsonContentModels.includes( conf.wgPageContentModel ) ) {
// Handle Data namespace JSON content
return find_category_json( text, category, once );
}
else {
// For backwards-compatibility the default handler is wikitext
return find_category_wikitext( text, category, once );
}
}
function find_category_json( jsontext, category, once ) {
// Handle Data namespace JSON content
let jsonData;
try {
jsonData = JSON.parse( jsontext );
} catch ( err ) {
console.error( 'Invalid JSON in find_category_json:', err );
return once ? null : [];
}
// Check if mediawikiCategories exists and if not then return
if ( !Array.isArray( jsonData.mediawikiCategories ) ) {
return once ? null : [];
}
// Handle actual data
var matches = [];
const targetCategoryName = mw.Title.newFromText( "category:" + category ).getPrefixedText();
// Search through each category object
for ( var i = 0; i < jsonData.mediawikiCategories.length; i++ ) {
var catObj = jsonData.mediawikiCategories[ i ];
const currentCategoryName = mw.Title.newFromText( "category:" + catObj.name ).getPrefixedText();
// Compare category name (case-sensitive)
if ( currentCategoryName === targetCategoryName ) {
var match = [
'', // Full match placeholder
'', // Namespace placeholder
catObj.sort || '' // Sortkey if exists
];
if ( once ) {
return { match };
}
matches.push( { match: match } );
}
}
return once ? null : matches;
}
function find_category_wikitext( wikitext, category, once ) {
var cat_regex = null;
if ( HC.template_categories[ category ] ) {
cat_regex = new RegExp(
'\\{\\{' + wikiTextBlankOrBidi + '(' + HC.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi +
'(?:' + HC.template_categories[ category ] + ')' +
wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}',
'g'
);
} else {
var cat_name = escapeRE( category );
var initial = cat_name.substr( 0, 1 );
cat_regex = new RegExp(
'\\[\\[' + wikiTextBlankOrBidi + '(' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi +
( initial === '\\' || !HC.capitalizePageNames ?
initial :
'[' + initial.toUpperCase() + initial.toLowerCase() + ']' ) +
cat_name.substring( 1 ).replace( wikiTextBlankRE, wikiTextBlank ) +
wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]',
'g'
);
}
if ( once ) return cat_regex.exec( wikitext );
var copiedtext = wikitext
.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
var result = [];
var curr_match = null;
while ( ( curr_match = cat_regex.exec( copiedtext ) ) !== null ) {
result.push( {
match: curr_match
} );
}
result.re = cat_regex;
return result; // An array containing all matches, with positions, in result[ i ].match
}
var interlanguageRE = null;
function change_category( text, toRemove, toAdd, key, is_hidden ) {
if ( HC.jsonContentModels.includes( conf.wgPageContentModel ) ) {
// Handle Data namespace JSON content
return change_category_json( text, toRemove, toAdd, key, is_hidden );
}
else {
// For backwards-compatibility the default handler is wikitext
return change_category_wikitext( text, toRemove, toAdd, key, is_hidden );
}
// In cases of unknown page content model
return {
text: text,
summary: [],
error: 'Unknown wgPageContentModel: ' + conf.wgPageContentModel
};
}
function change_category_json( jsontext, toRemove, toAdd, key, is_hidden ) {
try {
var jsonData = JSON.parse( jsontext );
if ( !jsonData.mediawikiCategories ) {
jsonData.mediawikiCategories = [];
}
var summary = [];
// Remove category if specified
if ( toRemove ) {
var initialLength = jsonData.mediawikiCategories.length;
var normalizedToRemoveName = mw.Title.newFromText( "category:" + toRemove ).getPrefixedText();
jsonData.mediawikiCategories = jsonData.mediawikiCategories.filter( function( cat ) {
var normalizedCatName = mw.Title.newFromText( "category:" + cat.name ).getPrefixedText();
return normalizedCatName !== normalizedToRemoveName;
} );
if ( jsonData.mediawikiCategories.length < initialLength ) {
summary.push( HC.messages.cat_removed.replace( /\$1/g, toRemove ) );
}
}
// Add category if specified
if ( toAdd ) {
var normalizedToAddName = mw.Title.newFromText( "category:" + toAdd ).getPrefixedText();
var exists = jsonData.mediawikiCategories.some( function( cat ) {
var normalizedCatName = mw.Title.newFromText( "category:" + cat.name ).getPrefixedText();
return normalizedCatName === normalizedToAddName;
} );
if ( !exists ) {
jsonData.mediawikiCategories.push( {
name: toAdd,
sort: key || ''
} );
summary.push( HC.messages.cat_added.replace( /\$1/g, toAdd ) );
} else if ( key ) {
// Update sortkey if category exists and new key is provided
for ( var i = 0; i < jsonData.mediawikiCategories.length; i++ ) {
var cat = jsonData.mediawikiCategories[ i ];
var normalizedCatName = mw.Title.newFromText( "category:" + cat.name ).getPrefixedText();
if ( normalizedCatName === normalizedToAddName && ( !cat.sort || cat.sort !== key ) ) {
jsonData.mediawikiCategories[ i ].sort = key;
summary.push( HC.messages.cat_keychange.replace( /\$1/g, toAdd ).replace( /\$2/g, key ) );
break;
}
}
}
}
return {
text: JSON.stringify( jsonData, null, 2 ),
summary: summary,
error: null
};
} catch ( e ) {
return {
text: jsontext,
summary: [],
error: 'Failed to modify JSON categories: ' + e.message
};
}
}
function change_category_wikitext( wikitext, toRemove, toAdd, key, is_hidden ) {
function find_insertionpoint( wikitext ) {
var copiedtext = wikitext
.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
var index = -1;
findCatsRE.lastIndex = 0;
while ( findCatsRE.exec( copiedtext ) !== null ) index = findCatsRE.lastIndex;
if ( index < 0 ) {
// Find the index of the first interlanguage link...
var match = null;
if ( !interlanguageRE ) {
// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
// and "tokipona".
match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec( copiedtext );
} else {
match = interlanguageRE.exec( copiedtext );
}
if ( match ) index = match.index;
return {
idx: index,
onCat: false
};
}
return {
idx: index,
onCat: index >= 0
};
}
var summary = [],
nameSpace = HC.category_canonical,
cat_point = -1, // Position of removed category;
keyChange = ( toRemove && toAdd && toRemove === toAdd && toAdd.length ),
matches;
if ( key ) key = '|' + key;
// Remove
if ( toRemove && toRemove.length ) {
matches = find_category( wikitext, toRemove );
if ( !matches || !matches.length ) {
return {
text: wikitext,
summary: summary,
error: HC.messages.cat_notFound.replace( /\$1/g, toRemove )
};
} else {
var before = wikitext.substring( 0, matches[ 0 ].match.index ),
after = wikitext.substring( matches[ 0 ].match.index + matches[ 0 ].match[ 0 ].length );
if ( matches.length > 1 ) {
// Remove all occurrences in after
matches.re.lastIndex = 0;
after = after.replace( matches.re, '' );
}
if ( toAdd ) {
// nameSpace = matches[ 0 ].match[ 1 ] || nameSpace; Canonical namespace should be always preferred
if ( key === null ) key = matches[ 0 ].match[ 2 ];
// Remember the category key, if any.
}
// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
// If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
// whitespace characters, insert a blank.
var i = before.length - 1;
while ( i >= 0 && before.charAt( i ) !== '\n' && before.substr( i, 1 ).search( /\s/ ) >= 0 ) i--;
var j = 0;
while ( j < after.length && after.charAt( j ) !== '\n' && after.substr( j, 1 ).search( /\s/ ) >= 0 ) j++;
if ( i >= 0 && before.charAt( i ) === '\n' && ( !after.length || j < after.length && after.charAt( j ) === '\n' ) ) i--;
if ( i >= 0 ) before = before.substring( 0, i + 1 ); else before = '';
if ( j < after.length ) after = after.substring( j ); else after = '';
if (
before.length && before.substring( before.length - 1 ).search( /\S/ ) >= 0 &&
after.length && after.substr( 0, 1 ).search( /\S/ ) >= 0
) {
before += ' ';
}
cat_point = before.length;
if ( cat_point === 0 && after.length && after.substr( 0, 1 ) === '\n' ) after = after.substr( 1 );
wikitext = before + after;
if ( !keyChange ) {
if ( HC.template_categories[ toRemove ] ) { summary.push( HC.messages.template_removed.replace( /\$1/g, toRemove ) ); } else { summary.push( HC.messages.cat_removed.replace( /\$1/g, toRemove ) ); }
}
}
}
// Add
if ( toAdd && toAdd.length ) {
matches = find_category( wikitext, toAdd );
if ( matches && matches.length ) {
// Already exists
return {
text: wikitext,
summary: summary,
error: HC.messages.cat_exists.replace( /\$1/g, toAdd )
};
} else {
var onCat = false;
if ( cat_point < 0 ) {
var point = find_insertionpoint( wikitext );
cat_point = point.idx;
onCat = point.onCat;
} else {
onCat = true;
}
var newcatstring = '[[' + nameSpace + ':' + toAdd + ( key || '' ) + ']]';
if ( cat_point >= 0 ) {
var suffix = wikitext.substring( cat_point );
wikitext = wikitext.substring( 0, cat_point ) + ( cat_point > 0 ? '\n' : '' ) + newcatstring + ( !onCat ? '\n' : '' );
if ( suffix.length && suffix.substr( 0, 1 ) !== '\n' ) wikitext += '\n' + suffix; else wikitext += suffix;
} else {
if ( wikitext.length && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) wikitext += '\n';
wikitext += ( wikitext.length ? '\n' : '' ) + newcatstring;
}
if ( keyChange ) {
var k = key || '';
if ( k.length ) k = k.substr( 1 );
summary.push( substitute( HC.messages.cat_keychange, [ null, toAdd, k ] ) );
} else {
summary.push( HC.messages.cat_added.replace( /\$1/g, toAdd ) );
}
if ( HC.uncat_regexp && !is_hidden ) {
var txt = wikitext.replace( HC.uncat_regexp, '' ); // Remove "uncat" templates
if ( txt.length !== wikitext.length ) {
wikitext = txt;
summary.push( HC.messages.uncat_removed );
}
}
}
}
return {
text: wikitext,
summary: summary,
error: null
};
}
// The real HotCat UI
function evtKeys( e ) {
/* eslint-disable no-bitwise */
var code = 0;
if ( e.ctrlKey ) { // All modern browsers
// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
// as a ctrl-click, too.
if ( e.ctrlKey || e.metaKey ) code |= 1;
if ( e.shiftKey ) code |= 2;
}
return code;
}
function evtKill( e ) {
if ( e.preventDefault ) {
e.preventDefault();
e.stopPropagation();
} else {
e.cancelBubble = true;
}
return false;
}
var catLine = null,
onUpload = false,
editors = [],
commitButton = null,
commitForm = null,
multiSpan = null,
pageText = null,
pageTime = null,
pageWatched = false,
watchCreate = false,
watchCreateExpiry = '',
watchEdit = false,
watchDefaultExpiry = '',
minorEdits = false,
editToken = null,
is_rtl = false,
serverTime = null,
lastRevId = null,
pageTextRevId = null,
conflictingUser = null,
newDOM = false; // true if MediaWiki serves the new UL-LI DOM for categories
function CategoryEditor() {
this.initialize.apply( this, arguments );
}
function setPage( json ) {
var startTime = null;
if ( json && json.query ) {
if ( json.query.pages ) {
var page = json.query.pages[ !conf.wgArticleId ? '-1' : String( conf.wgArticleId ) ];
if ( page ) {
if ( page.revisions && page.revisions.length ) {
// Revisions are sorted by revision ID, hence [ 0 ] is the one we asked for, and possibly there's a [ 1 ] if we're
// not on the latest revision (edit conflicts and such).
pageText = page.revisions[ 0 ].slots.main[ '*' ];
if ( page.revisions[ 0 ].timestamp ) pageTime = page.revisions[ 0 ].timestamp.replace( /\D/g, '' );
if ( page.revisions[ 0 ].revid ) pageTextRevId = page.revisions[ 0 ].revid;
if ( page.revisions.length > 1 ) conflictingUser = page.revisions[ 1 ].user;
}
if ( page.lastrevid ) lastRevId = page.lastrevid;
if ( page.starttimestamp ) startTime = page.starttimestamp.replace( /\D/g, '' );
pageWatched = typeof page.watched === 'string';
if ( json.query.tokens ) editToken = json.query.tokens.csrftoken;
if ( page.langlinks && ( !json[ 'query-continue' ] || !json[ 'query-continue' ].langlinks ) ) {
// We have interlanguage links, and we got them all.
var re = '';
for ( var i = 0; i < page.langlinks.length; i++ ) re += ( i > 0 ? '|' : '' ) + page.langlinks[ i ].lang.replace( /([\\^$.?*+()])/g, '\\$1' );
if ( re.length ) interlanguageRE = new RegExp( '((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$' );
}
}
}
// Siteinfo
if ( json.query.general ) {
if ( json.query.general.time && !startTime ) startTime = json.query.general.time.replace( /\D/g, '' );
if ( HC.capitalizePageNames === null ) {
// ResourceLoader's JSParser doesn't like .case, so override eslint.
// eslint-disable-next-line dot-notation
HC.capitalizePageNames = ( json.query.general[ 'case' ] === 'first-letter' );
}
}
serverTime = startTime;
// Userinfo
if ( json.query.userinfo && json.query.userinfo.options ) {
watchCreate = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchcreations === '1';
watchCreateExpiry = json.query.userinfo.options[' watchcreations-expiry'] || '';
watchEdit = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchdefault === '1';
watchDefaultExpiry = json.query.userinfo.options['watchdefault-expiry'] || '';
minorEdits = json.query.userinfo.options.minordefault === 1;
// If the user has the "All edits are minor" preference enabled, we should honor that
// for single category changes, no matter what the site configuration is.
if ( minorEdits ) HC.single_minor = true;
}
}
}
var saveInProgress = false;
function initiateEdit( doEdit, failure ) {
if ( saveInProgress ) return;
saveInProgress = true;
var oldButtonState;
if ( commitButton ) {
oldButtonState = commitButton.disabled;
commitButton.disabled = true;
}
function fail() {
saveInProgress = false;
if ( commitButton ) commitButton.disabled = oldButtonState;
failure.apply( this, arguments );
}
// Must use Ajax here to get the user options and the edit token.
$.getJSON(
conf.wgScriptPath + '/api.php?' +
'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent( conf.wgPageName ) +
'&prop=info%7Crevisions%7Clanglinks&inprop=watched&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500&rvslots=main' +
'&rvlimit=2&rvdir=newer&rvstartid=' + conf.wgCurRevisionId + '&meta=siteinfo%7Cuserinfo%7Ctokens&type=csrf&uiprop=options',
function ( json ) {
setPage( json );
doEdit( fail );
}
).fail( function ( req ) {
fail( req.status + ' ' + req.statusText );
} );
}
function multiChangeMsg( count ) {
var msg = HC.messages.multi_change;
if ( typeof msg !== 'string' && msg.length )
if ( mw.language && mw.language.convertPlural ) { msg = mw.language.convertPlural( count, msg ); } else { msg = msg[ msg.length - 1 ]; }
return substitute( msg, [ null, String( count ) ] );
}
function currentTimestamp() {
var now = new Date();
var ts = String( now.getUTCFullYear() );
function two( s ) {
return s.substr( s.length - 2 );
}
ts +=
two( '0' + ( now.getUTCMonth() + 1 ) ) +
two( '0' + now.getUTCDate() ) +
two( '00' + now.getUTCHours() ) +
two( '00' + now.getUTCMinutes() ) +
two( '00' + now.getUTCSeconds() );
return ts;
}
function performChanges( failure, singleEditor ) {
if ( pageText === null ) {
failure( HC.messages.multi_error );
return;
}
// Backwards compatibility after message change (added $2 to cat_keychange)
if ( HC.messages.cat_keychange.indexOf( '$2' ) < 0 ) HC.messages.cat_keychange += '"$2"';
// More backwards-compatibility with earlier HotCat versions:
if ( !HC.messages.short_catchange ) HC.messages.short_catchange = '[[' + HC.category_canonical + ':$1]]';
// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
// (b) we want to show a diff for multi-edits anyway, and
// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
// automatically reloads the page with the updated categories on a successful submit, which
// we would have to do explicitly if we used the edit API.
var action;
// Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
// server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
// server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
// if you save, any more recent changes will be lost" screen.
var selfEditConflict = ( lastRevId !== null && lastRevId !== conf.wgCurRevisionId || pageTextRevId !== null &&
pageTextRevId !== conf.wgCurRevisionId ) && conflictingUser && conflictingUser === conf.wgUserName;
if ( singleEditor && !singleEditor.noCommit && !HC.no_autocommit && editToken && !selfEditConflict ) {
// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
commitForm.wpEditToken.value = editToken;
action = commitForm.wpDiff;
if ( action ) action.name = action.value = 'wpSave';
} else {
action = commitForm.wpSave;
if ( action ) action.name = action.value = 'wpDiff';
}
var result = {
text: pageText
},
changed = [],
added = [],
deleted = [],
changes = 0,
toEdit = singleEditor ? [ singleEditor ] : editors,
error = null,
edit,
i;
for ( i = 0; i < toEdit.length; i++ ) {
edit = toEdit[ i ];
if ( edit.state === CategoryEditor.CHANGED ) {
result = change_category(
result.text,
edit.originalCategory,
edit.currentCategory,
edit.currentKey,
edit.currentHidden );
if ( !result.error ) {
changes++;
if ( !edit.originalCategory || !edit.originalCategory.length ) {
added.push( edit.currentCategory );
} else {
changed.push( {
from: edit.originalCategory,
to: edit.currentCategory
} );
}
} else if ( error === null ) {
error = result.error;
}
} else if (
edit.state === CategoryEditor.DELETED && edit.originalCategory && edit.originalCategory.length ) {
result = change_category(
result.text,
edit.originalCategory,
null, null, false );
if ( !result.error ) {
changes++;
deleted.push( edit.originalCategory );
} else if ( error === null ) {
error = result.error;
}
}
}
if ( error !== null ) { // Do not commit if there were errors
action = commitForm.wpSave;
if ( action ) action.name = action.value = 'wpDiff';
}
// Fill in the form and submit it
commitForm.wpMinoredit.checked = minorEdits;
commitForm.wpWatchthis.checked = !conf.wgArticleId && watchCreate || watchEdit || pageWatched;
// Set watchlist expiry according to user preference
// If page is already in watchlist do not update expiry
if (commitForm.wpWatchthis.checked && !pageWatched) {
if (!conf.wgArticleId && watchCreate) {
commitForm.wpWatchlistExpiry.value = watchCreateExpiry;
} else {
commitForm.wpWatchlistExpiry.value = watchDefaultExpiry;
}
}
if ( conf.wgArticleId || !!singleEditor ) {
// Prepare change-tag save
if ( action && action.value === 'wpSave' ) {
if ( HC.changeTag ) {
commitForm.wpChangeTags.value = HC.changeTag;
HC.messages.using = '';
HC.messages.prefix = '';
}
} else {
commitForm.wpAutoSummary.value = HC.changeTag;
}
if ( changes === 1 ) {
if ( result.summary && result.summary.length ) commitForm.wpSummary.value = HC.messages.prefix + result.summary.join( HC.messages.separator ) + HC.messages.using;
commitForm.wpMinoredit.checked = HC.single_minor || minorEdits;
} else if ( changes ) {
var summary = [];
var shortSummary = [];
// Deleted
for ( i = 0; i < deleted.length; i++ ) summary.push( '−' + substitute( HC.messages.short_catchange, [ null, deleted[ i ] ] ) );
if ( deleted.length === 1 ) shortSummary.push( '−' + substitute( HC.messages.short_catchange, [ null, deleted[ 0 ] ] ) ); else if ( deleted.length ) shortSummary.push( '− ' + multiChangeMsg( deleted.length ) );
// Added
for ( i = 0; i < added.length; i++ ) summary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ i ] ] ) );
if ( added.length === 1 ) shortSummary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ 0 ] ] ) ); else if ( added.length ) shortSummary.push( '+ ' + multiChangeMsg( added.length ) );
// Changed
var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
for ( i = 0; i < changed.length; i++ ) {
if ( changed[ i ].from !== changed[ i ].to ) {
summary.push(
'±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) + arrow +
substitute( HC.messages.short_catchange, [ null, changed[ i ].to ] )
);
} else {
summary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) );
}
}
if ( changed.length === 1 ) {
if ( changed[ 0 ].from !== changed[ 0 ].to ) {
shortSummary.push(
'±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) + arrow +
substitute( HC.messages.short_catchange, [ null, changed[ 0 ].to ] )
);
} else {
shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) );
}
} else if ( changed.length ) {
shortSummary.push( '± ' + multiChangeMsg( changed.length ) );
}
if ( summary.length ) {
summary = summary.join( HC.messages.separator );
if ( summary.length > 200 - HC.messages.prefix.length - HC.messages.using.length ) summary = shortSummary.join( HC.messages.separator );
commitForm.wpSummary.value = HC.messages.prefix + summary + HC.messages.using;
}
}
}
commitForm.wpTextbox1.value = result.text;
commitForm.wpStarttime.value = serverTime || currentTimestamp();
commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
if ( selfEditConflict ) commitForm.oldid.value = String( pageTextRevId || conf.wgCurRevisionId );
// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
commitForm.hcCommit.click();
}
function resolveOne( page, toResolve ) {
var cats = page.categories,
lks = page.links,
is_dab = false,
is_redir = typeof page.redirect === 'string', // Hard redirect?
is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string',
is_missing = typeof page.missing === 'string',
i;
for ( i = 0; i < toResolve.length; i++ ) {
if ( i && toResolve[ i ].dabInputCleaned !== page.title.substring( page.title.indexOf( ':' ) + 1 ) ) continue;
// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
toResolve[ i ].currentHidden = is_hidden;
toResolve[ i ].inputExists = !is_missing;
toResolve[ i ].icon.src = ( is_missing ? HC.existsNo : HC.existsYes );
}
if ( is_missing ) return;
if ( !is_redir && cats && ( HC.disambig_category || HC.redir_category ) ) {
for ( var c = 0; c < cats.length; c++ ) {
var cat = cats[ c ].title;
// Strip namespace prefix
if ( cat ) {
cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
if ( cat === HC.disambig_category ) {
is_dab = true;
break;
} else if ( cat === HC.redir_category ) {
is_redir = true;
break;
}
}
}
}
if ( !is_redir && !is_dab ) return;
if ( !lks || !lks.length ) return;
var titles = [];
for ( i = 0; i < lks.length; i++ ) {
if (
// Category namespace -- always true since we ask only for the category links
lks[ i ].ns === 14 &&
// Name not empty
lks[ i ].title && lks[ i ].title.length
) {
// Internal link to existing thingy. Extract the page name and remove the namespace.
var match = lks[ i ].title;
match = match.substring( match.indexOf( ':' ) + 1 );
// Exclude blacklisted categories.
if ( !HC.blacklist || !HC.blacklist.test( match ) ) titles.push( match );
}
}
if ( !titles.length ) return;
for ( i = 0; i < toResolve.length; i++ ) {
if ( i && toResolve[ i ].dabInputCleaned !== page.title.substring( page.title.indexOf( ':' ) + 1 ) ) continue;
toResolve[ i ].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
toResolve[ i ].icon.src = HC.existsYes;
if ( titles.length > 1 ) {
toResolve[ i ].dab = titles;
} else {
toResolve[ i ].text.value =
titles[ 0 ] + ( toResolve[ i ].currentKey !== null ? '|' + toResolve[ i ].currentKey : '' );
}
}
}
function resolveRedirects( toResolve, params ) {
if ( !params || !params.query || !params.query.pages ) return;
for ( var p in params.query.pages ) resolveOne( params.query.pages[ p ], toResolve );
}
function resolveMulti( toResolve, callback ) {
var i;
for ( i = 0; i < toResolve.length; i++ ) {
toResolve[ i ].dab = null;
toResolve[ i ].dabInput = toResolve[ i ].lastInput;
}
if ( noSuggestions ) {
callback( toResolve );
return;
}
// Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
// category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14' +
'&pllimit=' + ( toResolve.length * 10 ) +
'&cllimit=' + ( toResolve.length * 10 ) +
'&format=json&titles=';
for ( i = 0; i < toResolve.length; i++ ) {
var v = toResolve[ i ].dabInput;
v = replaceShortcuts( v, HC.shortcuts );
toResolve[ i ].dabInputCleaned = v;
args += encodeURIComponent( 'Category:' + v );
if ( i + 1 < toResolve.length ) args += '%7C';
}
$.getJSON( conf.wgScriptPath + '/api.php?' + args,
function ( json ) {
resolveRedirects( toResolve, json );
callback( toResolve );
} ).fail( function ( req ) {
if ( !req ) noSuggestions = true;
callback( toResolve );
} );
}
function makeActive( which ) {
if ( which.is_active ) return;
for ( var i = 0; i < editors.length; i++ )
if ( editors[ i ] !== which ) editors[ i ].inactivate();
which.is_active = true;
if ( which.dab ) {
// eslint-disable-next-line no-use-before-define
showDab( which );
} else {
// Check for programmatic value changes.
var expectedInput = which.lastRealInput || which.lastInput || '';
var actualValue = which.text.value || '';
if ( !expectedInput.length && actualValue.length || expectedInput.length && actualValue.indexOf( expectedInput ) ) {
// Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
// cursor at the end of the category, and do not display the old suggestion list.
which.showsList = false;
var v = actualValue.split( '|' );
which.lastRealInput = which.lastInput = v[ 0 ];
if ( v.length > 1 ) which.currentKey = v[ 1 ];
if ( which.lastSelection ) {
which.lastSelection = {
start: v[ 0 ].length,
end: v[ 0 ].length
};
}
}
if ( which.showsList ) which.displayList();
if ( which.lastSelection ) {
if ( is_webkit ) {
// WebKit (Safari, Chrome) has problems selecting inside focus()
// See http://code.google.com/p/chromium/issues/detail?id=32865#c6
window.setTimeout(
function () {
which.setSelection( which.lastSelection.start, which.lastSelection.end );
},
1 );
} else {
which.setSelection( which.lastSelection.start, which.lastSelection.end );
}
}
}
}
function showDab( which ) {
if ( !which.is_active ) {
makeActive( which );
} else {
which.showSuggestions( which.dab, false, null, null ); // do autocompletion, no key, no engine selector
which.dab = null;
}
}
function multiSubmit() {
var toResolve = [];
for ( var i = 0; i < editors.length; i++ )
if ( editors[ i ].state === CategoryEditor.CHANGE_PENDING || editors[ i ].state === CategoryEditor.OPEN ) toResolve.push( editors[ i ] );
if ( !toResolve.length ) {
initiateEdit( function ( failure ) {
performChanges( failure );
}, function ( msg ) {
alert( msg );
} );
return;
}
resolveMulti( toResolve, function ( resolved ) {
var firstDab = null;
var dontChange = false;
for ( var i = 0; i < resolved.length; i++ ) {
if ( resolved[ i ].lastInput !== resolved[ i ].dabInput ) {
// We didn't disable all the open editors, but we did asynchronous calls. It is
// theoretically possible that the user changed something...
dontChange = true;
} else {
if ( resolved[ i ].dab ) {
if ( !firstDab ) firstDab = resolved[ i ];
} else {
if ( resolved[ i ].acceptCheck( true ) ) resolved[ i ].commit();
}
}
}
if ( firstDab ) {
showDab( firstDab );
} else if ( !dontChange ) {
initiateEdit( function ( failure ) {
performChanges( failure );
}, function ( msg ) {
alert( msg );
} );
}
} );
}
function setMultiInput() {
if ( commitButton || onUpload ) return;
commitButton = make( 'input' );
commitButton.type = 'button';
commitButton.value = HC.messages.commit;
commitButton.onclick = multiSubmit;
if ( multiSpan ) multiSpan.parentNode.replaceChild( commitButton, multiSpan ); else catLine.appendChild( commitButton );
}
function checkMultiInput() {
if ( !commitButton ) return;
var hasChanges = false;
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
hasChanges = true;
break;
}
}
commitButton.disabled = !hasChanges;
}
var suggestionEngines = {
opensearch: {
uri: '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1', // $1 = search term
// Function to convert result of uri into an array of category names
handler: function ( queryResult, queryKey ) {
if ( queryResult && queryResult.length >= 2 ) {
var key = queryResult[ 0 ].substring( queryResult[ 0 ].indexOf( ':' ) + 1 );
var titles = queryResult[ 1 ];
var exists = false;
if ( !cat_prefix ) cat_prefix = new RegExp( '^(' + HC.category_regexp + '):' );
for ( var i = 0; i < titles.length; i++ ) {
cat_prefix.lastIndex = 0;
var m = cat_prefix.exec( titles[ i ] );
if ( m && m.length > 1 ) {
titles[ i ] = titles[ i ].substring( titles[ i ].indexOf( ':' ) + 1 ); // rm namespace
if ( key === titles[ i ] ) exists = true;
} else {
titles.splice( i, 1 ); // Nope, it's not a category after all.
i--;
}
}
titles.exists = exists;
if ( queryKey !== key ) titles.normalized = key;
// Remember the NFC normalized key we got back from the server
return titles;
}
return null;
}
},
internalsearch: {
uri: '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1',
handler: function ( queryResult ) {
if ( queryResult && queryResult.query && queryResult.query.allpages ) {
var titles = queryResult.query.allpages;
for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace
return titles;
}
return null;
}
},
exists: {
uri: '/api.php?format=json&action=query&prop=info&titles=Category:$1',
handler: function ( queryResult, queryKey ) {
if ( queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[ -1 ] ) {
// Should have exactly 1
for ( var p in queryResult.query.pages ) {
var title = queryResult.query.pages[ p ].title;
title = title.substring( title.indexOf( ':' ) + 1 );
var titles = [ title ];
titles.exists = true;
if ( queryKey !== title ) titles.normalized = title;
// NFC
return titles;
}
}
return null;
}
},
subcategories: {
uri: '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1',
handler: function ( queryResult ) {
if ( queryResult && queryResult.query && queryResult.query.categorymembers ) {
var titles = queryResult.query.categorymembers;
for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace
return titles;
}
return null;
}
},
parentcategories: {
uri: '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max',
handler: function ( queryResult ) {
if ( queryResult && queryResult.query && queryResult.query.pages ) {
for ( var p in queryResult.query.pages ) {
if ( queryResult.query.pages[ p ].categories ) {
var titles = queryResult.query.pages[ p ].categories;
for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace
return titles;
}
}
}
return null;
}
}
};
var suggestionConfigs = {
searchindex: {
name: 'Search index',
engines: [ 'opensearch' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
pagelist: {
name: 'Page list',
engines: [ 'internalsearch', 'exists' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
combined: {
name: 'Combined search',
engines: [ 'opensearch', 'internalsearch' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
subcat: {
name: 'Subcategories',
engines: [ 'subcategories' ],
cache: {},
show: true,
temp: true,
noCompletion: true
},
parentcat: {
name: 'Parent categories',
engines: [ 'parentcategories' ],
cache: {},
show: true,
temp: true,
noCompletion: true
}
};
CategoryEditor.UNCHANGED = 0;
CategoryEditor.OPEN = 1; // Open, but no input yet
CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
CategoryEditor.CHANGED = 3;
CategoryEditor.DELETED = 4;
// Support: IE6
// IE6 sometimes forgets to redraw the list when editors are opened or closed.
// Adding/removing a dummy element helps, at least when opening editors.
var dummyElement = make( '\xa0', true );
function forceRedraw() {
if ( dummyElement.parentNode ) document.body.removeChild( dummyElement ); else document.body.appendChild( dummyElement );
}
// Event keyCodes that we handle in the text input field/suggestion list.
var BS = 8,
TAB = 9,
RET = 13,
ESC = 27,
SPACE = 32,
PGUP = 33,
PGDOWN = 34,
UP = 38,
DOWN = 40,
DEL = 46,
IME = 229;
CategoryEditor.prototype = {
initialize: function ( line, span, after, key, is_hidden ) {
// If a span is given, 'after' is the category title, otherwise it may be an element after which to
// insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
// known), otherwise it is a boolean indicating whether a bar shall be prepended.
if ( !span ) {
this.isAddCategory = true;
// Create add span and append to catLinks
this.originalCategory = '';
this.originalKey = null;
this.originalExists = false;
if ( !newDOM ) {
span = make( 'span' );
span.className = 'noprint';
if ( key ) {
span.appendChild( make( ' | ', true ) );
if ( after ) {
after.parentNode.insertBefore( span, after.nextSibling );
after = after.nextSibling;
} else if (line) {
line.appendChild( span );
}
} else if ( line && line.firstChild ) {
span.appendChild( make( ' ', true ) );
line.appendChild( span );
}
}
this.linkSpan = make( 'span' );
this.linkSpan.className = 'noprint nopopups hotcatlink';
var lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.open.bind( this );
lk.appendChild( make( HC.links.add, true ) );
lk.title = HC.tooltips.add;
this.linkSpan.appendChild( lk );
span = make( newDOM ? 'li' : 'span' );
span.className = 'noprint';
if ( is_rtl ) span.dir = 'rtl';
span.appendChild( this.linkSpan );
if ( after ) {
after.parentNode.insertBefore( span, after.nextSibling );
} else if ( line ) {
line.appendChild( span );
}
this.normalLinks = null;
this.undelLink = null;
this.catLink = null;
} else {
if ( is_rtl ) span.dir = 'rtl';
this.isAddCategory = false;
this.catLink = span.firstChild;
this.originalCategory = after;
this.originalKey = ( key && key.length > 1 ) ? key.substr( 1 ) : null; // > 1 because it includes the leading bar
this.originalExists = !hasClass( this.catLink, 'new' );
// Create change and del links
this.makeLinkSpan();
if ( !this.originalExists && this.upDownLinks ) this.upDownLinks.style.display = 'none';
span.appendChild( this.linkSpan );
}
this.originalHidden = is_hidden;
this.line = line;
this.engine = HC.suggestions;
this.span = span;
this.currentCategory = this.originalCategory;
this.currentExists = this.originalExists;
this.currentHidden = this.originalHidden;
this.currentKey = this.originalKey;
this.state = CategoryEditor.UNCHANGED;
this.lastSavedState = CategoryEditor.UNCHANGED;
this.lastSavedCategory = this.originalCategory;
this.lastSavedKey = this.originalKey;
this.lastSavedExists = this.originalExists;
this.lastSavedHidden = this.originalHidden;
if ( this.catLink && this.currentKey ) this.catLink.title = this.currentKey;
editors[ editors.length ] = this;
},
makeLinkSpan: function () {
this.normalLinks = make( 'span' );
var lk = null;
if ( this.originalCategory && this.originalCategory.length ) {
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.remove.bind( this );
lk.appendChild( make( HC.links.remove, true ) );
lk.title = HC.tooltips.remove;
this.normalLinks.appendChild( make( ' ', true ) );
this.normalLinks.appendChild( lk );
}
if ( !HC.template_categories[ this.originalCategory ] ) {
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.open.bind( this );
lk.appendChild( make( HC.links.change, true ) );
lk.title = HC.tooltips.change;
this.normalLinks.appendChild( make( ' ', true ) );
this.normalLinks.appendChild( lk );
if ( !noSuggestions && HC.use_up_down ) {
this.upDownLinks = make( 'span' );
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.down.bind( this );
lk.appendChild( make( HC.links.down, true ) );
lk.title = HC.tooltips.down;
this.upDownLinks.appendChild( make( ' ', true ) );
this.upDownLinks.appendChild( lk );
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.up.bind( this );
lk.appendChild( make( HC.links.up, true ) );
lk.title = HC.tooltips.up;
this.upDownLinks.appendChild( make( ' ', true ) );
this.upDownLinks.appendChild( lk );
this.normalLinks.appendChild( this.upDownLinks );
}
}
this.linkSpan = make( 'span' );
this.linkSpan.className = 'noprint nopopups hotcatlink';
this.linkSpan.appendChild( this.normalLinks );
this.undelLink = make( 'span' );
this.undelLink.className = 'nopopups hotcatlink';
this.undelLink.style.display = 'none';
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.restore.bind( this );
lk.appendChild( make( HC.links.restore, true ) );
lk.title = HC.tooltips.restore;
this.undelLink.appendChild( make( ' ', true ) );
this.undelLink.appendChild( lk );
this.linkSpan.appendChild( this.undelLink );
},
invokeSuggestions: function ( dont_autocomplete ) {
if ( this.engine && suggestionConfigs[ this.engine ] && suggestionConfigs[ this.engine ].temp && !dont_autocomplete ) this.engine = HC.suggestions; // Reset to a search upon input
this.state = CategoryEditor.CHANGE_PENDING;
var self = this;
window.setTimeout( function () {
self.textchange( dont_autocomplete );
}, HC.suggest_delay );
},
makeForm: function () {
var form = make( 'form' );
form.method = 'POST';
form.onsubmit = this.accept.bind( this );
this.form = form;
var self = this;
var text = make( 'input' );
text.type = 'text';
text.size = HC.editbox_width;
if ( !noSuggestions ) {
// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
// composition is not canceled, there'll be a textInput event following. During a composition key events are
// either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
// - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
// - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
// first. Gecko doesn't send any keydown while IME is active.
// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
// first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
// detected by a keyDown IME with a keyUp of space, tab, escape, or return.
text.onkeyup = function ( evt ) {
var key = evt.keyCode || 0;
if ( self.ime && self.lastKey === IME && !self.usesComposition && ( key === TAB || key === RET || key === ESC || key === SPACE ) ) self.ime = false;
if ( self.ime ) return true;
if ( key === UP || key === DOWN || key === PGUP || key === PGDOWN ) {
// In case a browser doesn't generate keypress events for arrow keys...
if ( self.keyCount === 0 ) return self.processKey( evt );
} else {
if ( key === ESC && self.lastKey !== IME ) {
if ( !self.resetKeySelection() ) {
// No undo of key selection: treat ESC as "cancel".
self.cancel();
return;
}
}
// Also do this for ESC as a workaround for Firefox bug 524360
// https://bugzilla.mozilla.org/show_bug.cgi?id=524360
self.invokeSuggestions( key === BS || key === DEL || key === ESC );
}
return true;
};
text.onkeydown = function ( evt ) {
var key = evt.keyCode || 0;
self.lastKey = key;
self.keyCount = 0;
// DOM Level < 3 IME input
if ( !self.ime && key === IME && !self.usesComposition ) {
// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
self.ime = true;
} else if ( self.ime && key !== IME && !( key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144 ) ) {
// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
// Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
self.ime = false;
}
if ( self.ime ) return true;
// Handle return explicitly, to override the default form submission to be able to check for ctrl
if ( key === RET ) return self.accept( evt );
// Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
return ( key === ESC ) ? evtKill( evt ) : true;
};
// And handle continued pressing of arrow keys
text.onkeypress = function ( evt ) {
self.keyCount++;
return self.processKey( evt );
};
$( text ).on( 'focus', function () {
makeActive( self );
} );
// On IE, blur events are asynchronous, and may thus arrive after the element has lost the focus. Since IE
// can get the selection only while the element is active (has the focus), we may not always get the selection.
// Therefore, use an IE-specific synchronous event on IE...
// Don't test for text.selectionStart being defined;
$( text ).on(
( text.onbeforedeactivate !== undefined && text.createTextRange ) ? 'beforedeactivate' : 'blur',
this.saveView.bind( this ) );
// DOM Level 3 IME handling
try {
// Setting lastKey = IME provides a fake keyDown for Gecko's single keyUp after a cmposition. If we didn't do this,
// cancelling a composition via ESC would also cancel and close the whole category input editor.
$( text ).on( 'compositionstart', function () {
self.lastKey = IME;
self.usesComposition = true;
self.ime = true;
} );
$( text ).on( 'compositionend', function () {
self.lastKey = IME;
self.usesComposition = true;
self.ime = false;
} );
$( text ).on( 'textInput', function () {
self.ime = false;
self.invokeSuggestions( false );
} );
} catch ( any ) {
// Just in case some browsers might produce exceptions with these DOM Level 3 events
}
$( text ).on( 'blur', function () {
self.usesComposition = false;
self.ime = false;
} );
}
this.text = text;
this.icon = make( 'img' );
var list = null;
if ( !noSuggestions ) {
list = make( 'select' );
list.onclick = function () {
if ( self.highlightSuggestion( 0 ) ) self.textchange( false, true );
};
list.ondblclick = function ( e ) {
if ( self.highlightSuggestion( 0 ) ) self.accept( e );
};
list.onchange = function () {
self.highlightSuggestion( 0 );
self.text.focus();
};
list.onkeyup = function ( evt ) {
if ( evt.keyCode === ESC ) {
self.resetKeySelection();
self.text.focus();
window.setTimeout( function () {
self.textchange( true );
}, HC.suggest_delay );
} else if ( evt.keyCode === RET ) {
self.accept( evt );
}
};
if ( !HC.fixed_search ) {
var engineSelector = make( 'select' );
for ( var key in suggestionConfigs ) {
if ( suggestionConfigs[ key ].show ) {
var opt = make( 'option' );
opt.value = key;
if ( key === this.engine ) opt.selected = true;
opt.appendChild( make( suggestionConfigs[ key ].name, true ) );
engineSelector.appendChild( opt );
}
}
engineSelector.onchange = function () {
self.engine = self.engineSelector.options[ self.engineSelector.selectedIndex ].value;
self.text.focus();
self.textchange( true, true ); // Don't autocomplete, force re-display of list
};
this.engineSelector = engineSelector;
}
}
this.list = list;
function button_label( id, defaultText ) {
var label = null;
if (
onUpload &&
window.UFUI !== undefined &&
window.UIElements !== undefined &&
UFUI.getLabel instanceof Function
) {
try {
label = UFUI.getLabel( id, true );
// Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
while ( label && label.nodeType !== 3 ) label = label.firstChild;
} catch ( ex ) {
label = null;
}
}
if ( !label || !label.data ) return defaultText;
return label.data;
}
// Do not use type 'submit'; we cannot detect modifier keys if we do
var OK = make( 'input' );
OK.type = 'button';
OK.value = button_label( 'wpOkUploadLbl', HC.messages.ok );
OK.onclick = this.accept.bind( this );
this.ok = OK;
var cancel = make( 'input' );
cancel.type = 'button';
cancel.value = button_label( 'wpCancelUploadLbl', HC.messages.cancel );
cancel.onclick = this.cancel.bind( this );
this.cancelButton = cancel;
var span = make( 'span' );
span.className = 'hotcatinput';
span.style.position = 'relative';
span.appendChild( text );
// Support: IE8, IE9
// Put some text into this span (a0 is nbsp) and make sure it always stays on the same
// line as the input field, otherwise, IE8/9 miscalculates the height of the span and
// then the engine selector may overlap the input field.
span.appendChild( make( '\xa0', true ) );
span.style.whiteSpace = 'nowrap';
if ( list ) span.appendChild( list );
if ( this.engineSelector ) span.appendChild( this.engineSelector );
if ( !noSuggestions ) span.appendChild( this.icon );
span.appendChild( OK );
span.appendChild( cancel );
form.appendChild( span );
form.style.display = 'none';
this.span.appendChild( form );
},
display: function ( evt ) {
if ( this.isAddCategory && !onUpload && this.line ) {
// eslint-disable-next-line no-new
new CategoryEditor( this.line, null, this.span, true ); // Create a new one
}
if ( !commitButton && !onUpload ) {
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
setMultiInput();
break;
}
}
}
if ( !this.form ) this.makeForm();
if ( this.list ) this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
this.currentCategory = this.lastSavedCategory;
this.currentExists = this.lastSavedExists;
this.currentHidden = this.lastSavedHidden;
this.currentKey = this.lastSavedKey;
this.icon.src = ( this.currentExists ? HC.existsYes : HC.existsNo );
this.text.value = this.currentCategory + ( this.currentKey !== null ? '|' + this.currentKey : '' );
this.originalState = this.state;
this.lastInput = this.currentCategory;
this.inputExists = this.currentExists;
this.state = this.state === CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
this.lastSelection = {
start: this.currentCategory.length,
end: this.currentCategory.length
};
this.showsList = false;
// Display the form
if ( this.catLink ) this.catLink.style.display = 'none';
this.linkSpan.style.display = 'none';
this.form.style.display = 'inline';
this.ok.disabled = false;
// Kill the event before focussing, otherwise IE will kill the onfocus event!
var result = evtKill( evt );
this.text.focus();
this.text.readOnly = false;
checkMultiInput();
return result;
},
show: function ( evt, engine, readOnly ) {
var result = this.display( evt );
var v = this.lastSavedCategory;
if ( !v.length ) return result;
this.text.readOnly = !!readOnly;
this.engine = engine;
this.textchange( false, true ); // do autocompletion, force display of suggestions
forceRedraw();
return result;
},
open: function ( evt ) {
return this.show( evt, ( this.engine && suggestionConfigs[ this.engine ].temp ) ? HC.suggestions : this.engine );
},
down: function ( evt ) {
return this.show( evt, 'subcat', true );
},
up: function ( evt ) {
return this.show( evt, 'parentcat' );
},
cancel: function () {
if ( this.isAddCategory && !onUpload ) {
this.removeEditor(); // We added a new adder when opening
return;
}
// Close, re-display link
this.inactivate();
this.form.style.display = 'none';
if ( this.catLink ) this.catLink.style.display = '';
this.linkSpan.style.display = '';
this.state = this.originalState;
this.currentCategory = this.lastSavedCategory;
this.currentKey = this.lastSavedKey;
this.currentExists = this.lastSavedExists;
this.currentHidden = this.lastSavedHidden;
if ( this.catLink )
if ( this.currentKey && this.currentKey.length ) { this.catLink.title = this.currentKey; } else { this.catLink.title = ''; }
if ( this.state === CategoryEditor.UNCHANGED ) {
if ( this.catLink ) this.catLink.style.backgroundColor = 'transparent';
} else {
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
}
}
checkMultiInput();
forceRedraw();
},
removeEditor: function () {
if ( !newDOM ) {
var next = this.span.nextSibling;
if ( next ) next.parentNode.removeChild( next );
}
if (this.span && this.span.parentNode) {
this.span.parentNode.removeChild( this.span );
}
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ] === this ) {
editors.splice( i, 1 );
break;
}
}
checkMultiInput();
},
rollback: function ( evt ) {
this.undoLink.parentNode.removeChild( this.undoLink );
this.undoLink = null;
this.currentCategory = this.originalCategory;
this.currentKey = this.originalKey;
this.currentExists = this.originalExists;
this.currentHidden = this.originalHidden;
this.lastSavedCategory = this.originalCategory;
this.lastSavedKey = this.originalKey;
this.lastSavedExists = this.originalExists;
this.lastSavedHidden = this.originalHidden;
this.state = CategoryEditor.UNCHANGED;
if ( !this.currentCategory || !this.currentCategory.length ) {
// It was a newly added category. Remove the whole editor.
this.removeEditor();
} else {
// Redisplay the link...
this.catLink.removeChild( this.catLink.firstChild );
this.catLink.appendChild( make( this.currentCategory, true ) );
this.catLink.href = wikiPagePath( HC.category_canonical + ':' + this.currentCategory );
this.catLink.title = this.currentKey || '';
this.catLink.className = this.currentExists ? '' : 'new';
this.catLink.style.backgroundColor = 'transparent';
if ( this.upDownLinks ) this.upDownLinks.style.display = this.currentExists ? '' : 'none';
checkMultiInput();
}
return evtKill( evt );
},
inactivate: function () {
if ( this.list ) this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
this.is_active = false;
},
acceptCheck: function ( dontCheck ) {
this.sanitizeInput();
var value = this.text.value.split( '|' );
var key = null;
if ( value.length > 1 ) key = value[ 1 ];
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g, '' );
if ( HC.capitalizePageNames ) v = capitalize( v );
this.lastInput = v;
v = replaceShortcuts( v, HC.shortcuts );
if ( !v.length ) {
this.cancel();
return false;
}
if ( !dontCheck && (
conf.wgNamespaceNumber === 14 && v === conf.wgTitle || HC.blacklist && HC.blacklist.test( v ) ) ) {
this.cancel();
return false;
}
this.currentCategory = v;
this.currentKey = key;
this.currentExists = this.inputExists;
return true;
},
accept: function ( evt ) {
// eslint-disable-next-line no-bitwise
this.noCommit = ( evtKeys( evt ) & 1 ) !== 0;
var result = evtKill( evt );
if ( this.acceptCheck() ) {
var toResolve = [ this ];
var original = this.currentCategory;
resolveMulti( toResolve, function ( resolved ) {
if ( resolved[ 0 ].dab ) {
showDab( resolved[ 0 ] );
} else {
if ( resolved[ 0 ].acceptCheck( true ) ) {
resolved[ 0 ].commit(
( resolved[ 0 ].currentCategory !== original ) ?
HC.messages.cat_resolved.replace( /\$1/g, original ) :
null );
}
}
} );
}
return result;
},
close: function () {
if ( !this.catLink ) {
// Create a catLink
this.catLink = make( 'a' );
this.catLink.appendChild( make( 'foo', true ) );
this.catLink.style.display = 'none';
this.span.insertBefore( this.catLink, this.span.firstChild.nextSibling );
}
this.catLink.removeChild( this.catLink.firstChild );
this.catLink.appendChild( make( this.currentCategory, true ) );
this.catLink.href = wikiPagePath( HC.category_canonical + ':' + this.currentCategory );
this.catLink.className = this.currentExists ? '' : 'new';
this.lastSavedCategory = this.currentCategory;
this.lastSavedKey = this.currentKey;
this.lastSavedExists = this.currentExists;
this.lastSavedHidden = this.currentHidden;
// Close form and redisplay category
this.inactivate();
this.form.style.display = 'none';
this.catLink.title = this.currentKey || '';
this.catLink.style.display = '';
if ( this.isAddCategory ) {
if ( onUpload && this.line ) {
// eslint-disable-next-line no-new
new CategoryEditor( this.line, null, this.span, true ); // Create a new one
}
this.isAddCategory = false;
this.linkSpan.parentNode.removeChild( this.linkSpan );
this.makeLinkSpan();
this.span.appendChild( this.linkSpan );
}
if ( !this.undoLink ) {
// Append an undo link.
var span = make( 'span' );
var lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.rollback.bind( this );
lk.appendChild( make( HC.links.undo, true ) );
lk.title = HC.tooltips.undo;
span.appendChild( make( ' ', true ) );
span.appendChild( lk );
this.normalLinks.appendChild( span );
this.undoLink = span;
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
}
}
if ( this.upDownLinks ) this.upDownLinks.style.display = this.lastSavedExists ? '' : 'none';
this.linkSpan.style.display = '';
this.state = CategoryEditor.CHANGED;
checkMultiInput();
forceRedraw();
},
commit: function () {
// Check again to catch problem cases after redirect resolution
if (
(
this.currentCategory === this.originalCategory &&
(
this.currentKey === this.originalKey ||
this.currentKey === null && !this.originalKey.length
)
) ||
conf.wgNamespaceNumber === 14 && this.currentCategory === conf.wgTitle ||
HC.blacklist && HC.blacklist.test( this.currentCategory )
) {
this.cancel();
return;
}
this.close();
if ( !commitButton && !onUpload ) {
var self = this;
initiateEdit( function ( failure ) {
performChanges( failure, self );
}, function ( msg ) {
alert( msg );
} );
}
},
remove: function ( evt ) {
// eslint-disable-next-line no-bitwise
this.doRemove( evtKeys( evt ) & 1 );
return evtKill( evt );
},
doRemove: function ( noCommit ) {
if ( this.isAddCategory ) { // Empty input on adding a new category
this.cancel();
return;
}
if ( !commitButton && !onUpload ) {
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
setMultiInput();
break;
}
}
}
if ( commitButton ) {
this.catLink.title = '';
this.catLink.style.cssText += '; text-decoration : line-through !important;';
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
this.originalState = this.state;
this.state = CategoryEditor.DELETED;
this.normalLinks.style.display = 'none';
this.undelLink.style.display = '';
checkMultiInput();
} else {
if ( onUpload ) {
// Remove this editor completely
this.removeEditor();
} else {
this.originalState = this.state;
this.state = CategoryEditor.DELETED;
this.noCommit = noCommit || HC.del_needs_diff;
var self = this;
initiateEdit(
function ( failure ) {
performChanges( failure, self );
},
function ( msg ) {
self.state = self.originalState;
alert( msg );
} );
}
}
},
restore: function ( evt ) {
// Can occur only if we do have a commit button and are not on the upload form
this.catLink.title = this.currentKey || '';
this.catLink.style.textDecoration = '';
this.state = this.originalState;
if ( this.state === CategoryEditor.UNCHANGED ) {
this.catLink.style.backgroundColor = 'transparent';
} else {
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
}
this.normalLinks.style.display = '';
this.undelLink.style.display = 'none';
checkMultiInput();
return evtKill( evt );
},
// Internal operations
selectEngine: function ( engineName ) {
if ( !this.engineSelector ) return;
for ( var i = 0; i < this.engineSelector.options.length; i++ ) this.engineSelector.options[ i ].selected = this.engineSelector.options[ i ].value === engineName;
},
sanitizeInput: function () {
var v = this.text.value || '';
v = v.replace( /^(\s|_)+/, '' ); // Trim leading blanks and underscores
var re = new RegExp( '^(' + HC.category_regexp + '):' );
if ( re.test( v ) ) v = v.substring( v.indexOf( ':' ) + 1 ).replace( /^(\s|_)+/, '' );
v = v.replace(/\u200E$/, ''); // Trim ending left-to-right mark
if ( HC.capitalizePageNames ) v = capitalize( v );
// Update the input field if needed, manually restoring the cursor afterwards
// (but not the whole selection, so that further typing does not overwrite anything)
if ( this.text.value !== null && this.text.value !== v ) {
var cursorPos = this.text.selectionStart;
this.text.value = v;
this.text.setSelectionRange( cursorPos, cursorPos );
}
},
makeCall: function ( url, callbackObj, engine, queryKey, cleanKey ) {
var cb = callbackObj,
e = engine,
v = queryKey,
z = cleanKey,
thisObj = this;
function done() {
cb.callsMade++;
if ( cb.callsMade === cb.nofCalls ) {
if ( cb.exists ) cb.allTitles.exists = true;
if ( cb.normalized ) cb.allTitles.normalized = cb.normalized;
if ( !cb.dontCache && !suggestionConfigs[ cb.engineName ].cache[ z ] ) suggestionConfigs[ cb.engineName ].cache[ z ] = cb.allTitles;
thisObj.text.readOnly = false;
if ( !cb.cancelled ) thisObj.showSuggestions( cb.allTitles, cb.noCompletion, v, cb.engineName );
if ( cb === thisObj.callbackObj ) thisObj.callbackObj = null;
cb = undefined;
}
}
$.getJSON( url, function ( json ) {
var titles = e.handler( json, z );
if ( titles && titles.length ) {
if ( cb.allTitles === null ) cb.allTitles = titles; else cb.allTitles = cb.allTitles.concat( titles );
if ( titles.exists ) cb.exists = true;
if ( titles.normalized ) cb.normalized = titles.normalized;
}
done();
} ).fail( function ( req ) {
if ( !req ) noSuggestions = true;
cb.dontCache = true;
done();
} );
},
callbackObj: null,
textchange: function ( dont_autocomplete, force ) {
// Hide all other lists
makeActive( this );
// Get input value, omit sort key, if any
this.sanitizeInput();
var v = this.text.value;
// Disregard anything after a pipe.
var pipe = v.indexOf( '|' );
if ( pipe >= 0 ) {
this.currentKey = v.substring( pipe + 1 );
v = v.substring( 0, pipe );
} else {
this.currentKey = null;
}
if ( this.lastInput === v && !force ) return; // No change
if ( this.lastInput !== v ) checkMultiInput();
this.lastInput = v;
this.lastRealInput = v;
// Mark blacklisted inputs.
this.ok.disabled = v.length && HC.blacklist && HC.blacklist.test( v );
if ( noSuggestions ) {
// No Ajax: just make sure the list is hidden
if ( this.list ) this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
if ( this.icon ) this.icon.style.display = 'none';
return;
}
if ( !v.length ) {
this.showSuggestions( [] );
return;
}
var cleanKey = v.replace( /[\u200E\u200F\u202A-\u202E]/g, '' ).replace( wikiTextBlankRE, ' ' );
cleanKey = replaceShortcuts( cleanKey, HC.shortcuts );
cleanKey = cleanKey.replace( /^\s+|\s+$/g, '' );
if ( !cleanKey.length ) {
this.showSuggestions( [] );
return;
}
if ( this.callbackObj ) this.callbackObj.cancelled = true;
var engineName = suggestionConfigs[ this.engine ] ? this.engine : 'combined';
dont_autocomplete = dont_autocomplete || suggestionConfigs[ engineName ].noCompletion;
if ( suggestionConfigs[ engineName ].cache[ cleanKey ] ) {
this.showSuggestions( suggestionConfigs[ engineName ].cache[ cleanKey ], dont_autocomplete, v, engineName );
return;
}
var engines = suggestionConfigs[ engineName ].engines;
this.callbackObj = {
allTitles: null,
callsMade: 0,
nofCalls: engines.length,
noCompletion: dont_autocomplete,
engineName: engineName
};
this.makeCalls( engines, this.callbackObj, v, cleanKey );
},
makeCalls: function ( engines, cb, v, cleanKey ) {
for ( var j = 0; j < engines.length; j++ ) {
var engine = suggestionEngines[ engines[ j ] ];
var url = conf.wgScriptPath + engine.uri.replace( /\$1/g, encodeURIComponent( cleanKey ) );
this.makeCall( url, cb, engine, v, cleanKey );
}
},
showSuggestions: function ( titles, dontAutocomplete, queryKey, engineName ) {
this.text.readOnly = false;
this.dab = null;
this.showsList = false;
if ( !this.list ) return;
if ( noSuggestions ) {
if ( this.list ) this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
if ( this.icon ) this.icon.style.display = 'none';
this.inputExists = true; // Default...
return;
}
this.engineName = engineName;
if ( engineName ) {
if ( !this.engineSelector ) this.engineName = null;
} else {
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
}
if ( queryKey ) {
if ( this.lastInput.indexOf( queryKey ) ) return;
if ( this.lastQuery && this.lastInput.indexOf( this.lastQuery ) === 0 && this.lastQuery.length > queryKey.length ) return;
}
this.lastQuery = queryKey;
// Get current input text
var v = this.text.value.split( '|' );
var key = v.length > 1 ? '|' + v[ 1 ] : '';
v = ( HC.capitalizePageNames ? capitalize( v[ 0 ] ) : v[ 0 ] );
var vNormalized = v;
var knownToExist = titles && titles.exists;
var i;
if ( titles ) {
if ( titles.normalized && v.indexOf( queryKey ) === 0 ) {
// We got back a different normalization than what is in the input field
vNormalized = titles.normalized + v.substring( queryKey.length );
}
var vLow = vNormalized.toLowerCase();
// Strip blacklisted categories
if ( HC.blacklist ) {
for ( i = 0; i < titles.length; i++ ) {
if ( HC.blacklist.test( titles[ i ] ) ) {
titles.splice( i, 1 );
i--;
}
}
}
titles.sort(
function ( a, b ) {
if ( a === b ) return 0;
if ( a.indexOf( b ) === 0 ) return 1;
// a begins with b: a > b
if ( b.indexOf( a ) === 0 ) return -1;
// b begins with a: a < b
// Opensearch may return stuff not beginning with the search prefix!
var prefixMatchA = ( a.indexOf( vNormalized ) === 0 ? 1 : 0 );
var prefixMatchB = ( b.indexOf( vNormalized ) === 0 ? 1 : 0 );
if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;
// Case-insensitive prefix match!
var aLow = a.toLowerCase(),
bLow = b.toLowerCase();
prefixMatchA = ( aLow.indexOf( vLow ) === 0 ? 1 : 0 );
prefixMatchB = ( bLow.indexOf( vLow ) === 0 ? 1 : 0 );
if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;
if ( a < b ) return -1;
if ( b < a ) return 1;
return 0;
} );
// Remove duplicates and self-references
for ( i = 0; i < titles.length; i++ ) {
if (
i + 1 < titles.length && titles[ i ] === titles[ i + 1 ] ||
conf.wgNamespaceNumber === 14 && titles[ i ] === conf.wgTitle
) {
titles.splice( i, 1 );
i--;
}
}
}
if ( !titles || !titles.length ) {
if ( this.list ) this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
if ( this.icon ) this.icon.src = HC.existsNo;
this.inputExists = false;
}
return;
}
var firstTitle = titles[ 0 ];
var completed = this.autoComplete( firstTitle, v, vNormalized, key, dontAutocomplete );
var existing = completed || knownToExist || firstTitle === replaceShortcuts( v, HC.shortcuts );
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
this.icon.src = ( existing ? HC.existsYes : HC.existsNo );
this.inputExists = existing;
}
if ( completed ) {
this.lastInput = firstTitle;
if ( titles.length === 1 ) {
this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
return;
}
}
// (Re-)fill the list
while ( this.list.firstChild ) this.list.removeChild( this.list.firstChild );
for ( i = 0; i < titles.length; i++ ) {
var opt = make( 'option' );
opt.appendChild( make( titles[ i ], true ) );
opt.selected = completed && ( i === 0 );
this.list.appendChild( opt );
}
this.displayList();
},
displayList: function () {
this.showsList = true;
if ( !this.is_active ) {
this.list.style.display = 'none';
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
return;
}
var nofItems = ( this.list.options.length > HC.listSize ? HC.listSize : this.list.options.length );
if ( nofItems <= 1 ) nofItems = 2;
this.list.size = nofItems;
this.list.style.align = is_rtl ? 'right' : 'left';
this.list.style.zIndex = 5;
this.list.style.position = 'absolute';
// Compute initial list position. First the height.
var anchor = is_rtl ? 'right' : 'left';
var listh = 0;
if ( this.list.style.display === 'none' ) {
// Off-screen display to get the height
this.list.style.top = this.text.offsetTop + 'px';
this.list.style[ anchor ] = '-10000px';
this.list.style.display = '';
listh = this.list.offsetHeight;
this.list.style.display = 'none';
} else {
listh = this.list.offsetHeight;
}
// Approximate calculation of maximum list size
var maxListHeight = listh;
if ( nofItems < HC.listSize ) maxListHeight = ( listh / nofItems ) * HC.listSize;
function viewport( what ) {
if ( is_webkit && !document.evaluate ) {
// Safari < 3.0
return window[ 'inner' + what ];
}
var s = 'client' + what;
if ( window.opera ) return document.body[ s ];
return ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
}
function scroll_offset( what ) {
var s = 'scroll' + what;
var result = ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
if ( is_rtl && what === 'Left' ) {
// RTL inconsistencies.
// FF: 0 at the far right, then increasingly negative values.
// IE >= 8: 0 at the far right, then increasingly positive values.
// Webkit: scrollWidth - clientWidth at the far right, then down to zero.
// Opera: don't know...
if ( result < 0 ) result = -result;
if ( !is_webkit ) result = scroll_offset( 'Width' ) - viewport( 'Width' ) - result;
// Now all have webkit behavior, i.e. zero if at the leftmost edge.
}
return result;
}
function position( node ) {
// Stripped-down simplified position function. It's good enough for our purposes.
if ( node.getBoundingClientRect ) {
var box = node.getBoundingClientRect();
return {
x: Math.round( box.left + scroll_offset( 'Left' ) ),
y: Math.round( box.top + scroll_offset( 'Top' ) )
};
}
var t = 0,
l = 0;
do {
t += ( node.offsetTop || 0 );
l += ( node.offsetLeft || 0 );
node = node.offsetParent;
} while ( node );
return {
x: l,
y: t
};
}
var textPos = position( this.text ),
nl = 0,
nt = 0,
offset = 0,
// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
if ( this.engineName ) {
this.engineSelector.style.zIndex = 5;
this.engineSelector.style.position = 'absolute';
this.engineSelector.style.width = textBoxWidth + 'px';
// Figure out the height of this selector: display it off-screen, then hide it again.
if ( this.engineSelector.style.display === 'none' ) {
this.engineSelector.style[ anchor ] = '-10000px';
this.engineSelector.style.top = '0';
this.engineSelector.style.display = '';
offset = this.engineSelector.offsetHeight;
this.engineSelector.style.display = 'none';
} else {
offset = this.engineSelector.offsetHeight;
}
this.engineSelector.style[ anchor ] = nl + 'px';
}
if ( textPos.y < maxListHeight + offset + 1 ) {
// The list might extend beyond the upper border of the page. Let's avoid that by placing it
// below the input text field.
nt = this.text.offsetHeight + offset + 1;
if ( this.engineName ) this.engineSelector.style.top = this.text.offsetHeight + 'px';
} else {
nt = -listh - offset - 1;
if ( this.engineName ) this.engineSelector.style.top = -( offset + 1 ) + 'px';
}
this.list.style.top = nt + 'px';
this.list.style.width = ''; // No fixed width (yet)
this.list.style[ anchor ] = nl + 'px';
if ( this.engineName ) {
this.selectEngine( this.engineName );
this.engineSelector.style.display = '';
}
this.list.style.display = 'block';
// Set the width of the list
if ( this.list.offsetWidth < textBoxWidth ) {
this.list.style.width = textBoxWidth + 'px';
return;
}
// If the list is wider than the textbox: make sure it fits horizontally into the browser window
var scroll = scroll_offset( 'Left' );
var view_w = viewport( 'Width' );
var w = this.list.offsetWidth;
var l_pos = position( this.list );
var left = l_pos.x;
var right = left + w;
if ( left < scroll || right > scroll + view_w ) {
if ( w > view_w ) {
w = view_w;
this.list.style.width = w + 'px';
if ( is_rtl ) left = right - w; else right = left + w;
}
var relative_offset = 0;
if ( left < scroll ) relative_offset = scroll - left; else if ( right > scroll + view_w ) relative_offset = -( right - scroll - view_w );
if ( is_rtl ) relative_offset = -relative_offset;
if ( relative_offset ) this.list.style[ anchor ] = ( nl + relative_offset ) + 'px';
}
},
autoComplete: function ( newVal, actVal, normalizedActVal, key, dontModify ) {
if ( newVal === actVal ) return true;
if ( dontModify || this.ime || !this.canSelect() ) return false;
// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
if ( newVal.indexOf( actVal ) ) {
// Maybe it'll work with the normalized value (NFC)?
if ( normalizedActVal && newVal.indexOf( normalizedActVal ) === 0 ) {
if ( this.lastRealInput === actVal ) this.lastRealInput = normalizedActVal;
actVal = normalizedActVal;
} else {
return false;
}
}
// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
// such that it can be easily removed by typing backspace if the suggestion is unwanted.
this.text.focus();
this.text.value = newVal + key;
this.setSelection( actVal.length, newVal.length );
return true;
},
canSelect: function () {
return this.text.setSelectionRange ||
this.text.createTextRange ||
this.text.selectionStart !== undefined &&
this.text.selectionEnd !== undefined;
},
setSelection: function ( from, to ) {
// this.text must be focused (at least on IE)
if ( !this.text.value ) return;
if ( this.text.setSelectionRange ) { // e.g. khtml
this.text.setSelectionRange( from, to );
} else if ( this.text.selectionStart !== undefined ) {
if ( from > this.text.selectionStart ) {
this.text.selectionEnd = to;
this.text.selectionStart = from;
} else {
this.text.selectionStart = from;
this.text.selectionEnd = to;
}
} else if ( this.text.createTextRange ) { // IE
var new_selection = this.text.createTextRange();
new_selection.move( 'character', from );
new_selection.moveEnd( 'character', to - from );
new_selection.select();
}
},
getSelection: function () {
var from = 0,
to = 0;
// this.text must be focused (at least on IE)
if ( !this.text.value ) {
// No text.
} else if ( this.text.selectionStart !== undefined ) {
from = this.text.selectionStart;
to = this.text.selectionEnd;
} else if ( document.selection && document.selection.createRange ) { // IE
var rng = document.selection.createRange().duplicate();
if ( rng.parentElement() === this.text ) {
try {
var textRng = this.text.createTextRange();
textRng.move( 'character', 0 );
textRng.setEndPoint( 'EndToEnd', rng );
// We're in a single-line input box: no need to care about IE's strange
// handling of line ends
to = textRng.text.length;
textRng.setEndPoint( 'EndToStart', rng );
from = textRng.text.length;
} catch ( notFocused ) {
from = this.text.value.length;
to = from; // At end of text
}
}
}
return {
start: from,
end: to
};
},
saveView: function () {
this.lastSelection = this.getSelection();
},
processKey: function ( evt ) {
var dir = 0;
switch ( this.lastKey ) {
case UP:
dir = -1;
break;
case DOWN:
dir = 1;
break;
case PGUP:
dir = -HC.listSize;
break;
case PGDOWN:
dir = HC.listSize;
break;
case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
return evtKill( evt );
}
if ( dir ) {
if ( this.list.style.display !== 'none' ) {
// List is visible, so there are suggestions
this.highlightSuggestion( dir );
// Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
// as "place the text cursor at the front", which we don't want here.
return evtKill( evt );
} else if (
this.keyCount <= 1 &&
( !this.callbackObj || this.callbackObj.callsMade === this.callbackObj.nofCalls )
) {
// If no suggestions displayed, get them, unless we're already getting them.
this.textchange();
}
}
return true;
},
highlightSuggestion: function ( dir ) {
if ( noSuggestions || !this.list || this.list.style.display === 'none' ) return false;
var curr = this.list.selectedIndex;
var tgt = -1;
if ( dir === 0 ) {
if ( curr < 0 || curr >= this.list.options.length ) return false;
tgt = curr;
} else {
tgt = curr < 0 ? 0 : curr + dir;
tgt = tgt < 0 ? 0 : tgt;
if ( tgt >= this.list.options.length ) tgt = this.list.options.length - 1;
}
if ( tgt !== curr || dir === 0 ) {
if ( curr >= 0 && curr < this.list.options.length && dir !== 0 ) this.list.options[ curr ].selected = false;
this.list.options[ tgt ].selected = true;
// Get current input text
var v = this.text.value.split( '|' );
var key = v.length > 1 ? '|' + v[ 1 ] : '';
var completed = this.autoComplete( this.list.options[ tgt ].text, this.lastRealInput, null, key, false );
if ( !completed || this.list.options[ tgt ].text === this.lastRealInput ) {
this.text.value = this.list.options[ tgt ].text + key;
if ( this.canSelect() ) this.setSelection( this.list.options[ tgt ].text.length, this.list.options[ tgt ].text.length );
}
this.lastInput = this.list.options[ tgt ].text;
this.inputExists = true; // Might be wrong if from a dab list...
if ( this.icon ) this.icon.src = HC.existsYes;
this.state = CategoryEditor.CHANGE_PENDING;
}
return true;
},
resetKeySelection: function () {
if ( noSuggestions || !this.list || this.list.style.display === 'none' ) return false;
var curr = this.list.selectedIndex;
if ( curr >= 0 && curr < this.list.options.length ) {
this.list.options[ curr ].selected = false;
// Get current input text
var v = this.text.value.split( '|' );
var key = v.length > 1 ? '|' + v[ 1 ] : '';
// ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
// our event handlers ever get a chance to run.
var result = v[ 0 ] !== this.lastInput;
if ( v[ 0 ] !== this.lastRealInput ) {
this.text.value = this.lastRealInput + key;
result = true;
}
this.lastInput = this.lastRealInput;
return result;
}
return false;
}
}; // end CategoryEditor.prototype
function initialize() {
// User configurations: Do this here, called from the onload handler, so that users can
// override it easily in their own user script files by just declaring variables. JSconfig
// is some feature used at Wikimedia Commons.
var config = ( window.JSconfig !== undefined && JSconfig.keys ) ? JSconfig.keys : {};
HC.dont_add_to_watchlist = ( window.hotcat_dont_add_to_watchlist !== undefined ?
!!window.hotcat_dont_add_to_watchlist :
( config.HotCatDontAddToWatchlist !== undefined ? config.HotCatDontAddToWatchlist :
HC.dont_add_to_watchlist ) );
HC.no_autocommit = ( window.hotcat_no_autocommit !== undefined ?
!!window.hotcat_no_autocommit : ( config.HotCatNoAutoCommit !== undefined ?
config.HotCatNoAutoCommit :
// On talk namespace default autocommit off
( conf.wgNamespaceNumber % 2 ?
true : HC.no_autocommit ) ) );
HC.del_needs_diff = ( window.hotcat_del_needs_diff !== undefined ?
!!window.hotcat_del_needs_diff :
( config.HotCatDelNeedsDiff !== undefined ?
config.HotCatDelNeedsDiff :
HC.del_needs_diff ) );
HC.suggest_delay = window.hotcat_suggestion_delay || config.HotCatSuggestionDelay || HC.suggest_delay;
HC.editbox_width = window.hotcat_editbox_width || config.HotCatEditBoxWidth || HC.editbox_width;
HC.suggestions = window.hotcat_suggestions || config.HotCatSuggestions || HC.suggestions;
if ( typeof HC.suggestions !== 'string' || !suggestionConfigs[ HC.suggestions ] ) HC.suggestions = 'combined';
HC.fixed_search = ( window.hotcat_suggestions_fixed !== undefined ?
!!window.hotcat_suggestions_fixed : ( config.HotCatFixedSuggestions !== undefined ?
config.HotCatFixedSuggestions : HC.fixed_search ) );
HC.single_minor = ( window.hotcat_single_changes_are_minor !== undefined ?
!!window.hotcat_single_changes_are_minor :
( config.HotCatMinorSingleChanges !== undefined ?
config.HotCatMinorSingleChanges :
HC.single_minor ) );
HC.bg_changed = window.hotcat_changed_background || config.HotCatChangedBackground || HC.bg_changed;
HC.use_up_down = ( window.hotcat_use_category_links !== undefined ?
!!window.hotcat_use_category_links :
( config.HotCatUseCategoryLinks !== undefined ?
config.HotCatUseCategoryLinks :
HC.use_up_down ) );
HC.listSize = window.hotcat_list_size || config.HotCatListSize || HC.listSize;
if ( conf.wgDBname !== 'commonswiki' ) HC.changeTag = config.HotCatChangeTag || '';
// The next whole shebang is needed, because manual tags get not submitted except of save
if ( HC.changeTag ) {
var eForm = document.editform,
catRegExp = new RegExp( '^\\[\\[(' + HC.category_regexp + '):' ),
oldTxt;
// Returns true if minor change
var isMinorChange = function () {
var newTxt = eForm.wpTextbox1;
if ( !newTxt ) return;
newTxt = newTxt.value;
var oldLines = oldTxt.match( /^.*$/gm ),
newLines = newTxt.match( /^.*$/gm ),
cArr; // changes
var except = function ( aArr, bArr ) {
var result = [],
lArr, // larger
sArr; // smaller
if ( aArr.length < bArr.length ) {
lArr = bArr;
sArr = aArr;
} else {
lArr = aArr;
sArr = bArr;
}
for ( var i = 0; i < lArr.length; i++ ) {
var item = lArr[ i ];
var ind = $.inArray( item, sArr );
if ( ind === -1 ) result.push( item );
else sArr.splice( ind, 1 ); // don't check this item again
}
return result.concat( sArr );
};
cArr = except( oldLines, newLines );
if ( cArr.length ) {
cArr = $.grep( cArr, function ( c ) {
c = $.trim( c );
return ( c && !catRegExp.test( c ) );
} );
}
if ( !cArr.length ) {
oldTxt = newTxt;
return true;
}
};
if ( conf.wgAction === 'submit' && conf.wgArticleId && eForm && eForm.wpSummary && document.getElementById( 'wikiDiff' ) ) {
var sum = eForm.wpSummary,
sumA = eForm.wpAutoSummary;
if ( sum.value && sumA.value === HC.changeTag ) { // HotCat diff
// MD5 hash of the empty string, as HotCat edit is based on empty sum
sumA.value = sumA.value.replace( HC.changeTag, 'd41d8cd98f00b204e9800998ecf8427e' );
// Attr creation and event handling is not same in all (old) browsers so use $
var $ct = $( '<input type="hidden" name="wpChangeTags">' ).val( HC.changeTag );
$( eForm ).append( $ct );
oldTxt = eForm.wpTextbox1.value;
$( '#wpSave' ).one( 'click', function () {
if ( $ct.val() )
sum.value = sum.value.replace( ( HC.messages.using || HC.messages.prefix ), '' );
} );
var removeChangeTag = function () {
$( eForm.wpTextbox1 ).add( sum ).one( 'input', function () {
window.setTimeout( function () {
if ( !isMinorChange() ) $ct.val( '' );
else removeChangeTag();
}, 500 );
} );
};
removeChangeTag();
}
}
}
// Numeric input, make sure we have a numeric value
HC.listSize = parseInt( HC.listSize, 10 );
if ( isNaN( HC.listSize ) || HC.listSize < 5 ) HC.listSize = 5;
HC.listSize = Math.min( HC.listSize, 30 ); // Max size
// Localize search engine names
if ( HC.engine_names ) {
for ( var key in HC.engine_names )
if ( suggestionConfigs[ key ] && HC.engine_names[ key ] ) suggestionConfigs[ key ].name = HC.engine_names[ key ];
}
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
is_rtl = hasClass( document.body, 'rtl' );
if ( !is_rtl ) {
if ( document.defaultView && document.defaultView.getComputedStyle ) { // Gecko etc.
is_rtl = document.defaultView.getComputedStyle( document.body, null ).getPropertyValue( 'direction' );
} else if ( document.body.currentStyle ) { // IE, has subtle differences to getComputedStyle
is_rtl = document.body.currentStyle.direction;
} else { // Not exactly right, but best effort
is_rtl = document.body.style.direction;
}
is_rtl = ( is_rtl === 'rtl' );
}
}
function can_edit() {
var container = null;
switch ( mw.config.get( 'skin' ) ) {
case 'cologneblue':
container = document.getElementById( 'quickbar' );
/* fall through */
case 'standard':
case 'nostalgia':
if ( !container ) container = document.getElementById( 'topbar' );
var lks = container.getElementsByTagName( 'a' );
for ( var i = 0; i < lks.length; i++ ) {
if (
param( 'title', lks[ i ].href ) === conf.wgPageName &&
param( 'action', lks[ i ].href ) === 'edit'
) {
return true;
}
}
return false;
default:
// all modern skins:
return document.getElementById( 'ca-edit' ) !== null;
}
}
// Legacy stuff
function closeForm() {
// Close all open editors without redirect resolution and other asynchronous stuff.
for ( var i = 0; i < editors.length; i++ ) {
var edit = editors[ i ];
if ( edit.state === CategoryEditor.OPEN ) {
edit.cancel();
} else if ( edit.state === CategoryEditor.CHANGE_PENDING ) {
edit.sanitizeInput();
var value = edit.text.value.split( '|' );
var key = null;
if ( value.length > 1 ) key = value[ 1 ];
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g, '' );
if ( !v.length ) {
edit.cancel();
} else {
edit.currentCategory = v;
edit.currentKey = key;
edit.currentExists = this.inputExists;
edit.close();
}
}
}
}
function setup_upload() {
onUpload = true;
// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
var ip = document.getElementById( 'mw-htmlform-description' ) || document.getElementById( 'wpDestFile' );
if ( !ip ) {
ip = document.getElementById( 'wpDestFile' );
while ( ip && ip.nodeName.toLowerCase() !== 'table' ) ip = ip.parentNode;
}
if ( !ip ) return;
var reupload = document.getElementById( 'wpForReUpload' );
var destFile = document.getElementById( 'wpDestFile' );
if (
( reupload && !!reupload.value ) ||
( destFile && ( destFile.disabled || destFile.readOnly ) )
) {
return; // re-upload form...
}
// Insert a table row with two fields (label and empty category bar)
var labelCell = make( 'td' );
var lineCell = make( 'td' );
// Create the category line
catLine = make( 'div' );
catLine.className = 'catlinks';
catLine.id = 'catlinks';
catLine.style.textAlign = is_rtl ? 'right' : 'left';
// We'll be inside a table row. Make sure that we don't have margins or strange borders.
catLine.style.margin = '0';
catLine.style.border = 'none';
lineCell.appendChild( catLine );
// Create the label
var label = null;
if ( window.UFUI && window.UIElements && UFUI.getLabel instanceof Function ) {
try {
label = UFUI.getLabel( 'wpCategoriesUploadLbl' );
} catch ( ex ) {
label = null;
}
}
if ( !label ) {
labelCell.id = 'hotcatLabel';
labelCell.appendChild( make( HC.categories, true ) );
} else {
labelCell.id = 'hotcatLabelTranslated';
labelCell.appendChild( label );
}
labelCell.className = 'mw-label';
labelCell.style.textAlign = 'right';
labelCell.style.verticalAlign = 'middle';
// Change the onsubmit handler
var form = document.getElementById( 'upload' ) || document.getElementById( 'mw-upload-form' );
if ( form && ip && ip.insertRow ) {
var newRow = ip.insertRow( -1 );
newRow.appendChild( labelCell );
newRow.appendChild( lineCell );
form.onsubmit = ( function ( oldSubmit ) {
return function () {
var do_submit = true;
if ( oldSubmit ) {
if ( typeof oldSubmit === 'string' ) {
// eslint-disable-next-line no-eval
do_submit = eval( oldSubmit );
} else if ( oldSubmit instanceof Function ) {
do_submit = oldSubmit.apply( form, arguments );
}
}
if ( !do_submit ) return false;
closeForm();
// Copy the categories
var eb = document.getElementById( 'wpUploadDescription' ) || document.getElementById( 'wpDesc' );
var addedOne = false;
for ( var i = 0; i < editors.length; i++ ) {
var t = editors[ i ].currentCategory;
if ( !t ) continue;
var key = editors[ i ].currentKey;
var new_cat = '[[' + HC.category_canonical + ':' + t + ( key ? '|' + key : '' ) + ']]';
// Only add if not already present
var cleanedText = eb.value
.replace( /<!--(\s|\S)*?-->/g, '' )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, '' );
if ( !find_category( cleanedText, t, true ) ) {
eb.value += '\n' + new_cat;
addedOne = true;
}
}
if ( addedOne ) {
// Remove "subst:unc" added by Flinfo if it didn't find categories
eb.value = eb.value.replace( /\{\{subst:unc\}\}/g, '' );
}
return true;
};
}( form.onsubmit ) );
}
}
var cleanedText = null;
function isOnPage( span ) {
if ( span.firstChild.nodeType !== Node.ELEMENT_NODE ) return null;
var catTitle = title( span.firstChild.href );
if ( !catTitle ) return null;
catTitle = catTitle.substr( catTitle.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
if ( HC.blacklist && HC.blacklist.test( catTitle ) ) return null;
var result = {
title: catTitle,
match: [ '', '', '' ]
};
if ( pageText === null ) return result;
if ( cleanedText === null ) {
cleanedText = pageText
.replace( /<!--(\s|\S)*?-->/g, '' )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, '' );
}
result.match = find_category( cleanedText, catTitle, true );
return result;
}
var initialized = false;
var setupTimeout = null;
function findByClass( scope, tag, className ) {
var result = $( scope ).find( tag + '.' + className );
return ( result && result.length ) ? result[ 0 ] : null;
}
function errorAMC() {
alert( 'An error occurred. Unable to setup HotCat' );
}
function enableAMC() {
var api = new mw.Api();
return api.saveOption( 'mf_amc_optin', '1' ).then( function ( r ) {
if ( !r || r.options !== 'success' ) {
errorAMC();
return;
}
if ( window.confirm( 'Please reload your page to use hotcat.' ) ) {
location.reload();
}
}, function () {
errorAMC();
} );
}
function showWarning( text ) {
var warning = document.createElement( 'div' );
warning.setAttribute( 'style', 'padding: 20px; background: orange; color: #333; font-weight: bold; margin-top: 20px;' );
warning.textContent = text;
var btn = document.createElement( 'button' );
btn.classList.add( 'mw-ui-button', 'cdx-button' );
btn.style.display = 'block';
btn.textContent = 'Enable HotCat and AMC mode on this page';
btn.addEventListener( 'click', function () {
enableAMC();
} );
warning.appendChild( btn );
document.getElementById( 'mw-content-text' ).appendChild( warning );
}
function setup( additionalWork ) {
if ( initialized ) return;
initialized = true;
if ( setupTimeout ) {
window.clearTimeout( setupTimeout );
setupTimeout = null;
}
// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
// each category, and add the + link.
catLine =
// Special:Upload
catLine ||
document.getElementById( 'mw-normal-catlinks' );
var hiddenCats = document.getElementById( 'mw-hidden-catlinks' );
if ( !catLine ) {
// Workaround for T24660
if ( mw.config.get('skin') === 'minerva' ) {
if ( document.body.classList.contains('mw-mf-amc-disabled') ) {
showWarning( 'HotCat requires AMC mode.' );
}
}
var footer = null;
if ( !hiddenCats ) {
footer = findByClass( document, 'div', 'printfooter' );
if ( !footer ) return; // Don't know where to insert the category line
}
catLine = make( 'div' );
catLine.id = 'mw-normal-catlinks';
catLine.style.textAlign = is_rtl ? 'right' : 'left';
// Add a label
var label = make( 'a' );
label.href = conf.wgArticlePath.replace( '$1', 'Special:Categories' );
label.title = HC.categories;
label.appendChild( make( HC.categories, true ) );
catLine.appendChild( label );
catLine.appendChild( make( ':', true ) );
// Insert the new category line
var container = ( hiddenCats ? hiddenCats.parentNode : document.getElementById( 'catlinks' ) );
if ( !container ) {
container = make( 'div' );
container.id = 'catlinks';
footer.parentNode.insertBefore( container, footer.nextSibling );
}
container.className = 'catlinks noprint';
container.style.display = '';
if ( !hiddenCats ) container.appendChild( catLine ); else container.insertBefore( catLine, hiddenCats );
} // end if catLine exists
if ( is_rtl ) catLine.dir = 'rtl';
// Create editors for all existing categories
function createEditors( line, is_hidden ) {
var i;
var cats = line.getElementsByTagName( 'li' );
if ( cats.length ) {
newDOM = true;
line = cats[ 0 ].parentNode;
} else {
cats = line.getElementsByTagName( 'span' );
}
// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
var copyCats = new Array( cats.length );
for ( i = 0; i < cats.length; i++ ) copyCats[ i ] = cats[ i ];
for ( i = 0; i < copyCats.length; i++ ) {
var test = isOnPage( copyCats[ i ] );
if ( test !== null && test.match !== null && line ) {
// eslint-disable-next-line no-new
new CategoryEditor( line, copyCats[ i ], test.title, test.match[ 2 ], is_hidden );
}
}
return copyCats.length ? copyCats[ copyCats.length - 1 ] : null;
}
var lastSpan = createEditors( catLine, false );
// Create one to add a new category
// eslint-disable-next-line no-new
new CategoryEditor( newDOM ? catLine.getElementsByTagName( 'ul' )[ 0 ] : catLine, null, null, lastSpan !== null, false );
if ( !onUpload ) {
if ( pageText !== null && hiddenCats ) {
if ( is_rtl ) hiddenCats.dir = 'rtl';
createEditors( hiddenCats, true );
}
// And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
var enableMulti = make( 'span' );
enableMulti.className = 'noprint';
if ( is_rtl ) enableMulti.dir = 'rtl';
catLine.insertBefore( enableMulti, catLine.firstChild.nextSibling );
enableMulti.appendChild( make( '\xa0', true ) ); // nbsp
multiSpan = make( 'span' );
enableMulti.appendChild( multiSpan );
multiSpan.innerHTML = '(<a>' + HC.addmulti + '</a>)';
var lk = multiSpan.getElementsByTagName( 'a' )[ 0 ];
lk.onclick = function ( evt ) {
setMultiInput();
checkMultiInput();
return evtKill( evt );
};
lk.title = HC.multi_tooltip;
lk.style.cursor = 'pointer';
}
cleanedText = null;
if ( additionalWork instanceof Function ) additionalWork();
mw.hook( 'hotcat.ready' ).fire(); // Execute registered callback functions
$( 'body' ).trigger( 'hotcatSetupCompleted' );
}
function createCommitForm() {
if ( commitForm ) return;
var format = '';
if ( HC.jsonContentModels.includes( conf.wgPageContentModel ) ) {
// Handle Data namespace JSON content
format = 'application/json';
}
else {
// For backwards-compatibility the default handler is wikitext
format = 'text/x-wiki';
}
var formContainer = make( 'div' );
formContainer.style.display = 'none';
document.body.appendChild( formContainer );
formContainer.innerHTML =
'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="' +
conf.wgScript + '?title=' + encodeURIComponent( conf.wgPageName ) + '&action=submit">' +
'<input type="hidden" name="wpTextbox1">' +
'<input type="hidden" name="model" value="' + conf.wgPageContentModel + '">' +
'<input type="hidden" name="format" value="' + format + '">' +
'<input type="hidden" name="wpSummary" value="">' +
'<input type="checkbox" name="wpMinoredit" value="1">' +
'<input type="checkbox" name="wpWatchthis" value="1">' +
'<input type="hidden" name="wpWatchlistExpiry" value="">' +
'<input type="hidden" name="wpAutoSummary" value="d41d8cd98f00b204e9800998ecf8427e">' +
'<input type="hidden" name="wpEdittime">' +
'<input type="hidden" name="wpStarttime">' +
'<input type="hidden" name="wpDiff" value="wpDiff">' +
'<input type="hidden" name="oldid" value="0">' +
'<input type="hidden" name="wpIgnoreBlankSummary" value="1">' +
'<input type="submit" name="hcCommit" value="hcCommit">' +
'<input type="hidden" name="wpEditToken">' +
'<input type="hidden" name="wpUltimateParam" value="1">' +
'<input type="hidden" name="wpChangeTags">' +
'<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck">' +
'</form>';
commitForm = document.getElementById( 'hotcatCommitForm' );
}
function getPage() {
// We know we have an article here.
if ( !conf.wgArticleId ) {
// Doesn't exist yet. Disable on non-existing User pages -- might be a global user page.
if ( conf.wgNamespaceNumber === 2 ) return;
pageText = '';
pageTime = null;
setup( createCommitForm );
} else {
var url = conf.wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&rawcontinue=&titles=' +
encodeURIComponent( conf.wgPageName ) +
'&prop=info%7Crevisions&rvprop=content%7Ctimestamp%7Cids&meta=siteinfo&rvslots=main&rvlimit=1&rvstartid=' +
conf.wgCurRevisionId;
var s = make( 'script' );
s.src = url;
HC.start = function ( json ) {
setPage( json );
setup( createCommitForm );
};
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
setupTimeout = window.setTimeout( function () {
setup( createCommitForm );
}, 4000 ); // 4 sec, just in case getting the wikitext takes longer.
}
}
function setState( state ) {
var cats = state.split( '\n' );
if ( !cats.length ) return null;
if ( initialized && editors.length === 1 && editors[ 0 ].isAddCategory ) {
// Insert new spans and create new editors for them.
var newSpans = [];
var before = editors.length === 1 ? editors[ 0 ].span : null;
var i;
for ( i = 0; i < cats.length; i++ ) {
if ( !cats[ i ].length ) continue;
var cat = cats[ i ].split( '|' );
var key = cat.length > 1 ? cat[ 1 ] : null;
cat = cat[ 0 ];
var lk = make( 'a' );
lk.href = wikiPagePath( HC.category_canonical + ':' + cat );
lk.appendChild( make( cat, true ) );
lk.title = cat;
var span = make( 'span' );
span.appendChild( lk );
if ( !i ) catLine.insertBefore( make( ' ', true ), before );
catLine.insertBefore( span, before );
if ( before && i + 1 < cats.length ) parent.insertBefore( make( ' | ', true ), before );
newSpans.push( {
element: span,
title: cat,
key: key
} );
}
// And change the last one...
if ( before ) before.parentNode.insertBefore( make( ' | ', true ), before );
for ( i = 0; i < newSpans.length; i++ ) {
// eslint-disable-next-line no-new
new CategoryEditor( catLine, newSpans[ i ].element, newSpans[ i ].title, newSpans[ i ].key );
}
}
return null;
}
function getState() {
if ( HC.jsonContentModels.includes( conf.wgPageContentModel ) ) {
// Handle Data namespace JSON content
return getStateJson();
}
else {
// For backwards-compatibility the default handler is wikitext
return getStateWikitext();
}
}
function getStateJson() {
// Handle Data namespace JSON content
try {
var jsonData = JSON.parse( pageText );
if ( jsonData.mediawikiCategories ) {
return jsonData.mediawikiCategories.map( function( cat ) {
return cat.name + ( cat.sort ? '|' + cat.sort : '' );
} ).join( '\n' );
}
} catch ( e ) {
console.error( '[getStateJson]Error parsing JSON:', e );
}
return null;
}
function getStateWikitext() {
var result = null;
for ( var i = 0; i < editors.length; i++ ) {
var text = editors[ i ].currentCategory;
var key = editors[ i ].currentKey;
if ( text && text.length ) {
if ( key !== null ) text += '|' + key;
if ( result === null ) result = text; else result += '\n' + text;
}
}
return result;
}
function really_run() {
initialize();
if ( !HC.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName === 'Upload' && conf.wgUserName ) {
setup_upload();
setup( function () {
// Check for state restoration once the setup is done otherwise, but before signalling setup completion
if ( window.UploadForm && UploadForm.previous_hotcat_state ) UploadForm.previous_hotcat_state = setState( UploadForm.previous_hotcat_state );
} );
} else {
if ( !conf.wgIsArticle || conf.wgAction !== 'view' || param( 'diff' ) !== null || param( 'oldid' ) !== null || !can_edit() || HC.disable() ) return;
if (!HC.isSupportedContentModel()) {
console.info("Unknown contentModel: " + conf.wgPageContentModel + " in " + conf.wgPageName +". If you see this please report it to https://commons.wikimedia.org/wiki/MediaWiki_talk:Gadget-Cat-a-lot.js");
}
getPage();
}
}
function run() {
if ( HC.started ) return;
HC.started = true;
loadTrigger.register( really_run );
}
// Export legacy functions
window.hotcat_get_state = function () {
return getState();
};
window.hotcat_set_state = function ( state ) {
return setState( state );
};
window.hotcat_close_form = function () {
closeForm();
};
HC.runWhenReady = function ( callback ) {
// run user-registered code once HotCat is fully set up and ready.
mw.hook( 'hotcat.ready' ).add( callback );
};
// Make sure we don't get conflicts with AjaxCategories (core development that should one day
// replace HotCat).
mw.config.set( 'disableAJAXCategories', true );
// Run as soon as possible. This varies depending on MediaWiki version;
// window's 'load' event is always safe, but usually we can do better than that.
if ( conf.wgCanonicalSpecialPageName !== 'Upload' ) {
// Reload HotCat after (VE) edits (bug T103285)
mw.hook( 'postEdit' ).add( function () {
// Reset HotCat in case this is a soft reload (e.g. VisualEditor edit), unless the categories
// were not re-rendered and our interface is still there (e.g. DiscussionTools edit)
if ( document.querySelector( '#catlinks .hotcatlink' ) ) {
return;
}
catLine = null;
editors = [];
initialized = false;
HC.started = false;
run();
} );
}
// We can safely trigger just after user configuration is loaded.
// Use always() instead of then() to also start HotCat if the user module has problems.
$.when( mw.loader.using( 'user' ), $.ready ).always( run );
}( jQuery, mediaWiki ) );
// </nowiki>
b4wg4v89dm04bfnyveiqg9yuwazpelq
MediaWiki:Gadget-charinsert.js
8
29057
326080
2026-04-04T08:52:57Z
Kannotlogin
29153
nieuw blad: /** * charinsert loader */ if ( /^(edit|submit)$/.test( mw.config.get( 'wgAction' ) ) || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Upload' ) { mw.loader.load( 'ext.gadget.charinsert-core' ); }
326080
javascript
text/javascript
/**
* charinsert loader
*/
if ( /^(edit|submit)$/.test( mw.config.get( 'wgAction' ) ) || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Upload' ) {
mw.loader.load( 'ext.gadget.charinsert-core' );
}
kt0voujo93kc03xdrn4wl6rtrpu6miz
MediaWiki:Gadget-charinsert-core.js
8
29058
326081
2026-04-04T08:53:15Z
Kannotlogin
29153
nieuw blad: /** * <https://en.wikipedia.org/wiki/MediaWiki:Gadget-charinsert-core.js> * * Originally based on [[mw:User:Alex Smotrov/edittools.js]], modified for use on the English Wikipedia. * * Configuration (to be set from [[Special:MyPage/common.js]]): * window.charinsertCustom – Object. Merged into the default charinsert list. For example, setting * this to { Symbols: '‽' } will add the interrobang to the end of the Symbols section. * window.editToolsRecall – Bo…
326081
javascript
text/javascript
/**
* <https://en.wikipedia.org/wiki/MediaWiki:Gadget-charinsert-core.js>
*
* Originally based on [[mw:User:Alex Smotrov/edittools.js]], modified for use on the English Wikipedia.
*
* Configuration (to be set from [[Special:MyPage/common.js]]):
* window.charinsertCustom – Object. Merged into the default charinsert list. For example, setting
* this to { Symbols: '‽' } will add the interrobang to the end of the Symbols section.
* window.editToolsRecall – Boolean. Set true to create a recall switch.
* window.charinsertDontMove – Boolean. Set true to leave the box in its default position, rather
* than moving it above the edit summary.
* window.updateEditTools() – Function. Call after updating window.charinsertCustom to regenerate the
* EditTools window.
*/
/* global $, mw, charinsertCustom */
window.updateEditTools = function () {
};
$( function () {
var $currentFocused,
editTools;
function getSelectedSection() {
var selectedSection = mw.storage.get( editTools.storageKey )
|| mw.storage.session.get( editTools.storageKey );
return selectedSection;
}
function saveSelectedSection( newIndex ) {
mw.storage.set( editTools.storageKey, newIndex )
|| mw.storage.session.set( editTools.storageKey, newIndex );
}
editTools = {
// Entries prefixed with ␥ (U+2425 SYMBOL FOR DELETE FORM TWO) will not appear in the article namespace (namespace 0).
// Please make any changes to [[MediaWiki:Edittools]] as well, however, instead of using the ␥ symbol, use {{#ifeq:{{NAMESPACE}}|{{ns:0}}| | }}.
charinsert: {
'Insert': ' – — ° ′ ″ ≈ ≠ ≤ ≥ ± − × ÷ ← → · § ␥Sign_your_posts_on_talk_pages: ␥~~\~~ Cite_your_sources: <ref>+</ref>',
'Wiki markup': 'Insert: – — ° ′ ″ ≈ ≠ ≤ ≥ ± − × ÷ ← → · § ␥~~\~~ <ref>+</ref> Wiki_markup: {\{+}} {\{\{+}}} | [+] [\[+]] [\[Category:+]] #REDIRECT.[\[+]] <s>+</s> <sup>+</sup> <sub>+</sub> <code>+</code> <pre>+</pre> <blockquote>+</blockquote> <ref.name="+"_/> {\{#tag:ref|+|group="nb"|name=""}} {\{Reflist}} <references./> <includeonly>+</includeonly> <noinclude>+</noinclude> {\{DEFAULTSORT:+}} <nowiki>+</nowiki> <!--.+_--> <span.class="plainlinks">+</span>',
'Symbols': '~ | ¡¿†‡↔↑↓•¶#∞ ‹+› «+» {\{angle.bracket|+}} ¤₳฿₵¢₡₢$₫₯€₠₣ƒ₴₭₤ℳ₥₦₧₰£៛₨₪৳₮₩¥ ♠♣♥♦ 𝄫♭♮♯𝄪 ¼½¾ © ◌ ☉☾☿♀🜨♂♃♄⛢♆',
'Latin': 'A a Á á À à  â Ä ä Ǎ ǎ Ă ă Ā ā à ã Å å Ą ą Æ æ Ǣ ǣ B b C c Ć ć Ċ ċ Ĉ ĉ Č č Ç ç D d Ď ď Đ đ Ḍ ḍ Ð ð E e É é È è Ė ė Ê ê Ë ë Ě ě Ĕ ĕ Ē ē Ẽ ẽ Ę ę Ẹ ẹ Ɛ ɛ Ǝ ǝ Ə ə F f G g Ġ ġ Ĝ ĝ Ğ ğ Ģ ģ H h Ĥ ĥ Ħ ħ Ḥ ḥ I i İ ı Í í Ì ì Î î Ï ï Ǐ ǐ Ĭ ĭ Ī ī Ĩ ĩ Į į Ị ị J j Ĵ ĵ K k Ķ ķ L l Ĺ ĺ Ŀ ŀ Ľ ľ Ļ ļ Ł ł Ḷ ḷ Ḹ ḹ M m Ṃ ṃ N n Ń ń Ň ň Ñ ñ Ņ ņ Ṇ ṇ Ŋ ŋ O o Ó ó Ò ò Ô ô Ö ö Ǒ ǒ Ŏ ŏ Ō ō Õ õ Ǫ ǫ Ọ ọ Ő ő Ø ø Œ œ Ɔ ɔ P p Q q R r Ŕ ŕ Ř ř Ŗ ŗ Ṛ ṛ Ṝ ṝ S s Ś ś Ŝ ŝ Š š Ş ş Ș ș Ṣ ṣ ß T t Ť ť Ţ ţ Ț ț Ṭ ṭ Þ þ U u Ú ú Ù ù Û û Ü ü Ǔ ǔ Ŭ ŭ Ū ū Ũ ũ Ů ů Ų ų Ụ ụ Ű ű Ǘ ǘ Ǜ ǜ Ǚ ǚ Ǖ ǖ V v W w Ŵ ŵ X x Y y Ý ý Ŷ ŷ Ÿ ÿ Ỹ ỹ Ȳ ȳ Z z Ź ź Ż ż Ž ž ß Ð ð Þ þ Ŋ ŋ Ə ə Ɂ ɂ Ꞌ ꞌ ʻ ʼ ʽ ꞉ ꞏ',
'Greek': 'ΆάΈέΉήΊίΌόΎύΏώ ΑαΒβΓγΔδ ΕεΖζΗηΘθ ΙιΚκΛλΜμ ΝνΞξΟοΠπ ΡρΣσςΤτΥυ ΦφΧχΨψΩω Ϝϝυ̯ι̯ ᾼᾳᾴᾺὰᾲᾶᾷἈἀᾈᾀἉἁᾉᾁἌἄᾌᾄἊἂᾊᾂἎἆᾎᾆἍἅᾍᾅἋἃᾋᾃἏἇᾏᾇ ῈὲἘἐἙἑἜἔἚἒἝἕἛἓ ῌῃῄῊὴῂῆῇἨἠᾘᾐἩἡᾙᾑἬἤᾜᾔἪἢᾚᾒἮἦᾞᾖἭἥᾝᾕἫἣᾛᾓἯἧᾟᾗ ῚὶῖἸἰἹἱἼἴἺἲἾἶἽἵἻἳἿἷΪϊΐῒῗ ῸὸὈὀὉὁὌὄὊὂὍὅὋὃ ῤῬῥ ῪὺῦὐὙὑὔὒὖὝὕὛὓὟὗΫϋΰῢῧ ῼῳῴῺὼῲῶῷὨὠᾨᾠὩὡᾩᾡὬὤᾬᾤὪὢᾪᾢὮὦᾮᾦὭὥᾭᾥὫὣᾫᾣὯὧᾯᾧ ᾹᾱᾸᾰῙῑῘῐῩῡῨῠ {{lang|el|+}} {{lang|grc|+}}',
'Cyrillic': 'АаБбВвГг ҐґЃѓДдЂђ ЕеЁёЄєЖж ЗзЅѕИиІі ЇїЙйЈјКк ЌќЛлЉљМм НнЊњОоПп РрСсТтЋћ УуЎўФфХх ЦцЧчЏџШш ЩщЪъЫыЬь ЭэЮюЯя ӘәӨөҒғҖҗ ҚқҜҝҢңҮү ҰұҲҳҸҹҺһ ҔҕӢӣӮӯҘҙ ҠҡҤҥҪҫӐӑ ӒӓӔӕӖӗӰӱ ӲӳӸӹӀ ҞҟҦҧҨҩҬҭ ҴҵҶҷҼҽҾҿ ӁӂӃӄӇӈӋӌ ӚӛӜӝӞӟӠӡ ӤӥӦӧӪӫӴӵ ́',
'Hebrew': 'אבגדהוזחטיכךלמםנןסעפףצץקרשת ׳ ״ װױײ',
'Arabic': ' Transliteration: ʾ ā ī ū ṯ ḥ ḫ ẖ ḏ š ṣ ḍ ṭ ẓ ʿ ġ ẗ á ا ﺁ ب ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ف ق ك ل م ن ه ة و ي ى ء أ إ ؤ ئ',
'IPA (English)': 'ˈ ˌ ŋ ɡ tʃ dʒ ʃ ʒ θ ð ʔ ɑː ɒ æ aɪ aʊ ɛ ɛər+ eɪ ɪ ɪər+ iː ɔː ɔɪ oʊ ʊ ʊər+ uː ʌ ɜːr+ ə ər ɒ̃ æ̃ {\{IPAc-en|+}} {\{IPA|+}} {\{angle.bracket|+}}',
'IPA': 'Consonants: ɱɳɲŋɴ : t̪ d̪ ʈɖɟɡɢʡʔ : ɸβθð ʃʒʂʐɕʑ çʝɣχʁ ħʕʜʢɦɧ : ʋɹɻɥɰʍ : ʙⱱɾɽʀ ɺ ɫɬɮɭʎʟ : ɓɗᶑʄɠʛ ʘǀǃǂǁ Vowels: ɪʏɨʉɯʊ : øɘɵɤ ə ɚ ɛœɜɝɞʌɔ : æɶɐɑɒ Spacing_diacritics: ˈˌːˑʼˀˤᵝᵊᶢˠʰʱʲˡⁿᵑʷᶣ˞‿˕˔ Combining_diacritics: ̚ ̪ ̺ ̻ ̼ ̬ ̊ ̥ ̞ ̝ ̘ ̙ ̽ ̟ ̠ ̈ ̤ ̹ ̜ ̍ ̩ ̆ ̯ ̃ ̰ ͡ ͜ Tone: ̋ ́ ̄ ̀ ̏ ̌ ̂ ᷄ ᷅ ᷇ ᷆ ᷈ ᷉ ˥˦˧˨˩ꜛꜜ : ↗↘‖ extIPA: ͈ ͉ ͎ ̣ ̫ ͊ ᷽ ͇ : ˭ᵻᵿ {\{angle.bracket|+}} {\{IPA|+}} {\{IPA.link|+}}',
'Math and logic': '− × ÷ ⋅ ° ∗ ∘ ± ∓ ≤ ≥ ≠ ≡ ≅ ≜ ≝ ≐ ≃ ≈ ⊕ ⊗ ⇐ ⇔ ⇒ ∞ ← ↔ → ≪ ≫ ∝ √ ∤ ≀ ◅ ▻ ⋉ ⋊ ⋈ ∴ ∵ ↦ ¬ ∧ ∨ ⊻ ∀ ∃ ∈ ∉ ∋ ⊆ ⊈ ⊊ ⊂ ⊄ ⊇ ⊉ ⊋ ⊃ ⊅ ∪ ∩ ∑ ∏ ∐ ′ ∫ ∬ ∭ ∮ ∇ ∂ ∆ ∅ ℂ ℍ ℕ ℙ ℚ ℝ ℤ ℵ ⌊ ⌋ ⌈ ⌉ ⊤ ⊥ ⊢ ⊣ ⊧ □ ∠ ⟨ ⟩ <math>+</math> {\{math|+}} {\{mvar|+}} {\{frac|+|}} {\{sfrac|+|}}'
},
charinsertDivider: "\240",
storageKey: 'edittoolscharsubset',
createEditTools: function ( placeholder ) {
var sel, id;
var box = document.createElement( 'div' );
var prevSubset = 0, curSubset = 0;
box.id = 'editpage-specialchars';
box.className = "nopopups";
box.title = 'Click on the character or tag to insert it into the edit window';
// append user-defined sets
if ( window.charinsertCustom ) {
for ( id in charinsertCustom ) {
if ( !editTools.charinsert[id] ) {
editTools.charinsert[id] = '';
}
}
}
// create drop-down select
sel = document.createElement( 'select' );
for ( id in editTools.charinsert ) {
sel.options[sel.options.length] = new Option( id, id );
}
sel.selectedIndex = 0;
sel.style.marginRight = '.3em';
sel.title = 'Choose character subset';
sel.onchange = sel.onkeyup = selectSubset;
box.appendChild( sel );
// create "recall" switch
if ( window.editToolsRecall ) {
var recall = document.createElement( 'span' );
recall.appendChild( document.createTextNode( '↕' ) ); // ↔
recall.onclick = function() {
sel.selectedIndex = prevSubset;
selectSubset();
};
recall.style.cssFloat = 'left';
recall.style.marginRight = '5px';
recall.style.cursor = 'pointer';
box.appendChild( recall );
}
if ( getSelectedSection() ) {
sel.selectedIndex = getSelectedSection();
}
placeholder.parentNode.replaceChild( box, placeholder );
selectSubset();
return;
function selectSubset() {
// remember previous (for "recall" button)
prevSubset = curSubset;
curSubset = sel.selectedIndex;
//save into web storage for persistence
saveSelectedSection( curSubset );
//hide other subsets
var pp = box.getElementsByTagName( 'p' ) ;
for ( var i = 0; i < pp.length; i++ ) {
pp[i].style.display = 'none';
}
//show/create current subset
var id = sel.options[curSubset].value;
var p = document.getElementById( id );
if ( !p ) {
p = document.createElement( 'p' );
p.className = 'nowraplinks';
p.id = id;
if ( id == 'Arabic' || id == 'Hebrew' ) {
p.style.fontSize = '120%';
p.dir = 'rtl';
}
var tokens = editTools.charinsert[id];
if ( window.charinsertCustom && charinsertCustom[id] ) {
if ( tokens.length > 0 ) {
tokens += ' ';
}
tokens += charinsertCustom[id];
}
editTools.createTokens( p, tokens );
box.appendChild( p );
}
p.style.display = 'inline';
}
},
createTokens: function ( paragraph, str ) {
var tokens = str.split( ' ' ), token, i, n;
for ( i = 0; i < tokens.length; i++ ) {
token = tokens[i];
n = token.indexOf( '+' );
if ( token.charAt( 0 ) === '␥' ) {
if ( token.length > 1 && mw.config.get( 'wgNamespaceNumber' ) === 0 ) {
continue;
} else {
token = token.substring( 1 );
}
}
if ( token === '' || token === '_' ) {
addText( editTools.charinsertDivider + ' ' );
} else if ( token === '\n' ) {
paragraph.appendChild( document.createElement( 'br' ) );
} else if ( token === '___' ) {
paragraph.appendChild( document.createElement( 'hr' ) );
} else if ( token.charAt( token.length-1 ) === ':' ) { // : at the end means just text
addBold( token );
} else if ( n === 0 ) { // +<tag> -> <tag>+</tag>
addLink( token.substring( 1 ), '</' + token.substring( 2 ), token.substring( 1 ) );
} else if ( n > 0 ) { // <tag>+</tag>
addLink( token.substring( 0, n ), token.substring( n+1 ) );
} else {
var chars = Array.from(token);
if ( chars.length > 2 && token.charCodeAt( 0 ) > 127 ) { // a string of insertable characters
for ( var j = 0; j < chars.length; j++ ) {
addLink( chars[ j ], '' );
}
} else {
addLink( token, '' );
}
}
}
return;
function addLink( tagOpen, tagClose, name ) {
var handler;
var dle = tagOpen.indexOf( '\x10' );
var a = document.createElement( 'a' );
if ( dle > 0 ) {
var path = tagOpen.substring( dle + 1 ).split( '.' );
tagOpen = tagOpen.substring( 0, dle );
handler = window;
for ( var i = 0; i < path.length; i++ ) {
handler = handler[path[i]];
}
$( a ).on( 'click', handler );
} else {
tagOpen = tagOpen.replace( /\./g,' ' );
tagClose = tagClose ? tagClose.replace( /_/g,' ' ) : '';
$( a ).on( 'click', {
tagOpen: tagOpen,
sampleText: '',
tagClose: tagClose
}, insertTags );
}
name = name || tagOpen + tagClose;
name = name.replace( /\\n/g,'' );
a.appendChild( document.createTextNode( name ) );
a.href = '';
paragraph.appendChild( a );
addText( ' ' );
}
function addBold( text ) {
var b = document.createElement( 'b' );
b.appendChild( document.createTextNode( text.replace( /_/g,' ' ) ) );
paragraph.appendChild( b );
addText( ' ' );
}
function addText( txt ) {
paragraph.appendChild( document.createTextNode( txt ) );
}
function insertTags( e ) {
e.preventDefault();
if ( $currentFocused && $currentFocused.length && !$currentFocused.prop( 'readonly' ) ) {
$currentFocused.textSelection(
'encapsulateSelection', {
pre: e.data.tagOpen,
peri: e.data.sampleText,
post: e.data.tagClose
}
);
}
}
},
setup: function () {
var placeholder;
if ( $( '#editpage-specialchars' ).length ) {
placeholder = $( '#editpage-specialchars' )[0];
} else {
placeholder = $( '<div id="editpage-specialchars"> </div>' ).prependTo( '.mw-editTools' )[0];
}
if ( !placeholder ) {
return;
}
if ( !window.charinsertDontMove ) {
$( '.editOptions' ).before( placeholder );
}
// Find the element that is focused
$currentFocused = $( '#wpTextbox1' );
// Apply to dynamically created textboxes as well as normal ones
$( document ).on( 'focus', 'textarea, input:text, .CodeMirror', function () {
if ( $( this ).is( '.CodeMirror' ) ) {
// CodeMirror hooks into #wpTextbox1 for textSelection changes
$currentFocused = $( '#wpTextbox1' );
} else {
$currentFocused = $( this );
}
} );
// Used to determine where to insert tags
editTools.createEditTools( placeholder );
window.updateEditTools = function () {
editTools.createEditTools( $( '#editpage-specialchars' )[0] );
};
}
}; // end editTools
editTools.setup();
} );
7ik9xga5j2pifgle9bhu70spew0ystq
MediaWiki:Gadget-charinsert-styles.css
8
29059
326082
2026-04-04T08:53:31Z
Kannotlogin
29153
nieuw blad: /* _____________________________________________________________________________ * | | * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. | * |_________________________________________________________________…
326082
css
text/css
/* _____________________________________________________________________________
* | |
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. |
* |_____________________________________________________________________________|
*
*/
/* Overwrites selector from MediaWiki:Common.css */
div#editpage-specialchars {
display: block;
border: 1px solid var( --border-color-base, #a2a9b1 );
padding: .5em 1em;
}
#editpage-specialchars a {
background-color: var( --background-color-interactive-subtle, #f8f9fa );
color: var( --color-progressive, #36c );
border: 1px solid var( --border-color-interactive, #72777d );
padding: 1px 4px;
}
textarea#wpTextbox1 + #editpage-specialchars,
.wikiEditor-ui-clear + #editpage-specialchars {
border-top: 0;
}
qyd3rw16ijjqrifoew5xtp699xbzpi4
MediaWiki:Gadget-wikEdDiff.js
8
29060
326083
2026-04-04T08:53:39Z
Kannotlogin
29153
nieuw blad: // _________________________________________________________________________________________ // | | // | === WARNING: GLOBAL GADGET FILE === | // | Changes to this page affect many users. | // | Please discuss changes on the talk page or on [[Wikipedia_talk:Gadget]] before editing. | // |_____…
326083
javascript
text/javascript
// _________________________________________________________________________________________
// | |
// | === WARNING: GLOBAL GADGET FILE === |
// | Changes to this page affect many users. |
// | Please discuss changes on the talk page or on [[Wikipedia_talk:Gadget]] before editing. |
// |_________________________________________________________________________________________|
//
// Imports [[User:Cacycle/wikEdDiff.js]]
// an improved diff view for article revisions, see [[User:Cacycle/wikEdDiff]] for details
mw.loader.load( '/w/index.php?title=User:Cacycle/wikEdDiff.js&action=raw&ctype=text/javascript' );
ap5s0tcb6fj2edzdh146dtx1rwss390
MediaWiki:Gadget-ProveIt.js
8
29061
326084
2026-04-04T08:53:48Z
Kannotlogin
29153
nieuw blad: /** * ProveIt is a reference manager for Wikipedia and any other MediaWiki wiki * Documentation: https://www.mediawiki.org/wiki/ProveIt * Source code: https://www.mediawiki.org/wiki/MediaWiki:Gadget-Global-ProveIt.js */ function loadProveIt() { mw.config.set( { // Citation templates (without namespace) 'proveit-templates': [ 'Citation', 'Cite arXiv', 'Cite AV media', 'Cite AV media notes', 'Cite book', 'Cite bioRxiv', 'Cite conference', 'Cite co…
326084
javascript
text/javascript
/**
* ProveIt is a reference manager for Wikipedia and any other MediaWiki wiki
* Documentation: https://www.mediawiki.org/wiki/ProveIt
* Source code: https://www.mediawiki.org/wiki/MediaWiki:Gadget-Global-ProveIt.js
*/
function loadProveIt() {
mw.config.set( {
// Citation templates (without namespace)
'proveit-templates': [
'Citation',
'Cite arXiv',
'Cite AV media',
'Cite AV media notes',
'Cite book',
'Cite bioRxiv',
'Cite conference',
'Cite comic',
'Cite encyclopedia',
'Cite episode',
'Cite Hansard',
'Cite Instagram',
'Cite interview',
'Cite journal',
'Cite magazine',
'Cite mailing list',
'Cite map',
'Cite Metacritic',
'Cite news',
'Cite newsgroup',
'Cite newspaper The Times',
'Cite ODNB',
'Cite paper',
'Cite podcast',
'Cite press release',
'Cite report',
'Cite Rotten Tomatoes',
'Cite serial',
'Cite sign',
'Cite speech',
'Cite techreport',
'Cite thesis',
'Cite tweet',
'Cite video',
'Cite video game',
'Cite ssrn',
'Cite wikisource',
'Cite web',
'Cite Q',
'R',
'Sfn',
'Sfnm'
],
// Citation templates that shouldn't go inside <ref> tags
'proveit-templates-noref': [ 'R', 'Sfn', 'Sfnm' ],
// Preferred date format, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options
'proveit-date-format': { year: 'numeric', month: 'long', day: 'numeric' },
// Revision tag defined at Special:Tags
'proveit-tag': 'ProveIt edit',
// Automatic edit summary
'proveit-summary': 'Reference edited with [[Wikipedia:ProveIt|ProveIt]]',
} );
// Load from the central, global version at MediaWiki.org
mw.loader.load( '//www.mediawiki.org/w/load.php?modules=ext.gadget.Global-ProveIt' );
}
// Only load when editing
mw.hook( 'wikipage.editform' ).add( () => window.ProveIt || loadProveIt() );
mw.hook( 've.newTarget' ).add( target => target.constructor.static.name === 'article' && target.on( 'surfaceReady', loadProveIt ) );
9x5twl8ctsrqs702cbjcwidvmaiiz0w
MediaWiki:Gadget-UTCLiveClock.js
8
29062
326085
2026-04-04T08:54:02Z
Kannotlogin
29153
nieuw blad: window.UTCLiveClockLocation = 'outside'; mw.loader.load( '//www.mediawiki.org/w/index.php?title=MediaWiki:Gadget-UTCLiveClock.js&action=raw&ctype=text/javascript' );
326085
javascript
text/javascript
window.UTCLiveClockLocation = 'outside';
mw.loader.load( '//www.mediawiki.org/w/index.php?title=MediaWiki:Gadget-UTCLiveClock.js&action=raw&ctype=text/javascript' );
95msqinjzhmr882u50mcnonvhgm88gy
MediaWiki:Gadget-UTCLiveClock.css
8
29063
326086
2026-04-04T08:54:10Z
Kannotlogin
29153
nieuw blad: /** * Explicitly set width of the UTC-clock list element, so that we can use a * hidden peer gadget to add space where the clock would go before it loads. */ .skin-vector #utcdate { width: 6em; /* * The default margin-left is 0.75em, but set it again here explicitly, so * we can be sure of the exact width. */ margin-left: 0.75em; } .skin-monobook #utcdate { width: 6.279em; display: inline-block; /* We need this for the width property to work */ /* * The defa…
326086
css
text/css
/**
* Explicitly set width of the UTC-clock list element, so that we can use a
* hidden peer gadget to add space where the clock would go before it loads.
*/
.skin-vector #utcdate {
width: 6em;
/*
* The default margin-left is 0.75em, but set it again here explicitly, so
* we can be sure of the exact width.
*/
margin-left: 0.75em;
}
.skin-monobook #utcdate {
width: 6.279em;
display: inline-block; /* We need this for the width property to work */
/*
* The default margin-left is 1em, but set it again here explicitly, so
* we can be sure of the exact width.
*/
margin-left: 1em;
}
/* Do not clip the seconds in the date */
.skin-vector-2022 #utcdate a {
width: 100%;
}
2nxlb3tmp6tu1gjr926cwu9krkw4vd0
MediaWiki:Gadget-purgetab.js
8
29064
326087
2026-04-04T08:54:19Z
Kannotlogin
29153
nieuw blad: /** * Add "Purge" content action link. * * Dependencies: mediawiki.util, mediawiki.api * * @source https://www.mediawiki.org/wiki/Snippets/Purge_action */ $( function () { if ( $( '#ca-purge' ).length || !mw.config.get( 'wgIsArticle' ) ) return; var node = mw.util.addPortletLink( 'p-cactions', mw.util.getUrl( null, { action: 'purge' } ), mw.config.get( 'skin' ) === 'vector' || mw.config.get( 'skin' ) === 'vector-2022' ? 'Purge' : '*', 'ca-purge', 'Purge the se…
326087
javascript
text/javascript
/**
* Add "Purge" content action link.
*
* Dependencies: mediawiki.util, mediawiki.api
*
* @source https://www.mediawiki.org/wiki/Snippets/Purge_action
*/
$( function () {
if ( $( '#ca-purge' ).length || !mw.config.get( 'wgIsArticle' ) ) return;
var node = mw.util.addPortletLink(
'p-cactions',
mw.util.getUrl( null, { action: 'purge' } ),
mw.config.get( 'skin' ) === 'vector' || mw.config.get( 'skin' ) === 'vector-2022' ? 'Purge' : '*',
'ca-purge',
'Purge the server cache of this page',
'*'
);
$(node).on( 'click', function (e) {
new mw.Api().post( { action: 'purge', titles: mw.config.get( 'wgPageName' ) } ).then(function () {
location.reload();
}, function () {
mw.notify( 'Purge failed', { type: 'error' } );
});
e.preventDefault();
});
});
nh2he2vdb8yv2ghzwnyagn106as59fl
MediaWiki:Gadget-edittop.js
8
29065
326088
2026-04-04T08:54:31Z
Kannotlogin
29153
nieuw blad: // ********************************************************************** // ** ***WARNING GLOBAL GADGET FILE*** ** // ** changes to this file affect many users. ** // ** please discuss on the talk page before editing ** // ** ** // ********************************************************************** // Imported from [[User:Alex Smotrov/ed…
326088
javascript
text/javascript
// **********************************************************************
// ** ***WARNING GLOBAL GADGET FILE*** **
// ** changes to this file affect many users. **
// ** please discuss on the talk page before editing **
// ** **
// **********************************************************************
// Imported from [[User:Alex Smotrov/edittop.js]], version as of: 2007-06-19T04:28:52
// Updated from [[User:TheDJ/Gadget-edittop.js]], version as of: 2009-04-28T11:54:22
if (
[ 'view', 'purge' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 &&
mw.config.get( 'wgNamespaceNumber' ) >= 0 &&
!mw.config.get( 'wgMFMode' )
) {
$( function edittopHook() {
var localtitles = {
bg: 'Редактиране на началото',
bn: 'সূচনা অনুচ্ছেদ সম্পাদনা করুন',
cs: 'Editovat úvodní sekci',
en: 'Edit lead section',
fa: 'ویرایش بخش آغازین',
fr: 'Modifier le résumé introductif',
id: 'Sunting bagian atas',
it: 'Modifica della sezione iniziale',
ja: '導入部を編集',
kk: 'Кіріспе бөлімді өңдеу',
min: 'Suntiang bagian ateh',
ko: '도입부를 편집',
pa: 'ਸੋਧ',
pt: 'Editar a seção superior',
'pt-br': 'Editar a seção superior',
sr: 'Уреди уводни део',
th: 'แก้ไขย่อหน้าแรกสุด',
vi: 'Sửa phần mở đầu'
};
var $content = $( '#content, #mw_content' ).first();
var $span1 = $content.find( 'span.mw-editsection:not(.plainlinks)' ).first();
if ( !$span1.length ) {
return;
}
var $span0 = $span1.clone();
$span0.removeClass( 'cdx-button--size-large' );
$( '#mw_header h1, #content h1' ).first().append( $span0 );
$span0.find( 'a' ).each( function () {
var $a = $( this ), href;
$a.attr( 'title', localtitles[ mw.config.get( 'wgUserLanguage' ) ] || localtitles.en );
href = $a.attr( 'href' ) || '#';
if ( !/&(ve|)section=T/.test( $a.attr( 'href' ) ) ) { // not transcluded
$a.attr( 'href', href.replace( /&(ve|)section=\d+/, '&$1section=0&summary=/*%20top%20*/%20' ) );
} else if ( /&vesection=/.test( $a.attr( 'href' ) ) ) { // transcluded, VE
$a.attr( 'href', mw.util.getUrl( mw.config.get( 'wgPageName' ) ) + '?veaction=edit&vesection=0&summary=/*%20top%20*/%20' );
} else { // transcluded, not VE
$a.attr( 'href', mw.util.getUrl( mw.config.get( 'wgPageName' ) ) + '?action=edit§ion=0&summary=/*%20top%20*/%20' );
}
} );
} );
}
72miasaati0agd94n0cnqkubb26cc4r
MediaWiki:Gadget-edittop.css
8
29066
326089
2026-04-04T08:54:52Z
Kannotlogin
29153
nieuw blad: /* @noflip */ body.ltr h1.firstHeading .mw-editsection-bracket:first-of-type, body.rtl h1.firstHeading .mw-editsection-bracket:not(:first-of-type) { margin-right: 0.25em; color: #555; } /* @noflip */ body.rtl h1.firstHeading .mw-editsection-bracket:first-of-type, body.ltr h1.firstHeading .mw-editsection-bracket:not(:first-of-type) { margin-left: 0.25em; color: #555; } /* For desktop Minerva skin T190989 */ .page-heading .mw-editsection a { display: inline-block; font-siz…
326089
css
text/css
/* @noflip */
body.ltr h1.firstHeading .mw-editsection-bracket:first-of-type,
body.rtl h1.firstHeading .mw-editsection-bracket:not(:first-of-type) {
margin-right: 0.25em;
color: #555;
}
/* @noflip */
body.rtl h1.firstHeading .mw-editsection-bracket:first-of-type,
body.ltr h1.firstHeading .mw-editsection-bracket:not(:first-of-type) {
margin-left: 0.25em;
color: #555;
}
/* For desktop Minerva skin T190989 */
.page-heading .mw-editsection a {
display: inline-block;
font-size: 1rem;
vertical-align: middle;
}
s6xz9vcff6oucdq5n3kmfns3pk7m0s5
MediaWiki:Gadget-dropdown-menus.js
8
29067
326090
2026-04-04T08:55:01Z
Kannotlogin
29153
nieuw blad: /********************************************************************* ** ***WARNING: GLOBAL GADGET FILE*** ** ** any changes to this file will affect many users ** ** please discuss changes on the talk page or at ** ** [[Wikipedia talk:Gadget]] before editing ** ** (consider dropping the script author a note as well...) ** **…
326090
javascript
text/javascript
/*********************************************************************
** ***WARNING: GLOBAL GADGET FILE*** **
** any changes to this file will affect many users **
** please discuss changes on the talk page or at **
** [[Wikipedia talk:Gadget]] before editing **
** (consider dropping the script author a note as well...) **
** **
**********************************************************************
** Script: MoreMenu **
** Author: MusikAnimal **
** Documentation: [[meta:MoreMenu]] **
*********************************************************************/
mw.loader.load('https://meta.wikimedia.org/w/load.php?modules=ext.gadget.MoreMenu');
00hi01moh046ectz4md6hyysxl0upc3
MediaWiki:Gadget-MoreMenu.vls.js
8
29068
326091
2026-04-04T08:55:12Z
Kannotlogin
29153
nieuw blad: /** * WARNING: GLOBAL GADGET FILE * * Enwiki extension to MoreMenu. When applicable, this adds the following links: * * User * - Analysis / BLP edits * - Analysis / AfD stats * - RfXs… * * Page * - AfDs * - MfDs * * See [[meta:MoreMenu#Customization]] for more information on extending MoreMenu. */ $(function () { /** * Look for and add links to RfAs, RfBs, Arbitration cases, etc. * @param {mw.Api} api * @param {Object} config */ f…
326091
javascript
text/javascript
/**
* WARNING: GLOBAL GADGET FILE
*
* Enwiki extension to MoreMenu. When applicable, this adds the following links:
*
* User
* - Analysis / BLP edits
* - Analysis / AfD stats
* - RfXs…
*
* Page
* - AfDs
* - MfDs
*
* See [[meta:MoreMenu#Customization]] for more information on extending MoreMenu.
*/
$(function () {
/**
* Look for and add links to RfAs, RfBs, Arbitration cases, etc.
* @param {mw.Api} api
* @param {Object} config
*/
function addRfXs(api, config) {
var rfxs = {
'Wikipedia:Requests for adminship': 'rfa',
'Wikipedia:Requests for bureaucratship': 'rfb',
'Wikipedia:Arbitration/Requests/Case': 'rfarb',
'Wikipedia:Requests for comment': 'rfc',
'Wikipedia:Requests for checkuser': 'rfcu',
'Wikipedia:Requests for checkuser/Case': 'rfcuc',
'Wikipedia:Requests for oversight': 'rfo',
'Wikipedia:Contributor copyright investigations': 'cci',
'Wikipedia:Sockpuppet investigations': 'spi',
'Wikipedia:Bots/Requests for approval': 'brfa',
'Wikipedia:Administrator recall': 'adrc'
};
$.extend(MoreMenu.messages, {
rfa: 'RfAs',
rfb: 'RfBs',
rfarb: 'RfArbs',
rfc: 'RfCs',
rfcu: 'RfCUs',
rfcuc: 'RfCUCs',
rfo: 'RfOs',
cci: 'CCIs',
spi: 'SPIs',
brfa: 'BRFAs',
adrc: 'ADRCs'
});
var links = {};
api.get({
titles: Object.keys(rfxs).map(function (rfx) {
return rfx + '/' + config.targetUser.name;
}).join('|'),
formatversion: 2
}).done(function (data) {
data.query.pages.forEach(function (page) {
if (!page.missing) {
var key = rfxs[page.title.replace('/' + config.targetUser.name, '')];
links[key] = {
url: mw.util.getUrl('Special:PrefixIndex/' + page.title)
};
}
});
if (Object.keys(links).length) {
MoreMenu.addSubmenu('user', 'RfXs', links, 'analysis');
}
});
}
/**
* Look for and add a link to Special:PrefixIndex for AfDs or XfDs.
* @param {mw.Api} api
* @param {Object} config
*/
function addXfD(api, config) {
api.get({
titles: [
'Wikipedia:Articles for deletion/' + config.page.name,
'Wikipedia:Miscellany for deletion/' + config.page.name
].join('|'),
prop: 'info',
formatversion: 2
}).done(function (data) {
data.query.pages.some(function (page) {
if (page.missing) {
return false;
}
var link = mw.util.getUrl('Special:PrefixIndex/' + page.title);
switch (page.title.split('/')[0]) {
case 'Wikipedia:Miscellany for deletion':
return MoreMenu.addLink('page', 'MfDs', link);
case 'Wikipedia:Articles for deletion':
return MoreMenu.addLink('page', 'AfDs', link);
default:
return false;
}
});
});
}
mw.hook('moremenu.ready').add(function (config) {
var api = new mw.Api();
if (config.targetUser.name && !config.targetUser.ipRange) {
addRfXs(api, config);
}
if (config.page.name) {
addXfD(api, config);
}
// Add link to BLP edits in the 'Analysis' menu.
if (!config.targetUser.ipRange) {
MoreMenu.addSubmenuLink('user', 'analysis', 'BLP Edits', 'https://xtools.wmflabs.org/categoryedits/' + config.project.domain + '/' + config.targetUser.encodedName + '/Living people');
// Add link to AfD stats.
MoreMenu.addSubmenuLink('user', 'analysis', 'AfD stats', 'https://afdstats.toolforge.org/afdstats.py?name=' + config.targetUser.encodedName, 'analysis-xtools');
}
});
});
80s58fakev3ozgelg4ave3tndbl6syt
MediaWiki:Gadget-RTRC.js
8
29069
326092
2026-04-04T08:55:31Z
Kannotlogin
29153
nieuw blad: // [[File:Krinkle_RTRC.js]] mw.loader.getState('ext.gadget.rtrc') ? mw.loader.load('ext.gadget.rtrc') : mw.loader.load('https://www.mediawiki.org/w/load.php?debug=false&modules=ext.gadget.rtrc&lang=' + mw.config.get('wgUserLanguage', 'en'));
326092
javascript
text/javascript
// [[File:Krinkle_RTRC.js]]
mw.loader.getState('ext.gadget.rtrc') ? mw.loader.load('ext.gadget.rtrc') : mw.loader.load('https://www.mediawiki.org/w/load.php?debug=false&modules=ext.gadget.rtrc&lang=' + mw.config.get('wgUserLanguage', 'en'));
7cixduefbu5uo14y1kzd9vdwtolwvvx
MediaWiki:Gadget-morebits.js
8
29070
326093
2026-04-04T08:55:51Z
Kannotlogin
29153
nieuw blad: // <nowiki> /** * A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia. * * The highlights include: * - {@link Morebits.wiki.Api} - make calls to the MediaWiki API * - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.) * - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js * - {@link Morebits.QuickForm} - generate quick HTML forms on the fly * - {@link Morebits.SimpleWi…
326093
javascript
text/javascript
// <nowiki>
/**
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - {@link Morebits.wiki.Api} - make calls to the MediaWiki API
* - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.)
* - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js
* - {@link Morebits.QuickForm} - generate quick HTML forms on the fly
* - {@link Morebits.SimpleWindow} - generate dialog windows and modals
* - {@link Morebits.Status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - {@link Morebits.wikitext} - utilities for dealing with wikitext
* - {@link Morebits.string} - utilities for manipulating strings
* - {@link Morebits.array} - utilities for manipulating arrays
* - {@link Morebits.ip} - utilities to help process IP addresses
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - {@link Morebits.QuickForm}, {@link Morebits.SimpleWindow}, and {@link Morebits.Status} rely on the "morebits.css" file for their styling.
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title]|morebits.js|morebits.css|GadgetName.js`
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition:
* - `*morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title|hidden]|morebits.js|morebits.css`
* and then load ext.gadget.morebits as one of the dependencies for the new gadget.
*
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
* The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/src/morebits.js|GitHub}.
*
* @namespace Morebits
*/
(function() {
/** @lends Morebits */
const Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* Wiki-specific configurations for Morebits
*/
Morebits.l10n = {
/**
* Local aliases for "redirect" magic word.
* Check using api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=magicwords
*/
redirectTagAliases: ['#REDIRECT'],
/**
* Takes a string as argument and checks if it is a timestamp or not
* If not, it returns null. If yes, it returns an array of integers
* in the format [year, month, date, hour, minute, second]
* which can be passed to Date.UTC()
*
* @param {string} str
* @return {number[] | null}
*/
signatureTimestampFormat: function (str) {
// HH:mm, DD Month YYYY (UTC)
const rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
const match = rgx.exec(str);
if (!match) {
return null;
}
const month = Morebits.Date.localeData.months.indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
};
/**
* Simple helper function to see what groups a user might belong.
*
* @param {string} group - e.g. `sysop`, `extendedconfirmed`, etc.
* @return {boolean}
*/
Morebits.userIsInGroup = function (group) {
return mw.config.get('wgUserGroups').includes(group);
};
/**
* Hardcodes whether the user is a sysop, used a lot.
*
* @type {boolean}
*/
Morebits.userIsSysop = Morebits.userIsInGroup('sysop');
/**
* Determines whether the current page is a redirect or soft redirect. Fails
* to detect soft redirects on edit, history, etc. pages. Will attempt to
* detect Module:RfD, with the same failure points.
*
* @return {boolean}
*/
Morebits.isPageRedirect = function() {
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length);
};
/**
* Stores a normalized (underscores converted to spaces) version of the
* `wgPageName` variable.
*
* @type {string}
*/
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' ');
/**
* Create a string for use in regex matching a page name. Accounts for
* leading character's capitalization, underscores as spaces, and special
* characters being escaped. See also {@link Morebits.namespaceRegex}.
*
* @param {string} pageName - Page name without namespace.
* @return {string} - For a page name `Foo bar`, returns the string `[Ff]oo[_ ]bar`.
*/
Morebits.pageNameRegex = function(pageName) {
if (pageName === '') {
return '';
}
const firstChar = pageName[0],
remainder = Morebits.string.escapeRegExp(pageName.slice(1));
if (mw.Title.phpCharToUpper(firstChar) !== firstChar.toLowerCase()) {
return '[' + mw.Title.phpCharToUpper(firstChar) + firstChar.toLowerCase() + ']' + remainder;
}
return Morebits.string.escapeRegExp(firstChar) + remainder;
};
/**
* Converts string or array of DOM nodes into an HTML fragment.
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
* Used in Morebits.QuickForm and Morebits.Status
*
* @internal
* @param {string|Node|(string|Node)[]} input
* @return {DocumentFragment}
*/
Morebits.createHtml = function(input) {
const fragment = document.createDocumentFragment();
if (!input) {
return fragment;
}
if (!Array.isArray(input)) {
input = [ input ];
}
for (let i = 0; i < input.length; ++i) {
if (input[i] instanceof Node) {
fragment.appendChild(input[i]);
} else {
$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach((node) => {
fragment.appendChild(node);
});
}
}
return fragment;
};
/**
* Converts wikilinks to HTML anchor tags.
*
* @param {string} text
* @return {string}
*/
Morebits.createHtml.renderWikilinks = function (text) {
const ub = new Morebits.Unbinder(text);
// Don't convert wikilinks within code tags as they're used for displaying wiki-code
ub.unbind('<code>', '</code>');
ub.content = ub.content.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
(_, target, text) => {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '"') + '">' + text + '</a>';
});
return ub.rebind();
};
/**
* Create a string for use in regex matching all namespace aliases, regardless
* of the capitalization and underscores/spaces. Doesn't include the optional
* leading `:`, but if there's more than one item, wraps the list in a
* non-capturing group. This means you can do `Morebits.namespaceRegex([4]) +
* ':' + Morebits.pageNameRegex('Twinkle')` to match a full page. Uses
* {@link Morebits.pageNameRegex}.
*
* @param {number[]} namespaces - Array of namespace numbers. Unused/invalid
* namespace numbers are silently discarded.
* @example
* // returns '(?:[Ff][Ii][Ll][Ee]|[Ii][Mm][Aa][Gg][Ee])'
* Morebits.namespaceRegex([6])
* @return {string} - Regex-suitable string of all namespace aliases.
*/
Morebits.namespaceRegex = function(namespaces) {
if (!Array.isArray(namespaces)) {
namespaces = [namespaces];
}
const aliases = [];
let regex;
$.each(mw.config.get('wgNamespaceIds'), (name, number) => {
if (namespaces.includes(number)) {
// Namespaces are completely agnostic as to case,
// and a regex string is more useful/compatible than a RegExp object,
// so we accept any casing for any letter.
aliases.push(name.split('').map((char) => Morebits.pageNameRegex(char)).join(''));
}
});
switch (aliases.length) {
case 0:
regex = '';
break;
case 1:
regex = aliases[0];
break;
default:
regex = '(?:' + aliases.join('|') + ')';
break;
}
return regex;
};
/* **************** Morebits.QuickForm **************** */
/**
* Creation of simple and standard forms without much specific coding.
*
* @namespace Morebits.QuickForm
* @memberof Morebits
* @class
* @param {event} event - Function to execute when form is submitted.
* @param {string} [eventType=submit] - Type of the event.
*/
Morebits.QuickForm = function QuickForm(event, eventType) {
this.root = new Morebits.QuickForm.Element({ type: 'form', event: event, eventType: eventType });
};
/**
* Renders the HTML output of the quickForm.
*
* @memberof Morebits.QuickForm
* @return {HTMLElement}
*/
Morebits.QuickForm.prototype.render = function QuickFormRender() {
const ret = this.root.render();
ret.names = {};
return ret;
};
/**
* Append element to the form.
*
* @memberof Morebits.QuickForm
* @param {(object|Morebits.QuickForm.Element)} data - A quickform element, or the object with which
* a quickform element is constructed.
* @return {Morebits.QuickForm.Element} - Same as what is passed to the function.
*/
Morebits.QuickForm.prototype.append = function QuickFormAppend(data) {
return this.root.append(data);
};
/**
* Create a new element for the the form.
*
* Index to Morebits.QuickForm.Element types:
* - Global attributes: id, className, style, tooltip, extra, $data, adminonly
* - `select`: A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event, disabled
* - `option`: An element for a combo box.
* - Attributes: value, label, selected, disabled
* - `optgroup`: A group of "option"s.
* - Attributes: label, list
* - `field`: A fieldset (aka group box).
* - Attributes: name, label, disabled
* - `checkbox`: A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `radio`: A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `input`: A text input box.
* - Attributes: name, label, value, size, placeholder, maxlength, disabled, required, readonly, event
* - `number`: A number input box.
* - Attributes: Everything the text `input` has, as well as: min, max, step, list
* - `dyninput`: A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, inputs, sublabel, value, size, maxlength, event
* - `hidden`: An invisible form field.
* - Attributes: name, value
* - `header`: A level 5 header.
* - Attributes: label
* - `div`: A generic placeholder element or label.
* - Attributes: name, label
* - `submit`: A submit button. Morebits.SimpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* - `button`: A generic button.
* - Attributes: name, label, disabled, event
* - `textarea`: A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, required, readonly
* - `fragment`: A DocumentFragment object.
* - No attributes, and no global attributes except adminonly.
* There is some difference on how types handle the `label` attribute:
* - `div`, `select`, `field`, `checkbox`/`radio`, `input`, `textarea`, `header`, and `dyninput` can accept an array of items,
* and the label item(s) can be `Element`s.
* - `option`, `optgroup`, `_dyninput_cell`, `submit`, and `button` accept only a single string.
*
* @memberof Morebits.QuickForm
* @class
* @param {Object} data - Object representing the quickform element. Should
* specify one of the available types from the index above, as well as any
* relevant and available attributes.
* @example new Morebits.QuickForm.Element({
* name: 'target',
* type: 'input',
* label: 'Your target:',
* tooltip: 'Enter your target. Required.',
* required: true
* });
*/
Morebits.QuickForm.Element = function QuickFormElement(data) {
this.data = data;
this.childs = [];
};
/**
* @memberof Morebits.QuickForm.Element
* @type {number}
*/
Morebits.QuickForm.Element.id = 0;
/**
* Appends an element to current element.
*
* @memberof Morebits.QuickForm.Element
* @param {Morebits.QuickForm.Element} data - A quickForm element or the object required to
* create the quickForm element.
* @return {Morebits.QuickForm.Element} The same element passed in.
*/
Morebits.QuickForm.Element.prototype.append = function QuickFormElementAppend(data) {
let child;
if (data instanceof Morebits.QuickForm.Element) {
child = data;
} else {
child = new Morebits.QuickForm.Element(data);
}
this.childs.push(child);
return child;
};
/**
* Renders the HTML output for the quickForm element. This should be called
* without parameters: `form.render()`.
*
* @memberof Morebits.QuickForm.Element
* @return {HTMLElement}
*/
Morebits.QuickForm.Element.prototype.render = function QuickFormElementRender(internal_subgroup_id) {
const currentNode = this.compute(this.data, internal_subgroup_id);
for (let i = 0; i < this.childs.length; ++i) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild(this.childs[i].render());
}
return currentNode[0];
};
/** @memberof Morebits.QuickForm.Element */
Morebits.QuickForm.Element.prototype.compute = function QuickFormElementCompute(data, in_id) {
let node;
let childContainer = null;
let label;
const id = (in_id ? in_id + '_' : '') + 'node_' + Morebits.QuickForm.Element.id++;
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
data.type = 'hidden';
}
let i, current, subnode;
switch (data.type) {
case 'form':
node = document.createElement('form');
node.className = 'quickform';
node.setAttribute('action', 'javascript:void(0);');
if (data.event) {
node.addEventListener(data.eventType || 'submit', data.event, false);
}
break;
case 'fragment':
node = document.createDocumentFragment();
// fragments can't have any attributes, so just return it straight away
return [ node, node ];
// Sometimes Twinkle uses fancy searchable "select" elements. This is powered by the third party library "select2". Activate it by creating a Morebits "select" element, then call `$('select[name=sub_group]').select2({});` or similar towards the end of your main code.
case 'select':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.appendChild(Morebits.createHtml(data.label));
label.style.marginRight = '3px';
}
var select = node.appendChild(document.createElement('select'));
if (data.event) {
select.addEventListener('change', data.event, false);
}
if (data.multiple) {
select.setAttribute('multiple', 'multiple');
}
if (data.size) {
select.setAttribute('size', data.size);
}
if (data.disabled) {
select.setAttribute('disabled', 'disabled');
}
select.setAttribute('name', data.name);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
if (current.list) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute(current);
select.appendChild(subnode[0]);
}
}
childContainer = select;
break;
case 'option':
node = document.createElement('option');
node.values = data.value;
node.setAttribute('value', data.value);
if (data.selected) {
node.setAttribute('selected', 'selected');
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
node.setAttribute('label', data.label);
node.appendChild(document.createTextNode(data.label));
break;
case 'optgroup':
node = document.createElement('optgroup');
node.setAttribute('label', data.label);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
current.type = 'option'; // must be options here
subnode = this.compute(current);
node.appendChild(subnode[0]);
}
}
break;
case 'field':
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label.appendChild(Morebits.createHtml(data.label));
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
break;
case 'checkbox':
case 'radio':
node = document.createElement('div');
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
const cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if (current.type === 'header') {
// inline hack
cur_div = node.appendChild(document.createElement('h6'));
cur_div.appendChild(document.createTextNode(current.label));
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(cur_div, current);
}
continue;
}
cur_div = node.appendChild(document.createElement('div'));
subnode = cur_div.appendChild(document.createElement('input'));
subnode.values = current.value;
subnode.setAttribute('value', current.value);
subnode.setAttribute('type', data.type);
subnode.setAttribute('id', cur_id);
subnode.setAttribute('name', current.name || data.name);
// If name is provided on the individual checkbox, add a data-single
// attribute which indicates it isn't part of a list of checkboxes with
// same name. Used in getInputData()
if (current.name) {
subnode.setAttribute('data-single', 'data-single');
}
if (current.checked) {
subnode.setAttribute('checked', 'checked');
}
if (current.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label, current);
}
// styles go on the label, doesn't make sense to style a checkbox/radio
if (current.style) {
label.setAttribute('style', current.style);
}
var event;
if (current.subgroup) {
let tmpgroup = current.subgroup;
if (!Array.isArray(tmpgroup)) {
tmpgroup = [ tmpgroup ];
}
var subgroupRaw = new Morebits.QuickForm.Element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each(tmpgroup, (idx, el) => {
const newEl = $.extend({}, el);
if (!newEl.type) {
newEl.type = data.type;
}
newEl.name = (current.name || data.name) + '.' + newEl.name;
subgroupRaw.append(newEl);
});
const subgroup = subgroupRaw.render(cur_id);
subgroup.className = 'quickformSubgroup';
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if (e.target.checked) {
e.target.parentNode.appendChild(e.target.subgroup);
if (e.target.type === 'radio') {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild(e.target.subgroup);
}
};
subnode.addEventListener('change', event, true);
if (current.checked) {
subnode.parentNode.appendChild(subgroup);
}
} else if (data.type === 'radio') {
event = function(e) {
if (e.target.checked) {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
delete e.target.form.names[name];
}
};
subnode.addEventListener('change', event, true);
}
// add users' event last, so it can interact with the subgroup
if (data.event) {
subnode.addEventListener('change', data.event, false);
} else if (current.event) {
subnode.addEventListener('change', current.event, true);
}
}
}
if (data.shiftClickSupport && data.type === 'checkbox') {
Morebits.checkboxShiftClickSupport(Morebits.QuickForm.getElements(node, data.name));
}
break;
// input is actually a text-type, so number here inherits the same stuff
case 'number':
case 'input':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(data.label));
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('name', data.name);
if (data.type === 'input') {
subnode.setAttribute('type', 'text');
} else {
subnode.setAttribute('type', 'number');
['min', 'max', 'step', 'list'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
}
['value', 'size', 'placeholder', 'maxlength'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
['disabled', 'required', 'readonly'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, att);
}
});
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
childContainer = subnode;
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement('div');
label = node.appendChild(document.createElement('h5'));
label.appendChild(Morebits.createHtml(data.label));
var listNode = node.appendChild(document.createElement('div'));
var more = this.compute({
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
const new_node = new Morebits.QuickForm.Element(e.target.sublist);
e.target.area.appendChild(new_node.render());
if (++e.target.counter >= e.target.max) {
e.target.setAttribute('disabled', 'disabled');
}
e.stopPropagation();
}
});
node.appendChild(more[0]);
var moreButton = more[1];
var sublist = {
type: '_dyninput_row',
remove: false,
maxlength: data.maxlength,
event: data.event,
inputs: data.inputs || [{
// compatibility
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size
}]
};
for (i = 0; i < min; ++i) {
const elem = new Morebits.QuickForm.Element(sublist);
listNode.appendChild(elem.render());
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_row': // Private
node = document.createElement('div');
data.inputs.forEach((subdata) => {
const cell = new Morebits.QuickForm.Element($.extend(subdata, { type: '_dyninput_cell' }));
node.appendChild(cell.render());
});
if (data.remove) {
const remove = this.compute({
type: 'button',
label: 'remove',
event: function(e) {
const list = e.target.listnode;
const node = e.target.inputnode;
const more = e.target.morebutton;
list.removeChild(node);
--more.counter;
more.removeAttribute('disabled');
e.stopPropagation();
}
});
node.appendChild(remove[0]);
const removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case '_dyninput_cell': // Private, similar to normal input
node = document.createElement('span');
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id + '_input');
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('id', id + '_input');
if (data.value) {
subnode.setAttribute('value', data.value);
}
subnode.setAttribute('name', data.name);
subnode.setAttribute('type', 'text');
subnode.setAttribute('data-dyninput', 'data-dyninput');
if (data.size) {
subnode.setAttribute('size', data.size);
}
if (data.maxlength) {
subnode.setAttribute('maxlength', data.maxlength);
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.disabled) {
subnode.setAttribute('required', 'disabled');
}
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
node.style.marginRight = '3px';
break;
case 'hidden':
node = document.createElement('input');
node.setAttribute('type', 'hidden');
node.values = data.value;
node.setAttribute('value', data.value);
node.setAttribute('name', data.name);
break;
case 'header':
node = document.createElement('h5');
node.appendChild(Morebits.createHtml(data.label));
break;
case 'div':
node = document.createElement('div');
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.label) {
const result = document.createElement('span');
result.className = 'quickformDescription';
result.appendChild(Morebits.createHtml(data.label));
node.appendChild(result);
}
break;
case 'submit':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'submit');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name || 'submit');
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
break;
case 'button':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'button');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name);
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
if (data.event) {
childContainer.addEventListener('click', data.event, false);
}
break;
case 'textarea':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('h5'));
const labelElement = document.createElement('label');
labelElement.appendChild(Morebits.createHtml(data.label));
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
}
subnode = node.appendChild(document.createElement('textarea'));
subnode.setAttribute('name', data.name);
if (data.cols) {
subnode.setAttribute('cols', data.cols);
}
if (data.rows) {
subnode.setAttribute('rows', data.rows);
}
if (data.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.readonly) {
subnode.setAttribute('readonly', 'readonly');
}
if (data.value) {
subnode.value = data.value;
}
childContainer = subnode;
break;
default:
throw new Error('Morebits.QuickForm: unknown element type ' + data.type.toString());
}
if (!childContainer) {
childContainer = node;
}
if (data.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label || node, data);
}
if (data.extra) {
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
if (data.style) {
childContainer.setAttribute('style', data.style);
}
if (data.className) {
childContainer.className = childContainer.className ?
childContainer.className + ' ' + data.className :
data.className;
}
childContainer.setAttribute('id', data.id || id);
return [ node, childContainer ];
};
Morebits.QuickForm.$tooltip = null;
/**
* Create a tooltip.
*
* @memberof Morebits.QuickForm.Element
* @param {HTMLElement} node - The HTML element beside which a tooltip is to be generated.
* @param {Object} data - Tooltip-related configuration data.
*/
Morebits.QuickForm.Element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) {
if (!Morebits.QuickForm.$tooltip) {
Morebits.QuickForm.$tooltip = $('<div>')
.attr('id', 'morebits-ui-tooltip')
.attr('role', 'tooltip')
.addClass('morebits-ui-tooltip')
.appendTo('body');
}
const $tooltip = Morebits.QuickForm.$tooltip;
const $button = $('<span>')
.addClass('morebits-tooltipButton')
.text('?')
.appendTo(node);
$button.on('mouseenter', () => {
$tooltip.html(data.tooltip).addClass('visible');
const buttonRect = $button[0].getBoundingClientRect();
const tooltipRect = $tooltip[0].getBoundingClientRect();
const topOffset = buttonRect.bottom + tooltipRect.height < window.innerHeight ?
buttonRect.bottom : // It fits at the top of the question mark - place it there
Math.max(0, buttonRect.top - tooltipRect.height); // Else, place to the bottom of the question mark
const leftOffset = buttonRect.right + tooltipRect.width < window.innerWidth ?
buttonRect.right : // It fits at the right of the question mark - place it there
Math.max(0, buttonRect.left - tooltipRect.width); // Else, place to the left of the question mark
$tooltip.css('top', window.scrollY + topOffset);
$tooltip.css('left', window.scrollX + leftOffset);
}).on('mouseleave', () => {
$tooltip.removeClass('visible');
});
};
// Some utility methods for manipulating quickForms after their creation:
// (None of these work for "dyninput" type fields at present)
/**
* Returns an object containing all filled form data entered by the user, with the object
* keys being the form element names. Disabled fields will be ignored, but not hidden fields.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @return {Object} With field names as keys, input data as values.
*/
Morebits.QuickForm.getInputData = function(form) {
const result = {};
for (let i = 0; i < form.elements.length; i++) {
const field = form.elements[i];
if (field.disabled || !field.name || !field.type ||
field.type === 'submit' || field.type === 'button') {
continue;
}
// For elements in subgroups, quickform prepends element names with
// name of the parent group followed by a period, get rid of that.
const fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1);
switch (field.type) {
case 'radio':
if (field.checked) {
result[fieldNameNorm] = field.value;
}
break;
case 'checkbox':
if (field.dataset.single) {
result[fieldNameNorm] = field.checked; // boolean
} else {
result[fieldNameNorm] = result[fieldNameNorm] || [];
if (field.checked) {
result[fieldNameNorm].push(field.value);
}
}
break;
case 'select-multiple':
result[fieldNameNorm] = $(field).val(); // field.value doesn't work
break;
case 'text': // falls through
case 'textarea':
if (field.dataset.dyninput) {
result[fieldNameNorm] = result[fieldNameNorm] || [];
result[fieldNameNorm].push(field.value.trim());
} else {
result[fieldNameNorm] = field.value.trim();
}
break;
default: // could be select-one, date, number, email, etc
if (field.value) {
result[fieldNameNorm] = field.value;
}
break;
}
}
return result;
};
/**
* Returns all form elements with a given field name or ID.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @param {string} fieldName - The name or id of the fields.
* @return {HTMLElement[]} - Array of matching form elements.
*/
Morebits.QuickForm.getElements = function QuickFormGetElements(form, fieldName) {
const $form = $(form);
fieldName = $.escapeSelector(fieldName); // sanitize input
let $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
return $elements.toArray();
};
/**
* Searches the array of elements for a checkbox or radio button with a certain
* `value` attribute, and returns the first such element. Returns null if not found.
*
* @memberof Morebits.QuickForm
* @param {HTMLInputElement[]} elementArray - Array of checkbox or radio elements.
* @param {string} value - Value to search for.
* @return {HTMLInputElement}
*/
Morebits.QuickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
const found = $.grep(elementArray, (el) => el.value === value);
if (found.length > 0) {
return found[0];
}
return null;
};
/**
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios.
*
* @memberof Morebits.QuickForm
* @param {HTMLElement} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
/**
* Gets the HTML element that contains the label of the given form element
* (mainly for internal use).
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === 'button' || element.type === 'submit' ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName('legend')[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName('h5')[0];
}
// for others, the label is the sibling <label> element
return element.parentNode.getElementsByTagName('label')[0];
};
/**
* Gets the label text of the element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {string}
*/
Morebits.QuickForm.getElementLabel = function QuickFormGetElementLabel(element) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
/**
* Sets the label of the element to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} labelText
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
/**
* Stores the element's current label, and temporarily sets the label to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} temporaryLabelText
* @return {boolean} `true` if succeeded, `false` if the label element is unavailable.
*/
Morebits.QuickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute('data-oldlabel')) {
element.setAttribute('data-oldlabel', Morebits.QuickForm.getElementLabel(element));
}
return Morebits.QuickForm.setElementLabel(element, temporaryLabelText);
};
/**
* Restores the label stored by overrideElementLabel.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute('data-oldlabel')) {
return Morebits.QuickForm.setElementLabel(element, element.getAttribute('data-oldlabel'));
}
return null;
};
/**
* Shows or hides a form element plus its label and tooltip.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery|string)} element - HTML/jQuery element, or jQuery selector string.
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
/**
* Shows or hides the question mark icon (which displays the tooltip) next to a form element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery)} element
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.QuickForm.getElementContainer(element)).find('.morebits-tooltipButton').toggle(visibility);
};
/**
* @external HTMLFormElement
*/
/**
* Get checked items in the form.
*
* @method external:HTMLFormElement.getChecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getChecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (elements.checked) {
return [ elements.value ];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Does the same as {@link HTMLFormElement.getChecked|getChecked}, but with unchecked elements.
*
* @method external:HTMLFormElement.getUnchecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getUnchecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (!options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (!elements.checked) {
return [ elements.value ];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (!elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Utilities to help process IP addresses.
*
* @namespace Morebits.ip
* @memberof Morebits
*/
Morebits.ip = {
/**
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* function from the IPUtils library. Addresses are verbose, uppercase,
* normalized, and expanded to 8 words.
*
* @param {string} address - The IPv6 address, with or without CIDR.
* @return {string}
*/
sanitizeIPv6: function (address) {
address = address.trim();
if (address === '') {
return null;
}
if (!mw.util.isIPv6Address(address, true)) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
const abbrevPos = address.indexOf('::');
if (abbrevPos > -1) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
const CIDRStart = address.indexOf('/');
const addressEnd = CIDRStart !== -1 ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
let repeat, extra, pad;
if (abbrevPos === 0) {
repeat = '0:';
extra = address === '::' ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if (abbrevPos === (addressEnd - 1)) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
let replacement = repeat;
pad -= address.split(':').length - 1;
for (let i = 1; i < pad; i++) {
replacement += repeat;
}
replacement += extra;
address = address.replace('::', replacement);
}
// Remove leading zeros from each bloc as needed
return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2');
},
/**
* Determine if the given IP address is a range. Just conjoins
* `mw.util.isIPAddress` with and without the `allowBlock` option.
*
* @param {string} ip
* @return {boolean} - True if given a valid IP address range, false otherwise.
*/
isRange: function (ip) {
return mw.util.isIPAddress(ip, true) && !mw.util.isIPAddress(ip);
},
/**
* Check that an IP range is within the CIDR limits. Most likely to be useful
* in conjunction with `wgRelevantUserName`. CIDR limits are hardcoded as /16
* for IPv4 and /32 for IPv6.
*
* @return {boolean} - True for valid ranges within the CIDR limits,
* otherwise false (ranges outside the limit, single IPs, non-IPs).
*/
validCIDR: function (ip) {
if (Morebits.ip.isRange(ip)) {
const subnet = parseInt(ip.match(/\/(\d{1,3})$/)[1], 10);
if (subnet) { // Should be redundant
if (mw.util.isIPv6Address(ip, true)) {
if (subnet >= 32) {
return true;
}
} else {
if (subnet >= 16) {
return true;
}
}
}
}
return false;
},
/**
* Get the /64 subnet for an IPv6 address.
*
* @param {string} ipv6 - The IPv6 address, with or without a subnet.
* @return {boolean|string} - False if not IPv6 or bigger than a 64,
* otherwise the (sanitized) /64 address.
*/
get64: function (ipv6) {
if (!ipv6 || !mw.util.isIPv6Address(ipv6, true)) {
return false;
}
const subnetMatch = ipv6.match(/\/(\d{1,3})$/);
if (subnetMatch && parseInt(subnetMatch[1], 10) < 64) {
return false;
}
ipv6 = Morebits.ip.sanitizeIPv6(ipv6);
const ip_re = /^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/;
// eslint-disable-next-line no-useless-concat
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
};
/**
* Helper functions to manipulate strings.
*
* @namespace Morebits.string
* @memberof Morebits
*/
Morebits.string = {
/**
* @param {string} str
* @return {string}
*/
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toUpperCase() + str.slice(1);
},
/**
* @param {string} str
* @return {string}
*/
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toLowerCase() + str.slice(1);
},
/**
* Gives an array of substrings of `str` - starting with `start` and
* ending with `end` - which is not in `skiplist`. Intended for use
* on wikitext with templates or links.
*
* @param {string} str
* @param {string} start
* @param {string} end
* @param {(string[]|string)} [skiplist]
* @return {string[]}
* @throws {Error} If the `start` and `end` strings aren't of the same length.
* @throws {Error} If `skiplist` isn't an array or string
*/
splitWeightedByKeys: function(str, start, end, skiplist) {
if (start.length !== end.length) {
throw new Error('start marker and end marker must be of the same length');
}
let level = 0;
let initial = null;
const result = [];
if (!Array.isArray(skiplist)) {
if (skiplist === undefined) {
skiplist = [];
} else if (typeof skiplist === 'string') {
skiplist = [ skiplist ];
} else {
throw new Error('non-applicable skiplist parameter');
}
}
for (let i = 0; i < str.length; ++i) {
for (let j = 0; j < skiplist.length; ++j) {
if (str.substr(i, skiplist[j].length) === skiplist[j]) {
i += skiplist[j].length - 1;
continue;
}
}
if (str.substr(i, start.length) === start) {
if (initial === null) {
initial = i;
}
++level;
i += start.length - 1;
} else if (str.substr(i, end.length) === end) {
--level;
i += end.length - 1;
}
if (!level && initial !== null) {
result.push(str.substring(initial, i + 1));
initial = null;
}
}
return result;
},
/**
* Formats freeform "reason" (from a textarea) for deletion/other
* templates that are going to be substituted, (e.g. PROD, XFD, RPP).
* Handles `|` outside a nowiki tag.
* Optionally, also adds a signature if not present already.
*
* @param {string} str
* @param {boolean} [addSig]
* @return {string}
*/
formatReasonText: function(str, addSig) {
let reason = (str || '').toString().trim();
const unbinder = new Morebits.Unbinder(reason);
// eslint-disable-next-line no-useless-concat
unbinder.unbind('<no' + 'wiki>', '</no' + 'wiki>');
unbinder.content = unbinder.content.replace(/\|/g, '{{subst:!}}');
reason = unbinder.rebind();
if (addSig) {
const sig = '~~~~', sigIndex = reason.lastIndexOf(sig);
if (sigIndex === -1 || sigIndex !== reason.length - sig.length) {
reason += ' ' + sig;
}
}
return reason.trim();
},
/**
* Formats a "reason" (from a textarea) for inclusion in a userspace
* log. Replaces newlines with {{Pb}}, and adds an extra `#` before
* list items for proper formatting.
*
* @param {string} str
* @return {string}
*/
formatReasonForLog: function(str) {
return str
// handle line breaks, which otherwise break numbering
.replace(/\n+/g, '{{pb}}')
// put an extra # in front before bulleted or numbered list items
.replace(/^(#+)/mg, '#$1')
.replace(/^(\*+)/mg, '#$1');
},
/**
* Like `String.prototype.replace()`, but escapes any dollar signs in
* the replacement string. Useful when the the replacement string is
* arbitrary, such as a username or freeform user input, and could
* contain dollar signs.
*
* @param {string} string - Text in which to replace.
* @param {(string|RegExp)} pattern
* @param {string} replacement
* @return {string}
*/
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) {
return string.replace(pattern, replacement.replace(/\$/g, '$$$$'));
},
/**
* Determine if the user-provided expiration will be considered an
* infinite-length by MW.
*
* @see {@link https://phabricator.wikimedia.org/T68646}
*
* @param {string} expiry
* @return {boolean}
*/
isInfinity: function morebitsStringIsInfinity(expiry) {
return ['indefinite', 'infinity', 'infinite', 'never'].includes(expiry);
},
/**
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
*
* @param {string} text - String to be escaped.
* @return {string} - The escaped text.
*/
escapeRegExp: function(text) {
return mw.util.escapeRegExp(text).replace(/ |_/g, '[_ ]');
}
};
/**
* Helper functions to manipulate arrays.
*
* @namespace Morebits.array
* @memberof Morebits
*/
Morebits.array = {
/**
* Remove duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with duplicates removed.
* @throws {Error} When provided a non-array.
*/
uniq: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.uniq');
}
return arr.filter((item, idx) => arr.indexOf(item) === idx);
},
/**
* Remove non-duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain.
* @throws {Error} When provided a non-array.
*/
dups: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.dups');
}
return arr.filter((item, idx) => arr.indexOf(item) !== idx);
},
/**
* Break up an array into smaller arrays.
*
* @param {Array} arr
* @param {number} size - Size of each chunk (except the last, which could be different).
* @return {Array[]} An array containing the smaller, chunked arrays.
* @throws {Error} When provided a non-array.
*/
chunk: function(arr, size) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.chunk');
}
if (typeof size !== 'number' || size <= 0) { // pretty impossible to do anything :)
return [ arr ]; // we return an array consisting of this array.
}
const numChunks = Math.ceil(arr.length / size);
const result = new Array(numChunks);
for (let i = 0; i < numChunks; i++) {
result[i] = arr.slice(i * size, (i + 1) * size);
}
return result;
}
};
/**
* Utilities to enhance select2 menus. See twinklewarn, twinklexfd,
* twinkleblock for sample usages.
*
* @see {@link https://select2.org/}
*
* @namespace Morebits.select2
* @memberof Morebits
* @requires jQuery.select2
*/
Morebits.select2 = {
matchers: {
/**
* Custom matcher in which if the optgroup name matches, all options in that
* group are shown, like in jquery.chosen.
*/
optgroupFull: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (result && params.term &&
data.text.toUpperCase().includes(params.term.toUpperCase())) {
result.children = data.children;
}
return result;
},
/** Custom matcher that matches from the beginning of words only. */
wordBeginning: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (!params.term || (result &&
new RegExp('\\b' + mw.util.escapeRegExp(params.term), 'i').test(result.text))) {
return result;
}
return null;
}
},
/** Underline matched part of options. */
highlightSearchMatches: function(data) {
const searchTerm = Morebits.select2SearchQuery;
if (!searchTerm || data.loading) {
return data.text;
}
const idx = data.text.toUpperCase().indexOf(searchTerm.toUpperCase());
if (idx < 0) {
return data.text;
}
return $('<span>').append(
data.text.slice(0, idx),
$('<span>').css('text-decoration', 'underline').text(data.text.slice(idx, idx + searchTerm.length)),
data.text.slice(idx + searchTerm.length)
);
},
/** Intercept query as it is happening, for use in highlightSearchMatches. */
queryInterceptor: function(params) {
Morebits.select2SearchQuery = params && params.term;
},
/**
* Open dropdown and begin search when the `.select2-selection` has
* focus and a key is pressed.
*
* @see {@link https://github.com/select2/select2/issues/3279#issuecomment-442524147}
*/
autoStart: function(ev) {
if (ev.which < 48) {
return;
}
let $target = $(ev.target).closest('.select2-container');
if (!$target.length) {
return;
}
$target = $target.prev();
$target.select2('open');
const search = $target.data('select2').dropdown.$search ||
$target.data('select2').selection.$search;
// Use DOM .focus() to work around a jQuery 3.6.0 regression (https://github.com/select2/select2/issues/5993)
search[0].focus();
}
};
/**
* Temporarily hide a part of a string while processing the rest of it.
* Used by {@link Morebits.wikitext.Page#commentOutImage|Morebits.wikitext.Page.commentOutImage}.
*
* @memberof Morebits
* @class
* @param {string} string - The initial text to process.
* @example var u = new Morebits.Unbinder('Hello world <!-- world --> world');
* u.unbind('<!--', '-->'); // text inside comment remains intact
* u.content = u.content.replace(/world/g, 'earth');
* u.rebind(); // gives 'Hello earth <!-- world --> earth'
*/
Morebits.Unbinder = function Unbinder(string) {
if (typeof string !== 'string') {
throw new Error('not a string');
}
/** The text being processed. */
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
};
Morebits.Unbinder.prototype = {
/**
* Hide the region encapsulated by the `prefix` and `postfix` from
* string processing. `prefix` and `postfix` will be used in a
* RegExp, so items that need escaping should be use `\\`.
*
* @param {string} prefix
* @param {string} postfix
* @throws {Error} If either `prefix` or `postfix` is missing.
*/
unbind: function UnbinderUnbind(prefix, postfix) {
if (!prefix || !postfix) {
throw new Error('Both prefix and postfix must be provided');
}
const re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');
this.content = this.content.replace(re, Morebits.Unbinder.getCallback(this));
},
/**
* Restore the hidden portion of the `content` string.
*
* @return {string} The processed output.
*/
rebind: function UnbinderRebind() {
let content = this.content;
content.self = this;
for (const current in this.history) {
if (Object.prototype.hasOwnProperty.call(this.history, current)) {
content = content.replace(current, this.history[current]);
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
/** @memberof Morebits.Unbinder */
Morebits.Unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback(match) {
const current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/* **************** Morebits.Date **************** */
/**
* Create a date object with enhanced processing capabilities, a la
* {@link https://momentjs.com/|moment.js}. MediaWiki timestamp format is also
* acceptable, in addition to everything that JS Date() accepts.
*
* @memberof Morebits
* @class
*/
Morebits.Date = class extends Date {
constructor(...args) {
// Check MediaWiki formats
// Must be first since firefox erroneously accepts the timestamp
// format, sans timezone (See also: #921, #936, #1174, #1187), and the
// 14-digit string will be interpreted differently.
if (args.length === 1) {
const param = args[0];
if (/^\d{14}$/.test(param)) {
// YYYYMMDDHHmmss
const digitMatch = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);
if (digitMatch) {
// ..... year ... month .. date ... hour .... minute ..... second
super(Date.UTC(digitMatch[1], digitMatch[2] - 1, digitMatch[3], digitMatch[4], digitMatch[5], digitMatch[6]));
} else {
super(...args);
}
} else if (typeof param === 'string') {
// Wikitext signature timestamp
const dateParts = Morebits.l10n.signatureTimestampFormat(param);
if (dateParts) {
super(Date.UTC(...dateParts));
} else {
super(...args);
}
} else {
super(...args);
}
} else {
super(...args);
}
if (!this.isValid()) {
mw.log.warn('Invalid Morebits.Date initialisation:', args);
}
}
/** @return {boolean} */
isValid() {
return !isNaN(this.getTime());
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isBefore(date) {
return this.getTime() < date.getTime();
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isAfter(date) {
return this.getTime() > date.getTime();
}
/** @return {string} */
getUTCMonthName() {
return Morebits.Date.localeData.months[this.getUTCMonth()];
}
/** @return {string} */
getUTCMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getUTCMonth()];
}
/** @return {string} */
getMonthName() {
return Morebits.Date.localeData.months[this.getMonth()];
}
/** @return {string} */
getMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getMonth()];
}
/** @return {string} */
getUTCDayName() {
return Morebits.Date.localeData.days[this.getUTCDay()];
}
/** @return {string} */
getUTCDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getUTCDay()];
}
/** @return {string} */
getDayName() {
return Morebits.Date.localeData.days[this.getDay()];
}
/** @return {string} */
getDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getDay()];
}
/**
* Add a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
add(number, unit) {
let num = parseInt(number, 10); // normalize
if (isNaN(num)) {
throw new Error('Invalid number "' + number + '" provided.');
}
unit = unit.toLowerCase(); // normalize
const unitMap = Morebits.Date.unitMap;
let unitNorm = unitMap[unit] || unitMap[unit + 's']; // so that both singular and plural forms work
if (unitNorm) {
// No built-in week functions, so rather than build out ISO's getWeek/setWeek, just multiply
// Probably can't be used for Julian->Gregorian changeovers, etc.
if (unitNorm === 'Week') {
unitNorm = 'Date';
num *= 7;
}
this['set' + unitNorm](this['get' + unitNorm]() + num);
return this;
}
throw new Error('Invalid unit "' + unit + '": Only ' + Object.keys(unitMap).join(', ') + ' are allowed.');
}
/**
* Subtracts a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
subtract(number, unit) {
return this.add(-number, unit);
}
/**
* Format the date into a string per the given format string.
* Replacement syntax is a subset of that in moment.js:
*
* | Syntax | Output |
* |--------|--------|
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | m | Minutes |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment, 3 digits |
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | D | Date |
* | DD | Date (padded to 2 digits) |
* | M | Month number (1-indexed) |
* | MM | Month number (1-indexed, padded to 2 digits) |
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
* | Y | Year |
* | YY | Final two digits of year (20 for 2020, 42 for 1942) |
* | YYYY | Year (same as `Y`) |
*
* @param {string} formatstr - Format the date into a string, using
* the replacement syntax. Use `[` and `]` to escape items. If not
* provided, will return the ISO-8601-formatted string.
* @param {(string|number)} [zone=system] - `system` (for browser-default time zone),
* `utc`, or specify a time zone as number of minutes relative to UTC.
* @return {string}
*/
format(formatstr, zone) {
if (!this.isValid()) {
return 'Invalid date'; // Put the truth out, preferable to "NaNNaNNan NaN:NaN" or whatever
}
let udate = this;
// create a new date object that will contain the date to display as system time
if (zone === 'utc') {
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset(), 'minutes');
} else if (typeof zone === 'number') {
// convert to utc, then add the utc offset given
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset() + zone, 'minutes');
}
// default to ISOString
if (!formatstr) {
return udate.toISOString();
}
const pad = function(num, len) {
len = len || 2; // Up to length of 00 + 1
return ('00' + num).toString().slice(0 - len);
};
const h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
const D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
const h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? 'PM' : 'AM';
const replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
mm: pad(m), m: m,
ss: pad(s), s: s,
SSS: pad(ms, 3),
dddd: udate.getDayName(), ddd: udate.getDayNameAbbrev(), d: udate.getDay(),
DD: pad(D), D: D,
MMMM: udate.getMonthName(), MMM: udate.getMonthNameAbbrev(), MM: pad(M), M: M,
YYYY: Y, YY: pad(Y % 100), Y: Y
};
const unbinder = new Morebits.Unbinder(formatstr); // escape stuff between [...]
unbinder.unbind('\\[', '\\]');
unbinder.content = unbinder.content.replace(
/* Regex notes:
* d(d{2,3})? matches exactly 1, 3 or 4 occurrences of 'd' ('dd' is treated as a double match of 'd')
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y'
*/
/H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,
(match) => replacementMap[match]
);
return unbinder.rebind().replace(/\[(.*?)\]/g, '$1');
}
/**
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM".
* Similar to `calendar` in moment.js, but with time zone support.
*
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone),
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC.
* @return {string}
*/
calendar(zone) {
// Zero out the hours, minutes, seconds and milliseconds - keeping only the date;
// find the difference. Note that setHours() returns the same thing as getTime().
const dateDiff = (new Date().setHours(0, 0, 0, 0) -
new Date(this).setHours(0, 0, 0, 0)) / 8.64e7;
switch (true) {
case dateDiff === 0:
return this.format(Morebits.Date.localeData.relativeTimes.thisDay, zone);
case dateDiff === 1:
return this.format(Morebits.Date.localeData.relativeTimes.prevDay, zone);
case dateDiff > 0 && dateDiff < 7:
return this.format(Morebits.Date.localeData.relativeTimes.pastWeek, zone);
case dateDiff === -1:
return this.format(Morebits.Date.localeData.relativeTimes.nextDay, zone);
case dateDiff < 0 && dateDiff > -7:
return this.format(Morebits.Date.localeData.relativeTimes.thisWeek, zone);
default:
return this.format(Morebits.Date.localeData.relativeTimes.other, zone);
}
}
/**
* Get a regular expression that matches wikitext section titles, such
* as `==December 2019==` or `=== Jan 2018 ===`.
*
* @return {RegExp}
*/
monthHeaderRegex() {
return new RegExp('^(==+)\\s*(?:' + this.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() +
')\\s+' + this.getUTCFullYear() + '\\s*\\1', 'mg');
}
/**
* Creates a wikitext section header with the month and year.
*
* @param {number} [level=2] - Header level. Pass 0 for just the text
* with no wikitext markers (==).
* @return {string}
*/
monthHeader(level) {
// Default to 2, but allow for 0 or stringy numbers
level = parseInt(level, 10);
level = isNaN(level) ? 2 : level;
const header = '='.repeat(level);
const text = this.getUTCMonthName() + ' ' + this.getUTCFullYear();
if (header.length) { // wikitext-formatted header
return header + ' ' + text + ' ' + header;
}
return text; // Just the string
}
};
/**
* Localized strings for date processing.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string[]} months
* @property {string[]} monthsShort
* @property {string[]} days
* @property {string[]} daysShort
* @property {object.<string, string>} relativeTimes
* @private
*/
Morebits.Date.localeData = {
// message names here correspond to MediaWiki message names
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
relativeTimes: {
thisDay: '[Today at] h:mm A',
prevDay: '[Yesterday at] h:mm A',
nextDay: '[Tomorrow at] h:mm A',
thisWeek: 'dddd [at] h:mm A',
pastWeek: '[Last] dddd [at] h:mm A',
other: 'YYYY-MM-DD'
}
};
/**
* Map units with getter/setter function names, for `add` and `subtract`
* methods.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string} seconds
* @property {string} minutes
* @property {string} hours
* @property {string} days
* @property {string} weeks
* @property {string} months
* @property {string} years
*/
Morebits.Date.unitMap = {
seconds: 'Seconds',
minutes: 'Minutes',
hours: 'Hours',
days: 'Date',
weeks: 'Week', // Not a function but handled in `add` through cunning use of multiplication
months: 'Month',
years: 'FullYear'
};
/* **************** Morebits.wiki **************** */
/**
* Various objects for wiki editing and API access, including
* {@link Morebits.wiki.Api} and {@link Morebits.wiki.Page}.
*
* @namespace Morebits.wiki
* @memberof Morebits
*/
Morebits.wiki = {};
/* **************** Morebits.wiki.actionCompleted **************** */
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.numberOfActionsLeft = 0;
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.nbrOfCheckpointsLeft = 0;
/**
* Display message and/or redirect to page upon completion of tasks.
*
* Every call to Morebits.wiki.Api.post() results in the dispatch of an
* asynchronous callback. Each callback can in turn make an additional call to
* Morebits.wiki.Api.post() to continue a processing sequence. At the
* conclusion of the final callback of a processing sequence, it is not
* possible to simply return to the original caller because there is no call
* stack leading back to the original context. Instead,
* Morebits.wiki.actionCompleted.event() is called to display the result to
* the user and to perform an optional page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event() is
* managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.Api call and decremented
* after the completion of a callback function. If a callback function does
* not create a new Morebits.wiki.Api object before exiting, it is the final
* step in the processing chain and Morebits.wiki.actionCompleted.event() will
* then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback
* function. This is used for batch operations. The end of a batch is
* signaled by calling Morebits.wiki.removeCheckpoint().
*
* @memberof Morebits.wiki
*/
Morebits.wiki.actionCompleted = function(self) {
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) {
Morebits.wiki.actionCompleted.event(self);
}
};
// Change per action wanted
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.event = function() {
if (Morebits.wiki.actionCompleted.notice) {
Morebits.Status.actionCompleted(Morebits.wiki.actionCompleted.notice);
}
if (Morebits.wiki.actionCompleted.redirect) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if (!(/^\w+:\/\//).test(Morebits.wiki.actionCompleted.redirect)) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);
if (Morebits.wiki.actionCompleted.followRedirect === false) {
Morebits.wiki.actionCompleted.redirect += '?redirect=no';
}
}
window.setTimeout(() => {
window.location = Morebits.wiki.actionCompleted.redirect;
}, Morebits.wiki.actionCompleted.timeOut);
}
};
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.redirect = null;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.notice = null;
/** @memberof Morebits.wiki */
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
/** @memberof Morebits.wiki */
Morebits.wiki.removeCheckpoint = function() {
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) {
Morebits.wiki.actionCompleted.event();
}
};
/* **************** Morebits.wiki.Api **************** */
/**
* An easy way to talk to the MediaWiki API. Accepts either json or xml
* (default) formats; if json is selected, will default to `formatversion=2`
* unless otherwise specified. Similarly, enforces newer `errorformat`s,
* defaulting to `html` if unspecified. `uselang` enforced to the wiki's
* content language.
*
* In new code, the use of the last 3 parameters should be avoided, instead
* use {@link Morebits.wiki.Api#setStatusElement|setStatusElement()} to bind
* the status element (if needed) and use `.then()` or `.catch()` on the
* promise returned by `post()`, rather than specify the `onSuccess` or
* `onFailure` callbacks.
*
* @memberof Morebits.wiki
* @class
* @param {string} currentAction - The current action (required).
* @param {Object} query - The query (required).
* @param {Function} [onSuccess] - The function to call when request is successful.
* @param {Morebits.Status} [statusElement] - A Morebits.Status object to use for status messages.
* @param {Function} [onError] - The function to call if an error occurs.
*/
Morebits.wiki.Api = function(currentAction, query, onSuccess, statusElement, onError) {
this.currentAction = currentAction;
this.query = query;
this.query.assert = 'user';
// Enforce newer error formats, preferring html
if (!query.errorformat || !['wikitext', 'plaintext'].includes(query.errorformat)) {
this.query.errorformat = 'html';
}
// Explicitly use the wiki's content language to minimize confusion,
// see #1179 for discussion
this.query.uselang = 'content';
this.query.errorlang = 'uselang';
this.query.errorsuselocal = 1;
this.onSuccess = onSuccess;
this.onError = onError;
if (statusElement) {
this.setStatusElement(statusElement);
} else {
this.statelem = new Morebits.Status(currentAction);
}
// JSON is used throughout Morebits/Twinkle, but xml remains the default for backwards compatibility
if (!query.format) {
this.query.format = 'xml';
} else if (query.format === 'json' && !query.formatversion) {
this.query.formatversion = '2';
} else if (!['xml', 'json'].includes(query.format)) {
this.statelem.error('Invalid API format: only xml and json are supported.');
}
// Ignore tags for queries and most common unsupported actions, produces warnings
if (query.action && ['query', 'review', 'stabilize', 'watch'].includes(query.action)) {
delete query.tags;
}
};
Morebits.wiki.Api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
response: null,
responseXML: null, // use `response` instead; retained for backwards compatibility
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error
/**
* Keep track of parent object for callbacks.
*
* @param {*} parent
*/
setParent: function(parent) {
this.parent = parent;
},
/** @param {Morebits.Status} statusElement */
setStatusElement: function(statusElement) {
this.statelem = statusElement;
this.statelem.status(this.currentAction);
},
/**
* Carry out the request.
*
* @param {Object} callerAjaxParameters - Do not specify a parameter unless you really
* really want to give jQuery some extra parameters.
* @return {jQuery.Promise} - A jQuery promise object that is resolved or rejected with the api object.
*/
post: function(callerAjaxParameters) {
++Morebits.wiki.numberOfActionsLeft;
const queryString = $.map(this.query, (val, i) => {
if (Array.isArray(val)) {
return encodeURIComponent(i) + '=' + val.map(encodeURIComponent).join('|');
} else if (val !== undefined) {
return encodeURIComponent(i) + '=' + encodeURIComponent(val);
}
}).join('&').replace(/^(.*?)(\btoken=[^&]*)&(.*)/, '$1$3&$2');
// token should always be the last item in the query string (bug TW-B-0013)
const headers = {
'Api-User-Agent': morebitsWikiApiUserAgent
};
if (this.query.action === 'parse') {
// Per https://www.mediawiki.org/wiki/API:Etiquette
headers['Promise-Non-Write-API-Action'] = 'true';
}
const ajaxparams = $.extend({}, {
context: this,
type: this.query.action === 'query' ? 'GET' : 'POST',
url: mw.util.wikiScript('api'),
data: queryString,
dataType: this.query.format,
headers
}, callerAjaxParameters);
return $.ajax(ajaxparams).then(
function onAPIsuccess(response, statusText) {
this.statusText = statusText;
this.response = this.responseXML = response;
// Limit to first error
if (this.query.format === 'json') {
this.errorCode = response.errors && response.errors[0].code;
if (this.query.errorformat === 'html') {
this.errorText = response.errors && response.errors[0].html;
} else if (this.query.errorformat === 'wikitext' || this.query.errorformat === 'plaintext') {
this.errorText = response.errors && response.errors[0].text;
}
} else {
this.errorCode = $(response).find('errors error').eq(0).attr('code');
// Sufficient for html, wikitext, or plaintext errorformats
this.errorText = $(response).find('errors error').eq(0).text();
}
if (typeof this.errorCode === 'string') {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
return this.returnError(callerAjaxParameters);
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call(this.parent, this);
} else {
this.statelem.info('done');
}
Morebits.wiki.actionCompleted();
return $.Deferred().resolveWith(this.parent, [this]);
},
// only network and server errors reach here - complaints from the API itself are caught in success()
function onAPIfailure(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
return this.returnError();
}
);
},
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn('Invalid token. Getting a new token and retrying...');
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
// type of action than CSRF, we do one pointless retry before bailing out
return Morebits.wiki.Api.getToken().then((token) => {
this.query.token = token;
return this.post(callerAjaxParameters);
});
}
this.statelem.error(this.errorText + ' (' + this.errorCode + ')');
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call(this.parent, this);
}
// don't complete the action so that the error remains displayed
return $.Deferred().rejectWith(this.parent, [this]);
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() { // retained for backwards compatibility, use getResponse() instead
return this.responseXML;
},
getResponse: function() {
return this.response;
}
};
/**
* Retrieves wikitext from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string|null>} Returns page content, or null if the page doesn't exist.
*/
Morebits.wiki.getCachedPage = function(title) {
return new mw.Api({ userAgent: morebitsWikiApiUserAgent }).get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: '*',
format: 'json',
formatversion: '2',
smaxage: '86400', // cache for 1 day
maxage: '86400', // cache for 1 day
uselang: 'content'
}).then((data) => {
const page = data.query.pages[0];
if (page.missing) {
return null;
}
return page.revisions[0].slots.main.content;
});
};
/**
* Retrieves JSON from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string>}
*/
Morebits.wiki.getCachedJson = function(title) {
return Morebits.wiki.getCachedPage(title).then((wikitext) => JSON.parse(wikitext));
};
var morebitsWikiApiUserAgent = 'morebits.js ([[w:WT:TW]])';
/**
* Set the custom user agent header, which is used for server-side logging.
* Note that doing so will set the useragent for every `Morebits.wiki.Api`
* process performed thereafter.
*
* @see {@link https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html}
* for original announcement.
*
* @memberof Morebits.wiki.Api
* @param {string} [ua=morebits.js ([[w:WT:TW]])] - User agent. The default
* value of `morebits.js ([[w:WT:TW]])` will be appended to any provided
* value.
*/
Morebits.wiki.Api.setApiUserAgent = function(ua) {
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js ([[w:WT:TW]])';
};
/**
* Get a new CSRF token on encountering token errors.
*
* @memberof Morebits.wiki.Api
* @return {string} MediaWiki CSRF token.
*/
Morebits.wiki.Api.getToken = function() {
const tokenApi = new Morebits.wiki.Api('Getting token', {
action: 'query',
meta: 'tokens',
type: 'csrf',
format: 'json'
});
return tokenApi.post().then((apiobj) => apiobj.response.query.tokens.csrftoken);
};
/* **************** Morebits.wiki.Page **************** */
/**
* Use the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set {@link Morebits.wiki.actionCompleted.notice} and {@link Morebits.wiki.actionCompleted.redirect}
* before the first call to {@link Morebits.wiki.Page.load()}.
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.Page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.Page prototype method using this reference.
*
*
* Call sequence for common operations (optional final user callbacks not shown):
*
* - Edit current contents of a page (no edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Edit current contents of a page (with edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnSaveError() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Append to a page (similar for prepend and newSection):
* `.append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> ctx.saveApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* Notes:
* 1. All functions following Morebits.wiki.Api.post() are invoked asynchronously from the jQuery AJAX library.
* 2. The sequence for append/prepend/newSection could be slightly shortened,
* but it would require significant duplication of code for little benefit.
*
* @memberof Morebits.wiki
* @class
* @param {string} pageName - The name of the page, prefixed by the namespace (if any).
* For the current page, use `mw.config.get('wgPageName')`.
* @param {string|Morebits.Status} [status] - A string describing the action about to be undertaken,
* or a Morebits.Status object
*/
Morebits.wiki.Page = function(pageName, status) {
if (!status) {
status = 'Opening page "' + pageName + '"';
}
/**
* Private context variables.
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*
* @private
*/
const ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
changeTags: null,
testActions: null, // array if any valid actions
callbackParameters: null,
statusElement: status instanceof Morebits.Status ? status : new Morebits.Status(status),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
newSectionText: null,
newSectionTitle: null,
createOption: null,
minorEdit: false,
botEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
followCrossNsRedirect: true,
watchlistOption: 'nochange',
watchlistExpiry: null,
discussionToolsAutoSubscribe: null,
creator: null,
timestamp: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: null,
// - delete
deleteTalkPage: false,
// - undelete
undeleteTalkPage: false,
// - creation lookup
lookupNonRedirectCreator: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
csrfToken: null,
loadTime: null,
lastEditTime: null,
pageID: null,
contentModel: null,
revertCurID: null,
revertUser: null,
watched: false,
fullyProtected: false,
suppressProtectWarning: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreationSuccess: null,
onLookupCreationFailure: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onUndeleteSuccess: null,
onUndeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
saveApi: null,
lookupCreationApi: null,
moveApi: null,
moveProcessApi: null,
patrolApi: null,
patrolProcessApi: null,
triageApi: null,
triageProcessListApi: null,
triageProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
undeleteApi: null,
undeleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
const emptyFunction = function() { };
/**
* Loads the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to load()!');
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
inprop: 'watched',
intestactions: 'edit', // can be expanded
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content|timestamp'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvprop = 'timestamp';
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsSysop) {
ctx.loadQuery.inprop += '|protection';
}
ctx.loadApi = new Morebits.wiki.Api('Retrieving page...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
/**
* Saves the text for the page to Wikipedia.
* Must be preceded by successfully calling `load()`.
*
* Warning: Calling `save()` can result in additional calls to the
* previous `load()` callbacks to recover from edit conflicts! In this
* case, callers must make the same edit to the new pageText and
* re-invoke `save()`. This behavior can be disabled with
* `setMaxConflictRetries(0)`.
*
* @param {Function} [onSuccess] - Callback function which is called when the save has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the save fails.
*/
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
// are we getting our editing token from mw.user.tokens?
const canUseMwUserToken = fnCanUseMwUserToken('edit');
if (!ctx.pageLoaded && !canUseMwUserToken) {
ctx.statusElement.error('Internal error: attempt to save a page that has not been loaded!');
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
// new section mode allows (nay, encourages) using the
// title as the edit summary, but the query needs
// editSummary to be undefined or '', not null
if (ctx.editMode === 'new' && ctx.newSectionTitle) {
ctx.editSummary = '';
} else {
ctx.statusElement.error('Internal error: edit summary not set before save!');
ctx.onSaveFailure(this);
return;
}
}
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm(
ctx.fullyProtected === 'infinity' ?
'You are about to make an edit to the fully protected page "' + ctx.pageName + '" (protected indefinitely). \n\nClick OK to proceed with the edit, or Cancel to skip this edit.' :
'You are about to make an edit to the fully protected page "' + ctx.pageName +
'" (protection expiring ' + new Morebits.Date(ctx.fullyProtected).calendar('utc') + ' (UTC)). \n\nClick OK to proceed with the edit, or Cancel to skip this edit.'
)
) {
ctx.statusElement.error('Edit to fully protected page was aborted.');
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
const query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
// Set bot edit attribute. If this parameter is present with any value, it is interpreted as true
if (ctx.botEdit) {
query.bot = true;
}
if (ctx.discussionToolsAutoSubscribe !== null) {
query.discussiontoolsautosubscribe = ctx.discussionToolsAutoSubscribe ? 'yes' : 'no';
}
switch (ctx.editMode) {
case 'append':
if (ctx.appendText === null) {
ctx.statusElement.error('Internal error: append text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
if (ctx.prependText === null) {
ctx.statusElement.error('Internal error: prepend text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'new':
if (!ctx.newSectionText) { // API doesn't allow empty new section text
ctx.statusElement.error('Internal error: new section text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.section = 'new';
query.text = ctx.newSectionText; // add a new section to current page
query.sectiontitle = ctx.newSectionTitle || ctx.editSummary; // done by the API, but non-'' values would get treated as text
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default: // 'all'
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].includes(ctx.createOption)) {
query[ctx.createOption] = '';
}
if (canUseMwUserToken && ctx.followRedirect) {
query.redirect = true;
}
ctx.saveApi = new Morebits.wiki.Api('Saving page...', query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
/**
* Adds the text provided via `setAppendText()` to the end of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Adds the text provided via `setPrependText()` to the start of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Creates a new section with the text provided by `setNewSectionText()`
* and section title from `setNewSectionTitle()`.
* If `editSummary` is provided, that will be used instead of the
* autogenerated "->Title (new section" edit summary.
* Does not require calling `load()` first, unless a watchlist expiry
* is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.newSection = function(onSuccess, onFailure) {
ctx.editMode = 'new';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/** @return {string} The name of the loaded page, including the namespace */
this.getPageName = function() {
return ctx.pageName;
};
/** @return {string} The text of the page after a successful load() */
this.getPageText = function() {
return ctx.pageText;
};
/** @param {string} pageText - Updated page text that will be saved when `save()` is called */
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
/** @param {string} appendText - Text that will be appended to the page when `append()` is called */
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
/** @param {string} prependText - Text that will be prepended to the page when `prepend()` is called */
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
/** @param {string} newSectionText - Text that will be added in a new section on the page when `newSection()` is called */
this.setNewSectionText = function(newSectionText) {
ctx.editMode = 'new';
ctx.newSectionText = newSectionText;
};
/**
* @param {string} newSectionTitle - Title for the new section created when `newSection()` is called
* If missing, `ctx.editSummary` will be used. Issues may occur if a substituted template is used.
*/
this.setNewSectionTitle = function(newSectionTitle) {
ctx.editMode = 'new';
ctx.newSectionTitle = newSectionTitle;
};
// Edit-related setter methods:
/**
* Set the edit summary that will be used when `save()` is called.
* Unnecessary if editMode is 'new' and newSectionTitle is provided.
*
* @param {string} summary
*/
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
/**
* Set any custom tag(s) to be applied to the API action.
* A number of actions don't support it, most notably watch, review,
* and stabilize ({@link https://phabricator.wikimedia.org/T247721|T247721}).
*
* @param {string|string[]} tags - String or array of tag(s).
*/
this.setChangeTags = function(tags) {
ctx.changeTags = tags;
};
/**
* @param {string} [createOption=null] - Can take the following four values:
* - recreate: create the page if it does not exist, or edit it if it exists.
* - createonly: create the page if it does not exist, but return an
* error if it already exists.
* - nocreate: don't create the page, only edit it if it already exists.
* - `null`: create the page if it does not exist, unless it was deleted
* in the moment between loading the page and saving the edit (default).
*/
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
/** @param {boolean} minorEdit - Set true to mark the edit as a minor edit. */
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
/** @param {boolean} botEdit - Set true to mark the edit as a bot edit */
this.setBotEdit = function(botEdit) {
ctx.botEdit = botEdit;
};
/**
* @param {number} pageSection - Integer specifying the section number to load or save.
* If specified as `null`, the entire page will be retrieved.
*/
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
/**
* @param {number} maxConflictRetries - Number of retries for save errors involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxConflictRetries = function(maxConflictRetries) {
ctx.maxConflictRetries = maxConflictRetries;
};
/**
* @param {number} maxRetries - Number of retries for save errors not involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
/**
* Set whether and how to watch the page, including setting an expiry.
*
* @param {boolean|string|Morebits.Date|Date} [watchlistOption=false] -
* Basically a mix of MW API and Twinkley options available pre-expiry:
* - `true`|`'yes'`|`'watch'`: page will be added to the user's
* watchlist when the action is called. Defaults to an indefinite
* watch unless `watchlistExpiry` is provided.
* - `false`|`'no'`|`'nochange'`: watchlist status of the page (including expiry) will not be changed.
* - `'default'`|`'preferences'`: watchlist status of the page will be
* set based on the user's preference settings when the action is
* called. Defaults to an indefinite watch unless `watchlistExpiry` is
* provided.
* - `'unwatch'`: explicitly unwatch the page.
* - Any other `string` or `number`, or a `Morebits.Date` or `Date`
* object: watch page until the specified time, deferring to
* `watchlistExpiry` if provided.
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlist = function(watchlistOption, watchlistExpiry) {
if (watchlistOption instanceof Morebits.Date || watchlistOption instanceof Date) {
watchlistOption = watchlistOption.toISOString();
}
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
switch (watchlistOption) {
case 'nochange':
case 'no':
case false:
case undefined:
ctx.watchlistOption = 'nochange';
// The MW API allows for changing expiry with nochange (as "nochange" refers to the binary status),
// but by keeping this null it will default to any existing expiry, ensure there is actually "no change."
ctx.watchlistExpiry = null;
break;
case 'unwatch':
// expiry unimportant
ctx.watchlistOption = 'unwatch';
break;
case 'preferences':
case 'default':
ctx.watchlistOption = 'preferences';
// The API allows an expiry here, but there is as of yet (T265716)
// no expiry preference option, so it's a bit devoid of context.
ctx.watchlistExpiry = watchlistExpiry;
break;
case 'watch':
case 'yes':
case true:
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistExpiry;
break;
default: // Not really a "default" per se but catches "any other string"
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistOption;
break;
}
};
/**
* Set a watchlist expiry. setWatchlist can mostly handle this by
* itself, so this is here largely for completeness and compatibility
* with the full suite of options.
*
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlistExpiry = function(watchlistExpiry) {
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
ctx.watchlistExpiry = watchlistExpiry;
};
/**
* If editing a talk page, set whether to subscribe to any talk
* page thread created by the edit.
*
* @param {boolean} subscribe
*/
this.setDiscussionToolsAutoSubscribe = function(subscribe) {
ctx.discussionToolsAutoSubscribe = subscribe;
};
/**
* @param {boolean} [followRedirect=false] -
* - `true`: a maximum of one redirect will be followed. In the event
* of a redirect, a message is displayed to the user and the redirect
* target can be retrieved with getPageName().
* - `false`: (default) the requested pageName will be used without regard to any redirect.
* @param {boolean} [followCrossNsRedirect=true] - Not applicable if `followRedirect` is not set true.
* - `true`: (default) follow redirect even if it is a cross-namespace redirect
* - `false`: don't follow redirect if it is cross-namespace, edit the redirect itself.
*/
this.setFollowRedirect = function(followRedirect, followCrossNsRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!');
return;
}
ctx.followRedirect = followRedirect;
ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect;
};
// lookup-creation setter function
/**
* @param {boolean} flag - If set true, the author and timestamp of
* the first non-redirect version of the page is retrieved.
*
* Warning:
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* 3. Must not be used when the page has a non-wikitext contentmodel
* such as Modulespace Lua or user JavaScript/CSS.
*/
this.setLookupNonRedirectCreator = function(flag) {
ctx.lookupNonRedirectCreator = flag;
};
// Move-related setter functions
/** @param {string} destination */
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
/** @param {boolean} flag */
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
/** @param {boolean} flag */
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
/** @param {boolean} flag */
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
// Protect-related setter functions
/**
* @param {string} level - The right required for the specific action
* e.g. autoconfirmed, sysop, templateeditor, extendedconfirmed
* (enWiki-only).
* @param {string} [expiry=infinity]
*/
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry || 'infinity' };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry || 'infinity' };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry || 'infinity' };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.suppressProtectWarning = function() {
ctx.suppressProtectWarning = true;
};
// Delete-related setter
/** @param {boolean} flag */
this.setDeleteTalkPage = function (flag) {
ctx.deleteTalkPage = !!flag;
};
// Undelete-related setter
/** @param {boolean} flag */
this.setUndeleteTalkPage = function (flag) {
ctx.undeleteTalkPage = !!flag;
};
// Revert-related getters/setters:
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
/** @return {string} The current revision ID of the page */
this.getCurrentID = function() {
return ctx.revertCurID;
};
/** @return {string} Last editor of the page */
this.getRevisionUser = function() {
return ctx.revertUser;
};
/** @return {string} ISO 8601 timestamp at which the page was last edited. */
this.getLastEditTime = function() {
return ctx.lastEditTime;
};
// Miscellaneous getters/setters:
/**
* Define an object for use in a callback function.
*
* `callbackParameters` is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback
* function. Callers must ensure that any changes to the
* callbackParameters object within a `load()` callback still permit a
* proper re-entry into the `load()` callback if an edit conflict is
* detected upon calling `save()`.
*
* @param {Object} callbackParameters
*/
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
/**
* @return {Object} - The object previously set by `setCallbackParameters()`.
*/
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
/**
* @param {Morebits.Status} statusElement
*/
this.setStatusElement = function(statusElement) {
ctx.statusElement = statusElement;
};
/**
* @return {Morebits.Status} Status element created by the constructor.
*/
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* @param {string} level - The right required for edits not to require
* review. Possible options: none, autoconfirmed, review (not on enWiki).
* @param {string} [expiry=infinity]
*/
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry || 'infinity' };
};
/**
* @return {boolean} True if the page existed on the wiki when it was last loaded.
*/
this.exists = function() {
return ctx.pageExists;
};
/**
* @return {string} Page ID of the page loaded. 0 if the page doesn't
* exist.
*/
this.getPageID = function() {
return ctx.pageID;
};
/**
* @return {string} - Content model of the page. Possible values
* include (but may not be limited to): `wikitext`, `javascript`,
* `css`, `json`, `Scribunto`, `sanitized-css`, `MassMessageListContent`.
* Also gettable via `mw.config.get('wgPageContentModel')`.
*/
this.getContentModel = function() {
return ctx.contentModel;
};
/**
* @return {boolean|string} - Watched status of the page. Boolean
* unless it's being watched temporarily, in which case returns the
* expiry string.
*/
this.getWatched = function () {
return ctx.watched;
};
/**
* @return {string} ISO 8601 timestamp at which the page was last loaded.
*/
this.getLoadTime = function() {
return ctx.loadTime;
};
/**
* @return {string} The user who created the page following `lookupCreation()`.
*/
this.getCreator = function() {
return ctx.creator;
};
/**
* @return {string} The ISOString timestamp of page creation following `lookupCreation()`.
*/
this.getCreationTimestamp = function() {
return ctx.timestamp;
};
/** @return {boolean} whether or not you can edit the page */
this.canEdit = function() {
return !!ctx.testActions && ctx.testActions.includes('edit');
};
/**
* Retrieves the username of the user who created the page as well as
* the timestamp of creation. The username can be retrieved using the
* `getCreator()` function; the timestamp can be retrieved using the
* `getCreationTimestamp()` function.
* Prior to June 2019 known as `lookupCreator()`.
*
* @param {Function} onSuccess - Callback function to be called when
* the username and timestamp are found within the callback.
* @param {Function} [onFailure] - Callback function to be called when
* the lookup fails
*/
this.lookupCreation = function(onSuccess, onFailure) {
ctx.onLookupCreationSuccess = onSuccess;
ctx.onLookupCreationFailure = onFailure || emptyFunction;
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to lookupCreation()!');
ctx.onLookupCreationFailure(this);
return;
}
const query = {
action: 'query',
prop: 'revisions',
titles: ctx.pageName,
rvlimit: 1,
rvprop: 'user|timestamp',
rvdir: 'newer',
format: 'json'
};
// Only the wikitext content model can reliably handle
// rvsection, others return an error when paired with the
// content rvprop. Relatedly, non-wikitext models don't
// understand the #REDIRECT concept, so we shouldn't attempt
// the redirect resolution in fnLookupCreationSuccess
if (ctx.lookupNonRedirectCreator) {
query.rvsection = 0;
query.rvprop += '|content';
}
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreationApi = new Morebits.wiki.Api('Retrieving page creation information', query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
};
/**
* Reverts a page to `revertOldID` set by `setOldID`.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error('Internal error: revision ID to revert to was not set before revert!');
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
/**
* Moves a page to another title.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'move', ctx.onMoveFailure)) {
return; // abort
}
if (!ctx.moveDestination) {
ctx.statusElement.error('Internal error: destination page name was not set before move!');
ctx.onMoveFailure(this);
return;
}
if (fnCanUseMwUserToken('move')) {
fnProcessMove.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('move');
ctx.moveApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
}
};
/**
* Marks the page as patrolled, using `rcid` (if available) or `revid`.
*
* Patrolling as such doesn't need to rely on loading the page in
* question; simply passing a revid to the API is sufficient, so in
* those cases just using {@link Morebits.wiki.Api} is probably preferable.
*
* No error handling since we don't actually care about the errors.
*/
this.patrol = function() {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If a link is present, don't need to check if it's patrolled
if ($('.patrollink').length) {
const patrolhref = $('.patrollink a').attr('href');
ctx.rcid = mw.util.getParamValue('rcid', patrolhref);
fnProcessPatrol(this, this);
} else {
const patrolQuery = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'patrol', // as long as we're querying, might as well get a token
list: 'recentchanges', // check if the page is unpatrolled
titles: ctx.pageName,
rcprop: 'patrolled',
rctitle: ctx.pageName,
rclimit: 1,
format: 'json'
};
ctx.patrolApi = new Morebits.wiki.Api('retrieving token...', patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
}
};
/**
* Marks the page as reviewed by the PageTriage extension.
*
* Will, by it's nature, mark as patrolled as well. Falls back to
* patrolling if not in an appropriate namespace.
*
* Doesn't inherently rely on loading the page in question; simply
* passing a `pageid` to the API is sufficient, so in those cases just
* using {@link Morebits.wiki.Api} is probably preferable.
*
* Will first check if the page is queued via
* {@link Morebits.wiki.Page~fnProcessTriageList|fnProcessTriageList}.
*
* No error handling since we don't actually care about the errors.
*
* @see {@link https://www.mediawiki.org/wiki/Extension:PageTriage} Referred to as "review" on-wiki.
*/
this.triage = function() {
// Fall back to patrol if not a valid triage namespace
if (!mw.config.get('pageTriageNamespaces').includes(new mw.Title(ctx.pageName).getNamespaceId())) {
this.patrol();
} else {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If on the page in question, don't need to query for page ID
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) {
ctx.pageID = mw.config.get('wgArticleId');
fnProcessTriageList(this, this);
} else {
const query = fnNeedTokenInfoQuery('triage');
ctx.triageApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
}
}
};
// |delete| is a reserved word in some flavours of JS
/**
* Deletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'delete', ctx.onDeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('delete')) {
fnProcessDelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('delete');
ctx.deleteApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
}
};
/**
* Undeletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.undeletePage = function(onSuccess, onFailure) {
ctx.onUndeleteSuccess = onSuccess;
ctx.onUndeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'undelete', ctx.onUndeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('undelete')) {
fnProcessUndelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('undelete');
ctx.undeleteApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
}
};
/**
* Protects a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'protect', ctx.onProtectFailure)) {
return; // abort
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!');
ctx.onProtectFailure(this);
return;
}
// because of the way MW API interprets protection levels
// (absolute, not differential), we always need to request
// protection levels from the server
const query = fnNeedTokenInfoQuery('protect');
ctx.protectApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
/**
* Apply FlaggedRevs protection settings. Only works on wikis where
* the extension is installed (`$wgFlaggedRevsProtection = true`
* i.e. where FlaggedRevs settings appear on the "protect" tab).
*
* @see {@link https://www.mediawiki.org/wiki/Extension:FlaggedRevs}
* Referred to as "pending changes" on-wiki.
*
* @param {Function} [onSuccess]
* @param {Function} [onFailure]
*/
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'FlaggedRevs', ctx.onStabilizeFailure)) {
return; // abort
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!');
ctx.onStabilizeFailure(this);
return;
}
if (fnCanUseMwUserToken('stabilize')) {
fnProcessStabilize.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('stabilize');
ctx.stabilizeApi = new Morebits.wiki.Api('retrieving token...', query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
}
};
/*
* Private member functions
* These are not exposed outside
*/
/**
* Determines whether we can save an API call by using the csrf token
* sent with the page HTML, or whether we need to ask the server for
* more info (e.g. protection or watchlist expiry).
*
* Currently used for `append`, `prepend`, `newSection`, `move`,
* `stabilize`, `deletePage`, and `undeletePage`. Not used for
* `protect` since it always needs to request protection status.
*
* @param {string} [action=edit] - The action being undertaken, e.g.
* "edit" or "delete". In practice, only "edit" or "notedit" matters.
* @return {boolean}
*/
var fnCanUseMwUserToken = function(action = 'edit') {
// If a watchlist expiry is set, we must always load the page
// to avoid overwriting indefinite protection. Of course, not
// needed if setting indefinite watching!
if (ctx.watchlistExpiry && !Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return false;
}
// API-based redirect resolution only works for action=query and
// action=edit in append/prepend/new modes
if (ctx.followRedirect) {
if (!ctx.followCrossNsRedirect) {
return false; // must load the page to check for cross namespace redirects
}
if (action !== 'edit' || (ctx.editMode === 'all' || ctx.editMode === 'revert')) {
return false;
}
}
// do we need to fetch the edit protection expiry?
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) {
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() !== new mw.Title(ctx.pageName).getPrefixedText()) {
return false;
}
// wgRestrictionEdit is null on non-existent pages,
// so this neatly handles nonexistent pages
const editRestriction = mw.config.get('wgRestrictionEdit');
if (!editRestriction || editRestriction.includes('sysop')) {
return false;
}
}
return !!mw.user.tokens.get('csrfToken');
};
/**
* When functions can't use
* {@link Morebits.wiki.Page~fnCanUseMwUserToken|fnCanUseMwUserToken}
* or require checking protection or watched status, maintain the query
* in one place. Used for {@link Morebits.wiki.Page#deletePage|delete},
* {@link Morebits.wiki.Page#undeletePage|undelete},
* {@link Morebits.wiki.Page#protect|protect},
* {@link Morebits.wiki.Page#stabilize|stabilize},
* and {@link Morebits.wiki.Page#move|move}
* (basically, just not {@link Morebits.wiki.Page#load|load}).
*
* @param {string} action - The action being undertaken, e.g. "edit" or
* "delete".
* @return {Object} Appropriate query.
*/
var fnNeedTokenInfoQuery = function(action) {
const query = {
action: 'query',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
prop: 'info',
inprop: 'watched',
format: 'json'
};
// Protection not checked for flagged-revs or non-sysop moves
if (action !== 'stabilize' && (action !== 'move' || Morebits.userIsSysop)) {
query.inprop += '|protection';
}
if (ctx.followRedirect && action !== 'undelete') {
query.redirects = ''; // follow all redirects
}
return query;
};
// callback from loadSuccess() for append(), prepend(), and newSection() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
const response = ctx.loadApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLoadFailure)) {
return; // abort
}
const page = response.pages[0];
let rev;
ctx.pageExists = !page.missing;
if (ctx.pageExists) {
rev = page.revisions[0];
ctx.lastEditTime = rev.timestamp;
ctx.pageText = rev.content;
ctx.pageID = page.pageid;
} else {
ctx.pageText = ''; // allow for concatenation, etc.
ctx.pageID = 0; // nonexistent in response, matches wgArticleId
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
ctx.statusElement.error('Failed to retrieve edit token.');
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = ctx.loadApi.getResponse().curtimestamp;
if (!ctx.loadTime) {
ctx.statusElement.error('Failed to retrieve current timestamp.');
ctx.onLoadFailure(this);
return;
}
ctx.contentModel = page.contentmodel;
ctx.watched = page.watchlistexpiry || page.watched;
// extract protection info, to alert admins when they are about to edit a protected page
// Includes cascading protection
if (Morebits.userIsSysop) {
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
ctx.fullyProtected = editProt.expiry;
} else {
ctx.fullyProtected = false;
}
}
ctx.revertCurID = page.lastrevid;
const testactions = page.actions;
ctx.testActions = []; // was null
Object.keys(testactions).forEach((action) => {
if (testactions[action]) {
ctx.testActions.push(action);
}
});
if (ctx.editMode === 'revert') {
ctx.revertCurID = rev && rev.revid;
if (!ctx.revertCurID) {
ctx.statusElement.error('Failed to retrieve current revision ID.');
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = rev && rev.user;
if (!ctx.revertUser) {
if (rev && rev.userhidden) { // username was RevDel'd or oversighted
ctx.revertUser = '<username hidden>';
} else {
ctx.statusElement.error('Failed to retrieve user who made the revision.');
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = '[[Help:Revert|Reverted]] to revision ' + ctx.revertOldID + ' by ' + ctx.revertUser + ': ' + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(response, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
const page = response.pages && response.pages[0];
if (page) {
// check for invalid titles
if (page.invalid) {
ctx.statusElement.error('The page title is invalid: ' + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
const resolvedName = page.title;
if (response.redirects) {
// check for cross-namespace redirect:
const origNs = new mw.Title(ctx.pageName).namespace;
const newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(ctx.pageName + ' is a cross-namespace redirect to ' + resolvedName + ', aborted');
onFailure(this);
return false;
}
// only notify user for redirects, not normalization
new Morebits.Status('Note', 'Redirected from ' + ctx.pageName + ' to ' + resolvedName);
}
ctx.pageName = resolvedName; // update to redirect target or normalized name
} else {
// could be a circular redirect or other problem
ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
/**
* Determine whether we should provide a watchlist expiry. Will not
* do so if the page is currently permanently watched, or the current
* expiry is *after* the new, provided expiry. Only handles strings
* recognized by {@link Morebits.Date} or relative timeframes with
* unit it can process. Relies on the fact that fnCanUseMwUserToken
* requires page loading if a watchlistexpiry is provided, so we are
* ensured of knowing the watch status by the use of this.
*
* @return {boolean}
*/
var fnApplyWatchlistExpiry = function() {
if (ctx.watchlistExpiry) {
if (!ctx.watched || Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return true;
} else if (typeof ctx.watched === 'string') {
let newExpiry;
// Attempt to determine if the new expiry is a
// relative (e.g. `1 month`) or absolute datetime
const rel = ctx.watchlistExpiry.split(' ');
try {
newExpiry = new Morebits.Date().add(rel[0], rel[1]);
} catch (e) {
newExpiry = new Morebits.Date(ctx.watchlistExpiry);
}
// If the date is valid, only use it if it extends the current expiry
if (newExpiry.isValid()) {
if (newExpiry.isAfter(new Morebits.Date(ctx.watched))) {
return true;
}
} else {
// If it's still not valid, hope it's a valid MW expiry format that
// Morebits.Date doesn't recognize, so just default to using it.
// This will also include minor typos.
return true;
}
}
}
return false;
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
const response = ctx.saveApi.getResponse();
// see if the API thinks we were successful
if (response.edit.result === 'Success') {
// real success
// default on success action - display link for edited page
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName));
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['completed (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
if (response.edit.captcha) {
ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
} else {
ctx.statusElement.error('Unknown error received from API while saving page');
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
const errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if (errorCode === 'editconflict' && ctx.conflictRetries++ < ctx.maxConflictRetries) {
// edit conflicts can occur when the page needs to be purged from the server cache
const purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
const purgeApi = new Morebits.wiki.Api('Edit conflict detected, purging server cache', purgeQuery, (() => {
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info('Edit conflict detected, reapplying edit');
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}
}), ctx.statusElement);
purgeApi.post();
// check for network or server error
} else if ((errorCode === null || errorCode === undefined) && ctx.retries++ < ctx.maxRetries) {
// the error might be transient, so try again
ctx.statusElement.info('Save failed, retrying in 2 seconds ...');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
// wait for sometime for client to regain connectivity
sleep(2000).then(() => {
ctx.saveApi.post(); // give it another go!
});
// hard error, give up
} else {
const response = ctx.saveApi.getResponse();
const errorData = response.error || // bc error format
response.errors[0].data; // html/wikitext/plaintext error format
switch (errorCode) {
case 'protectedpage':
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
ctx.statusElement.error('Failed to save edit: Page is protected');
break;
case 'abusefilter-disallowed':
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + errorData.abusefilter.description + '".');
break;
case 'abusefilter-warning':
ctx.statusElement.error([ 'A warning was returned by the edit filter: "', errorData.abusefilter.description, '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.' ]);
// We should provide the user with a way to automatically retry the action if they so choose -
// I can't see how to do this without creating a UI dependency on Morebits.wiki.Page though -- TTO
break;
case 'spamblacklist':
// If multiple items are blacklisted, we only return the first
var spam = errorData.spamblacklist.matches[0];
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
break;
default:
ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText());
}
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
const isTextRedirect = function(text) {
if (!text) { // no text - content empty or inaccessible (revdelled or suppressed)
return false;
}
return Morebits.l10n.redirectTagAliases.some((tag) => new RegExp('^\\s*' + tag + '\\W', 'i').test(text));
};
var fnLookupCreationSuccess = function() {
const response = ctx.lookupCreationApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLookupCreationFailure)) {
return; // abort
}
const rev = response.pages[0].revisions && response.pages[0].revisions[0];
if (!rev) {
ctx.statusElement.error('Could not find any revisions of ' + ctx.pageName);
ctx.onLookupCreationFailure(this);
return;
}
if (!ctx.lookupNonRedirectCreator || !isTextRedirect(rev.content)) {
ctx.creator = rev.user;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
ctx.lookupCreationApi = new Morebits.wiki.Api('Retrieving page creation information', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
};
var fnLookupNonRedirectCreator = function() {
const response = ctx.lookupCreationApi.getResponse().query;
const revs = response.pages[0].revisions;
for (let i = 0; i < revs.length; i++) {
if (!isTextRedirect(revs[i].content)) {
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
break;
}
}
if (!ctx.creator) {
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator = revs[0].user;
ctx.timestamp = revs[0].timestamp;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
}
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
};
/**
* Common checks for action methods. Used for move, undelete, delete,
* protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @return {boolean}
*/
var fnPreflightChecks = function(action, onFailure) {
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsSysop && action !== 'move') {
ctx.statusElement.error('Cannot ' + action + 'page : only admins can do that');
onFailure(this);
return false;
}
if (!ctx.editSummary) {
ctx.statusElement.error('Internal error: ' + action + ' reason not set (use setEditSummary function)!');
onFailure(this);
return false;
}
return true; // all OK
};
/**
* Common checks for fnProcess functions (`fnProcessDelete`, `fnProcessMove`, etc.
* Used for move, undelete, delete, protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @param {string} response - The response document from the API call.
* @return {boolean}
*/
const fnProcessChecks = function(action, onFailure, response) {
const missing = response.pages[0].missing;
// No undelete as an existing page could have deleted revisions
const actionMissing = missing && ['delete', 'stabilize', 'move'].includes(action);
const protectMissing = action === 'protect' && missing && (ctx.protectEdit || ctx.protectMove);
const saltMissing = action === 'protect' && !missing && ctx.protectCreate;
if (actionMissing || protectMissing || saltMissing) {
ctx.statusElement.error('Cannot ' + action + ' the page because it ' + (missing ? 'no longer' : 'already') + ' exists');
onFailure(this);
return false;
}
// Delete, undelete, move
// extract protection info
let editprot;
if (action === 'undelete') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else if (action === 'delete' || action === 'move') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
}
if (editprot && !ctx.suppressProtectWarning &&
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName +
(editprot.expiry === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.Date(editprot.expiry).calendar('utc') + ' (UTC))') +
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) {
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.');
onFailure(this);
return false;
}
if (!response.tokens.csrftoken) {
ctx.statusElement.error('Failed to retrieve token.');
onFailure(this);
return false;
}
return true; // all OK
};
var fnProcessMove = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('move')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.moveApi.getResponse().query;
if (!fnProcessChecks('move', ctx.onMoveFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'move',
from: pageTitle,
to: ctx.moveDestination,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true';
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.Api('moving page...', query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessPatrol = function() {
const query = {
action: 'patrol',
format: 'json'
};
// Didn't need to load the page
if (ctx.rcid) {
query.rcid = ctx.rcid;
query.token = mw.user.tokens.get('patrolToken');
} else {
const response = ctx.patrolApi.getResponse().query;
// Don't patrol if not unpatrolled
if (!response.recentchanges[0].unpatrolled) {
return;
}
const lastrevid = response.pages[0].lastrevid;
if (!lastrevid) {
return;
}
query.revid = lastrevid;
const token = response.tokens.csrftoken;
if (!token) {
return;
}
query.token = token;
}
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const patrolStat = new Morebits.Status('Marking page as patrolled');
ctx.patrolProcessApi = new Morebits.wiki.Api('patrolling page...', query, null, patrolStat);
ctx.patrolProcessApi.setParent(this);
ctx.patrolProcessApi.post();
};
// Ensure that the page is curatable
var fnProcessTriageList = function() {
if (ctx.pageID) {
ctx.csrfToken = mw.user.tokens.get('csrfToken');
} else {
const response = ctx.triageApi.getResponse().query;
ctx.pageID = response.pages[0].pageid;
if (!ctx.pageID) {
return;
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
return;
}
}
const query = {
action: 'pagetriagelist',
page_id: ctx.pageID,
format: 'json'
};
ctx.triageProcessListApi = new Morebits.wiki.Api('checking curation status...', query, fnProcessTriage);
ctx.triageProcessListApi.setParent(this);
ctx.triageProcessListApi.post();
};
// callback from triageProcessListApi.post()
var fnProcessTriage = function() {
const responseList = ctx.triageProcessListApi.getResponse().pagetriagelist;
// Exit if not in the queue
if (!responseList || responseList.result !== 'success') {
return;
}
const page = responseList.pages && responseList.pages[0];
// Do nothing if page already triaged/patrolled
if (!page || !parseInt(page.patrol_status, 10)) {
const query = {
action: 'pagetriageaction',
pageid: ctx.pageID,
reviewed: 1,
token: ctx.csrfToken,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const triageStat = new Morebits.Status('Marking page as curated');
ctx.triageProcessApi = new Morebits.wiki.Api('curating page...', query, null, triageStat);
ctx.triageProcessApi.setParent(this);
ctx.triageProcessApi.post();
}
};
var fnProcessDelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('delete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.deleteApi.getResponse().query;
if (!fnProcessChecks('delete', ctx.onDeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'delete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.deleteTalkPage) {
query.deletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.deleteProcessApi = new Morebits.wiki.Api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
// callback from deleteProcessApi.post()
var fnProcessDeleteError = function() {
const errorCode = ctx.deleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.deleteProcessApi.post(); // give it another go!
} else if (errorCode === 'missingtitle') {
ctx.statusElement.error('Cannot delete the page, because it no longer exists');
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to delete the page: ' + ctx.deleteProcessApi.getErrorText());
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
}
};
var fnProcessUndelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('undelete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.undeleteApi.getResponse().query;
if (!fnProcessChecks('undelete', ctx.onUndeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'undelete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.undeleteTalkPage) {
query.undeletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.undeleteProcessApi = new Morebits.wiki.Api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError);
ctx.undeleteProcessApi.setParent(this);
ctx.undeleteProcessApi.post();
};
// callback from undeleteProcessApi.post()
var fnProcessUndeleteError = function() {
const errorCode = ctx.undeleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError') {
if (ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.undeleteProcessApi.post(); // give it another go!
} else {
ctx.statusElement.error('Repeated database query error, please try again');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
} else if (errorCode === 'cantundelete') {
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to undelete the page: ' + ctx.undeleteProcessApi.getErrorText());
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
};
var fnProcessProtect = function() {
const response = ctx.protectApi.getResponse().query;
if (!fnProcessChecks('protect', ctx.onProtectFailure, response)) {
return; // abort
}
const token = response.tokens.csrftoken;
const page = response.pages[0];
const pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
// Fetch existing protection levels
const prs = response.pages[0].protection;
let editprot, moveprot, createprot;
prs.forEach((pr) => {
// Filter out protection from cascading
if (pr.type === 'edit' && !pr.source) {
editprot = pr;
} else if (pr.type === 'move') {
moveprot = pr;
} else if (pr.type === 'create') {
createprot = pr;
}
});
// Fall back to current levels if not explicitly set
if (!ctx.protectEdit && editprot) {
ctx.protectEdit = { level: editprot.level, expiry: editprot.expiry };
}
if (!ctx.protectMove && moveprot) {
ctx.protectMove = { level: moveprot.level, expiry: moveprot.expiry };
}
if (!ctx.protectCreate && createprot) {
ctx.protectCreate = { level: createprot.level, expiry: createprot.expiry };
}
// Default to pre-existing cascading protection if unchanged (similar to above)
if (ctx.protectCascade === null) {
ctx.protectCascade = !!prs.filter((pr) => pr.cascade).length;
}
// Warn if cascading protection being applied with an invalid protection level,
// which for edit protection will cause cascading to be silently stripped
if (ctx.protectCascade) {
// On move protection, this is technically stricter than the MW API,
// but seems reasonable to avoid dumb values and misleading log entries (T265626)
if (((!ctx.protectEdit || ctx.protectEdit.level !== 'sysop') ||
(!ctx.protectMove || ctx.protectMove.level !== 'sysop')) &&
!confirm('You have cascading protection enabled on "' + ctx.pageName +
'" but have not selected uniform sysop-level protection.\n\n' +
'Click OK to adjust and proceed with sysop-level cascading protection, or Cancel to skip this action.')) {
ctx.statusElement.error('Cascading protection was aborted.');
ctx.onProtectFailure(this);
return;
}
ctx.protectEdit.level = 'sysop';
ctx.protectMove.level = 'sysop';
}
// Build protection levels and expirys (expiries?) for query
const protections = [], expirys = [];
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
}
const query = {
action: 'protect',
title: pageTitle,
token: token,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
// Only shows up in logs, not page history [[phab:T259983]]
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.protectCascade) {
query.cascade = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.Api('protecting page...', query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('stabilize')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.stabilizeApi.getResponse().query;
// 'stabilize' as a verb not necessarily well understood
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
// Doesn't support watchlist expiry [[phab:T263336]]
// ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'stabilize',
title: pageTitle,
token: token,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]]
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
/* Doesn't support watchlist expiry [[phab:T263336]]
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
*/
ctx.stabilizeProcessApi = new Morebits.wiki.Api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
var sleep = function(milliseconds) {
const deferred = $.Deferred();
setTimeout(deferred.resolve, milliseconds);
return deferred;
};
}; // end Morebits.wiki.Page
/* Morebits.wiki.Page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/* **************** Morebits.wiki.Preview **************** */
/**
* Use the API to parse a fragment of wikitext and render it as HTML.
*
* The suggested implementation pattern (in {@link Morebits.SimpleWindow} and
* {@link Morebits.QuickForm} situations) is to construct a
* `Morebits.wiki.Preview` object after rendering a `Morebits.QuickForm`, and
* bind the object to an arbitrary property of the form (e.g. |previewer|).
* For an example, see twinklewarn.js.
*
* @memberof Morebits.wiki
* @class
* @param {HTMLElement} previewbox - The element that will contain the rendered HTML,
* usually a <div> element.
*/
Morebits.wiki.Preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass('morebits-previewbox').hide();
/**
* Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
*
* @param {string} wikitext - Wikitext to render; most things should work, including `subst:` and `~~~~`.
* @param {string} [pageTitle] - Optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page.
* @param {string} [sectionTitle] - If provided, render the text as a new section using this as the title.
* @return {jQuery.promise}
*/
this.beginRender = function(wikitext, pageTitle, sectionTitle) {
$(previewbox).show();
const statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.Status.init(statusspan);
const query = {
action: 'parse',
prop: ['text', 'modules'],
pst: true, // PST = pre-save transform; this makes substitution work properly
preview: true,
text: wikitext,
title: pageTitle || mw.config.get('wgPageName'),
disablelimitreport: true,
disableeditsection: true,
format: 'json'
};
if (sectionTitle) {
query.section = 'new';
query.sectiontitle = sectionTitle;
}
const renderApi = new Morebits.wiki.Api('loading...', query, fnRenderSuccess, new Morebits.Status('Preview'));
return renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
const response = apiobj.getResponse();
const html = response.parse.text;
if (!html) {
apiobj.statelem.error('failed to retrieve preview, or template was blanked');
return;
}
previewbox.innerHTML = html;
mw.loader.load(response.parse.modulestyles);
mw.loader.load(response.parse.modules);
// this makes links open in new tab
$(previewbox).find('a').attr('target', '_blank');
// Integrate with scripts that do things on rendered content, like navpopups
mw.hook('wikipage.content').fire($(previewbox));
};
/** Hides the preview box and clears it. */
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/* **************** Morebits.wikitext **************** */
/**
* Wikitext manipulation.
*
* @namespace Morebits.wikitext
* @memberof Morebits
*/
Morebits.wikitext = {};
/**
* Get the value of every parameter found in the wikitext of a given template.
*
* @memberof Morebits.wikitext
* @param {string} text - Wikitext containing a template.
* @param {number} [start=0] - Index noting where in the text the template begins.
* @return {Object} `{name: templateName, parameters: {key: value}}`.
*/
Morebits.wikitext.parseTemplate = function(text, start) {
start = start || 0;
const level = []; // Track of how deep we are ({{, {{{, or [[)
let count = -1; // Number of parameters found
let unnamed = 0; // Keep track of what number an unnamed parameter should receive
let equals = -1; // After finding "=" before a parameter, the index; otherwise, -1
let current = '';
const result = {
name: '',
parameters: {}
};
let key, value;
/**
* Function to handle finding parameter values.
*
* @param {boolean} [final=false] - Whether this is the final
* parameter and we need to remove the trailing `}}`.
*/
function findParam(final) {
// Nothing found yet, this must be the template name
if (count === -1) {
result.name = current.slice(2).trim();
++count;
} else {
// In a parameter
if (equals !== -1) {
// We found an equals, so save the parameter as key: value
key = current.substring(0, equals).trim();
value = final ? current.substring(equals + 1, current.length - 2).trim() : current.substring(equals + 1).trim();
result.parameters[key] = value;
equals = -1;
} else {
// No equals, so it must be unnamed; no trim since whitespace allowed
const param = final ? current.substring(equals + 1, current.length - 2) : current;
if (param) {
result.parameters[++unnamed] = param;
++count;
}
}
}
}
for (let i = start; i < text.length; ++i) {
const test3 = text.substr(i, 3);
if (test3 === '{{{' || (test3 === '}}}' && level[level.length - 1] === 3)) {
current += test3;
i += 2;
if (test3 === '{{{') {
level.push(3);
} else {
level.pop();
}
continue;
}
const test2 = text.substr(i, 2);
// Entering a template (or link)
if (test2 === '{{' || test2 === '[[') {
current += test2;
++i;
if (test2 === '{{') {
level.push(2);
} else {
level.push('wl');
}
continue;
}
// Either leaving a link or template/parser function
if ((test2 === '}}' && level[level.length - 1] === 2) ||
(test2 === ']]' && level[level.length - 1] === 'wl')) {
current += test2;
++i;
level.pop();
// Find the final parameter if this really is the end
if (test2 === '}}' && level.length === 0) {
findParam(true);
break;
}
continue;
}
if (text.charAt(i) === '|' && level.length === 1) {
// Another pipe found, toplevel, so parameter coming up!
findParam();
current = '';
} else if (equals === -1 && text.charAt(i) === '=' && level.length === 1) {
// Equals found, toplevel
equals = current.length;
current += text.charAt(i);
} else {
// Just advance the position
current += text.charAt(i);
}
}
return result;
};
/**
* Adjust and manipulate the wikitext of a page.
*
* @class
* @memberof Morebits.wikitext
* @param {string} text - Wikitext to be manipulated.
*/
Morebits.wikitext.Page = function mediawikiPage(text) {
this.text = text;
};
Morebits.wikitext.Page.prototype = {
text: '',
/**
* Removes links to `link_target` from the page text.
*
* @param {string} link_target
* @return {Morebits.wikitext.Page}
*/
removeLink: function(link_target) {
const mwTitle = mw.Title.newFromText(link_target);
const namespaceID = mwTitle.getNamespaceId();
const title = mwTitle.getMainText();
let link_regex_string = '';
if (namespaceID !== 0) {
link_regex_string = Morebits.namespaceRegex(namespaceID) + ':';
}
link_regex_string += Morebits.pageNameRegex(title);
// For most namespaces, unlink both [[User:Test]] and [[:User:Test]]
// For files and categories, only unlink [[:Category:Test]]. Do not unlink [[Category:Test]]
const isFileOrCategory = [6, 14].includes(namespaceID);
const colon = isFileOrCategory ? ':' : ':?';
const simple_link_regex = new RegExp('\\[\\[' + colon + '(' + link_regex_string + ')\\]\\]', 'g');
const piped_link_regex = new RegExp('\\[\\[' + colon + link_regex_string + '\\|(.+?)\\]\\]', 'g');
this.text = this.text.replace(simple_link_regex, '$1').replace(piped_link_regex, '$1');
return this;
},
/**
* Comments out images from page text; if used in a gallery, deletes the whole line.
* If used as a template argument (not necessarily with `File:` prefix), the template parameter is commented out.
*
* @param {string} image - Image name without `File:` prefix.
* @param {string} [reason] - Reason to be included in comment, alongside the commented-out image.
* @return {Morebits.wikitext.Page}
*/
commentOutImage: function(image, reason) {
const unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
reason = reason ? reason + ': ' : '';
const image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
const replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace(allLinks[i], replacement);
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
}
}
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
const gallery_image_re = new RegExp('(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '<!-- ' + reason + '$1 -->');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
const free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
/**
* Converts uses of [[File:`image`]] to [[File:`image`|`data`]].
*
* @param {string} image - Image name without File: prefix.
* @param {string} data - The display options.
* @return {Morebits.wikitext.Page}
*/
addToImageComment: function(image, data) {
const image_re_string = Morebits.pageNameRegex(image);
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(this.text, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
let replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace(/\]\]$/, '|' + data + ']]');
this.text = this.text.replace(allLinks[i], replacement);
}
}
const gallery_re = new RegExp('^(\\s*' + image_re_string + '.*?)\\|?(.*?)$', 'mg');
const newtext = '$1|$2 ' + data;
this.text = this.text.replace(gallery_re, newtext);
return this;
},
/**
* Remove all transclusions of a template from page text.
*
* @param {string} template - Page name whose transclusions are to be removed,
* include namespace prefix only if not in template namespace.
* @return {Morebits.wikitext.Page}
*/
removeTemplate: function(template) {
const template_re_string = Morebits.pageNameRegex(template);
const links_re = new RegExp('\\{\\{(?:' + Morebits.namespaceRegex(10) + ':)?\\s*' + template_re_string + '\\s*[\\|(?:\\}\\})]');
const allTemplates = Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', [ '{{{', '}}}' ]);
for (let i = 0; i < allTemplates.length; ++i) {
if (links_re.test(allTemplates[i])) {
this.text = this.text.replace(allTemplates[i], '');
}
}
return this;
},
/**
* Smartly insert a tag atop page text but after specified templates,
* such as hatnotes, short description, or deletion and protection templates.
* Notably, does *not* insert a newline after the tag.
*
* @param {string} tag - The tag to be inserted.
* @param {string|string[]} regex - Templates after which to insert tag,
* given as either as a (regex-valid) string or an array to be joined by pipes.
* @param {string} [flags=i] - Regex flags to apply. `''` to provide no flags;
* other falsey values will default to `i`.
* @param {string|string[]} [preRegex] - Optional regex string or array to match
* before any template matches (i.e. before `{{`), such as html comments.
* @return {Morebits.wikitext.Page}
*/
insertAfterTemplates: function(tag, regex, flags, preRegex) {
if (typeof tag === 'undefined') {
throw new Error('No tag provided');
}
// .length is only a property of strings and arrays so we
// shouldn't need to check type
if (typeof regex === 'undefined' || !regex.length) {
throw new Error('No regex provided');
} else if (Array.isArray(regex)) {
regex = regex.join('|');
}
if (typeof flags !== 'string') {
flags = 'i';
}
if (!preRegex || !preRegex.length) {
preRegex = '';
} else if (Array.isArray(preRegex)) {
preRegex = preRegex.join('|');
}
// Regex is extra complicated to allow for templates with
// parameters and to handle whitespace properly
this.text = this.text.replace(
new RegExp(
// leading whitespace
'^\\s*' +
// capture template(s)
'(?:((?:\\s*' +
// Pre-template regex, such as leading html comments
preRegex + '|' +
// begin template format
'\\{\\{\\s*(?:' +
// Template regex
regex +
// end main template name, optionally with a number
// Probably remove the (?:) though
')\\d*\\s*' +
// template parameters
'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' +
// end template format
'\\}\\})+' +
// end capture
'(?:\\s*\\n)?)' +
// trailing whitespace
'\\s*)?',
flags), '$1' + tag
);
return this;
},
/**
* Get the manipulated wikitext.
*
* @return {string}
*/
getText: function() {
return this.text;
}
};
/* *********** Morebits.UserspaceLogger ************ */
/**
* Handles logging actions to a userspace log.
* Used in CSD, PROD, and XFD.
*
* @memberof Morebits
* @class
* @param {string} logPageName - Title of the subpage of the current user's log.
*/
Morebits.UserspaceLogger = function(logPageName) {
if (!logPageName) {
throw new Error('no log page name specified');
}
/**
* The text to prefix the log with upon creation, defaults to empty.
*
* @type {string}
*/
this.initialText = '';
/**
* The header level to use for months, defaults to 3 (`===`).
*
* @type {number}
*/
this.headerLevel = 3;
this.changeTags = '';
/**
* Log the entry.
*
* @param {string} logText - Doesn't include leading `#` or `*`.
* @param {string} summaryText - Edit summary.
* @return {jQuery.Promise}
*/
this.log = function(logText, summaryText) {
const def = $.Deferred();
if (!logText) {
return def.reject();
}
const page = new Morebits.wiki.Page('User:' + mw.config.get('wgUserName') + '/' + logPageName,
'Adding entry to userspace log'); // make this '... to ' + logPageName ?
page.load((pageobj) => {
// add blurb if log page doesn't exist or is blank
let text = pageobj.getPageText() || this.initialText;
// create monthly header if it doesn't exist already
const date = new Morebits.Date(pageobj.getLoadTime());
if (!date.monthHeaderRegex().exec(text)) {
text += '\n\n' + date.monthHeader(this.headerLevel);
}
pageobj.setPageText(text + '\n' + logText);
pageobj.setEditSummary(summaryText);
pageobj.setChangeTags(this.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save(def.resolve, def.reject);
});
return def;
};
};
/* **************** Morebits.Status **************** */
/**
* Create and show status messages of varying urgency.
* {@link Morebits.Status.init|Morebits.Status.init()} must be called before
* any status object is created, otherwise those statuses won't be visible.
*
* @memberof Morebits
* @class
* @param {string} text - Text before the the colon `:`.
* @param {string} stat - Text after the colon `:`.
* @param {string} [type=status] - Determine the font color of the status
* line, allowable values are: `status` (blue), `info` (green), `warn` (red),
* or `error` (bold red).
*/
Morebits.Status = function Status(text, stat, type) {
this.textRaw = text;
this.text = Morebits.createHtml(text);
this.type = type || 'status';
this.generate();
if (stat) {
this.update(stat, type);
}
};
/**
* Specify an area for status message elements to be added to.
*
* @memberof Morebits.Status
* @param {HTMLElement} root - Usually a div element.
* @throws {Error} If `root` is not an `HTMLElement`.
*/
Morebits.Status.init = function(root) {
if (!(root instanceof Element)) {
throw new Error('object not an instance of Element');
}
while (root.hasChildNodes()) {
root.removeChild(root.firstChild);
}
Morebits.Status.root = root;
Morebits.Status.errorEvent = null;
};
Morebits.Status.root = null;
/**
* @memberof Morebits.Status
* @param {Function} handler - Function to execute on error.
* @throws {Error} When `handler` is not a function.
*/
Morebits.Status.onError = function(handler) {
if (typeof handler === 'function') {
Morebits.Status.errorEvent = handler;
} else {
throw new Error('Morebits.Status.onError: handler is not a function');
}
};
Morebits.Status.prototype = {
stat: null,
statRaw: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
/** Add the status element node to the DOM. */
link: function() {
if (!this.linked && Morebits.Status.root) {
Morebits.Status.root.appendChild(this.node);
this.linked = true;
}
},
/** Remove the status element node from the DOM. */
unlink: function() {
if (this.linked) {
Morebits.Status.root.removeChild(this.node);
this.linked = false;
}
},
/**
* Update the status.
*
* @param {string} status - Part of status message after colon.
* @param {string} type - 'status' (blue), 'info' (green), 'warn'
* (red), or 'error' (bold red).
*/
update: function(status, type) {
this.statRaw = status;
this.stat = Morebits.createHtml(status);
if (type) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.Status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.Status.errorEvent) {
Morebits.Status.errorEvent();
}
// also log error messages in the browser console
console.error(this.textRaw + ': ' + this.statRaw); // eslint-disable-line no-console
}
}
this.render();
},
/** Produce the html for first part of the status message. */
generate: function() {
this.node = document.createElement('div');
this.node.appendChild(document.createElement('span')).appendChild(this.text);
this.node.appendChild(document.createElement('span')).appendChild(document.createTextNode(': '));
this.target = this.node.appendChild(document.createElement('span'));
this.target.appendChild(document.createTextNode('')); // dummy node
},
/** Complete the html, for the second part of the status message. */
render: function() {
this.node.className = 'morebits_status_' + this.type;
while (this.target.hasChildNodes()) {
this.target.removeChild(this.target.firstChild);
}
this.target.appendChild(this.stat);
this.link();
},
status: function(status) {
this.update(status, 'status');
},
info: function(status) {
this.update(status, 'info');
},
warn: function(status) {
this.update(status, 'warn');
},
error: function(status) {
this.update(status, 'error');
}
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `status`-type (blue)
*/
Morebits.Status.status = function(text, status) {
return new Morebits.Status(text, status);
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `info`-type (green)
*/
Morebits.Status.info = function(text, status) {
return new Morebits.Status(text, status, 'info');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `warn`-type (red)
*/
Morebits.Status.warn = function(text, status) {
return new Morebits.Status(text, status, 'warn');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `error`-type (bold red)
*/
Morebits.Status.error = function(text, status) {
return new Morebits.Status(text, status, 'error');
};
/**
* For the action complete message at the end, create a status line without
* a colon separator.
*
* @memberof Morebits.Status
* @param {string} text
*/
Morebits.Status.actionCompleted = function(text) {
const node = document.createElement('div');
node.appendChild(document.createElement('b')).appendChild(document.createTextNode(text));
node.className = 'morebits_status_info morebits_action_complete';
if (Morebits.Status.root) {
Morebits.Status.root.appendChild(node);
}
};
/**
* Display the user's rationale, comments, etc. Back to them after a failure,
* so that they may re-use it.
*
* @memberof Morebits.Status
* @param {string} comments
* @param {string} message
*/
Morebits.Status.printUserText = function(comments, message) {
const p = document.createElement('p');
p.innerHTML = message;
const div = document.createElement('div');
div.className = 'morebits-usertext';
div.style.marginTop = '0';
div.style.whiteSpace = 'pre-wrap';
div.textContent = comments;
p.appendChild(div);
Morebits.Status.root.appendChild(p);
};
/**
* Simple helper function to create a simple node.
*
* @param {string} type - Type of HTML element.
* @param {string} content - Text content.
* @param {string} [color] - Font color.
* @return {HTMLElement}
*/
Morebits.htmlNode = function (type, content, color) {
const node = document.createElement(type);
if (color) {
node.style.color = color;
}
node.appendChild(document.createTextNode(content));
return node;
};
/**
* Add shift-click support for checkboxes. The wikibits version
* (`window.addCheckboxClickHandlers`) has some restrictions, and doesn't work
* with checkboxes inside a sortable table, so let's build our own.
*
* @param {jQuery} jQuerySelector
* @param {jQuery} jQueryContext
*/
Morebits.checkboxShiftClickSupport = function (jQuerySelector, jQueryContext) {
let lastCheckbox = null;
function clickHandler(event) {
const thisCb = this;
if (event.shiftKey && lastCheckbox !== null) {
const $cbs = $(jQuerySelector, jQueryContext); // can't cache them, obviously, if we want to support re-sorting
let index = -1, lastIndex = -1, i;
for (i = 0; i < $cbs.length; i++) {
if ($cbs[i] === thisCb) {
index = i;
if (lastIndex > -1) {
break;
}
}
if ($cbs[i] === lastCheckbox) {
lastIndex = i;
if (index > -1) {
break;
}
}
}
if (index > -1 && lastIndex > -1) {
// inspired by wikibits
const endState = thisCb.checked;
let start, finish;
if (index < lastIndex) {
start = index + 1;
finish = lastIndex;
} else {
start = lastIndex;
finish = index - 1;
}
for (i = start; i <= finish; i++) {
if ($cbs[i].checked !== endState) {
$cbs[i].click();
}
}
}
}
lastCheckbox = thisCb;
return true;
}
$(jQuerySelector, jQueryContext).on('click', clickHandler);
};
/* **************** Morebits.BatchOperation **************** */
/**
* Iterates over a group of pages (or arbitrary objects) and executes a worker function
* for each.
*
* `setPageList(pageList)`: Sets the list of pages to work on. It should be an
* array of page names strings.
*
* `setOption(optionName, optionValue)`: Sets a known option:
* - `chunkSize` (integer): The size of chunks to break the array into (default
* 50). Setting this to a small value (<5) can cause problems.
* - `preserveIndividualStatusLines` (boolean): Keep each page's status element
* visible when worker is complete? See note below.
*
* `run(worker, postFinish)`: Runs the callback `worker` for each page in the
* list. The callback must call `workerSuccess` when succeeding, or
* `workerFailure` when failing. If using {@link Morebits.wiki.Api} or
* {@link Morebits.wiki.Page}, this is easily done by passing these two
* functions as parameters to the methods on those objects: for instance,
* `page.save(batchOp.workerSuccess, batchOp.workerFailure)`. Make sure the
* methods are called directly if special success/failure cases arise. If you
* omit to call these methods, the batch operation will stall after the first
* chunk! Also ensure that either workerSuccess or workerFailure is called no
* more than once. The second callback `postFinish` is executed when the
* entire batch has been processed.
*
* If using `preserveIndividualStatusLines`, you should try to ensure that the
* `workerSuccess` callback has access to the page title. This is no problem for
* {@link Morebits.wiki.Page} objects. But when using the API, please set the
* |pageName| property on the {@link Morebits.wiki.Api} object.
*
* There are sample batchOperation implementations using Morebits.wiki.Page in
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js.
*
* @memberof Morebits
* @class
* @param {string} [currentAction]
*/
Morebits.BatchOperation = function(currentAction) {
const ctx = {
// backing fields for public properties
pageList: null,
options: {
chunkSize: 50,
preserveIndividualStatusLines: false
},
// internal counters, etc.
statusElement: new Morebits.Status(currentAction || 'Performing batch operation'),
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
countStarted: 0,
countFinished: 0,
countFinishedSuccess: 0,
currentChunkIndex: -1,
pageChunks: [],
running: false
};
// shouldn't be needed by external users, but provided anyway for maximum flexibility
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* Sets the list of pages to work on.
*
* @param {Array} pageList - Array of objects over which you wish to execute the worker function
* This is usually the list of page names (strings).
*/
this.setPageList = function(pageList) {
ctx.pageList = pageList;
};
/**
* Sets a known option.
*
* @param {string} optionName - Name of the option:
* - chunkSize (integer): The size of chunks to break the array into
* (default 50). Setting this to a small value (<5) can cause problems.
* - preserveIndividualStatusLines (boolean): Keep each page's status
* element visible when worker is complete?
* @param {number|boolean} optionValue - Value to which the option is
* to be set. Should be an integer for chunkSize and a boolean for
* preserveIndividualStatusLines.
*/
this.setOption = function(optionName, optionValue) {
ctx.options[optionName] = optionValue;
};
/**
* Runs the first callback for each page in the list.
* The callback must call workerSuccess when succeeding, or workerFailure when failing.
* Runs the optional second callback when the whole batch has been processed.
*
* @param {Function} worker
* @param {Function} [postFinish]
*/
this.run = function(worker, postFinish) {
if (ctx.running) {
ctx.statusElement.error('Batch operation is already running');
return;
}
ctx.running = true;
ctx.worker = worker;
ctx.postFinish = postFinish;
ctx.countStarted = 0;
ctx.countFinished = 0;
ctx.countFinishedSuccess = 0;
ctx.currentChunkIndex = -1;
ctx.pageChunks = [];
const total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info('no pages specified');
ctx.running = false;
if (ctx.postFinish) {
ctx.postFinish();
}
return;
}
// chunk page list into more manageable units
ctx.pageChunks = Morebits.array.chunk(ctx.pageList, ctx.options.chunkSize);
// start the process
Morebits.wiki.addCheckpoint();
ctx.statusElement.status('0%');
fnStartNewChunk();
};
/**
* To be called by worker before it terminates successfully.
*
* @param {(Morebits.wiki.Page|Morebits.wiki.Api|string)} arg -
* This should be the `Morebits.wiki.Page` or `Morebits.wiki.Api` object used by worker
* (for the adjustment of status lines emitted by them).
* If no Morebits.wiki.* object is used (e.g. you're using `mw.Api()` or something else), and
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument.
*/
this.workerSuccess = function(arg) {
if (arg instanceof Morebits.wiki.Api || arg instanceof Morebits.wiki.Page) {
// update or remove status line
const statelem = arg.getStatusElement();
if (ctx.options.preserveIndividualStatusLines) {
if (arg.getPageName || arg.pageName || (arg.query && arg.query.title)) {
// we know the page title - display a relevant message
const pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info('completed ([[' + pageName + ']])');
} else {
// we don't know the page title - just display a generic message
statelem.info('done');
}
} else {
// remove the status line automatically produced by Morebits.wiki.*
statelem.unlink();
}
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.Status(arg, 'completed ([[' + arg + ']])');
}
ctx.countFinishedSuccess++;
fnDoneOne();
};
this.workerFailure = function() {
fnDoneOne();
};
// private functions
const thisProxy = this;
var fnStartNewChunk = function() {
const chunk = ctx.pageChunks[++ctx.currentChunkIndex];
if (!chunk) {
return; // done! yay
}
// start workers for the current chunk
ctx.countStarted += chunk.length;
chunk.forEach((page) => {
ctx.worker(page, thisProxy);
});
};
var fnDoneOne = function() {
ctx.countFinished++;
// update overall status line
const total = ctx.pageList.length;
if (ctx.countFinished < total) {
const progress = Math.round(100 * ctx.countFinished / total);
ctx.statusElement.status(progress + '%');
// start a new chunk if we're close enough to the end of the previous chunk, and
// we haven't already started the next one
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) &&
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
fnStartNewChunk();
}
} else if (ctx.countFinished === total) {
const statusString = 'Done (' + ctx.countFinishedSuccess + '/' + ctx.countFinished + ' actions completed successfully)';
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
} else {
ctx.statusElement.info(statusString);
}
if (ctx.postFinish) {
ctx.postFinish();
}
Morebits.wiki.removeCheckpoint();
ctx.running = false;
} else {
// ctx.countFinished > total
// just for giggles! (well, serious debugging, actually)
ctx.statusElement.warn('Done (overshot by ' + (ctx.countFinished - total) + ')');
Morebits.wiki.removeCheckpoint();
ctx.running = false;
}
};
};
/**
* Given a set of asynchronous functions to run along with their dependencies,
* run them in an efficient sequence so that multiple functions
* that don't depend on each other are triggered simultaneously. Where
* dependencies exist, it ensures that the dependency functions finish running
* before the dependent function runs. The values resolved by the dependencies
* are made available to the dependant as arguments.
*
* @memberof Morebits
* @class
*/
Morebits.TaskManager = function(context) {
this.taskDependencyMap = new Map();
this.failureCallbackMap = new Map();
this.deferreds = new Map();
this.context = context || window;
/**
* Register a task along with its dependencies (tasks which should have finished
* execution before we can begin this one). Each task is a function that must return
* a promise. The function will get the values resolved by the dependency functions
* as arguments.
*
* @param {Function} func - A task.
* @param {Function[]} deps - Its dependencies.
* @param {Function} [onFailure] - a failure callback that's run if the task or any one
* of its dependencies fail.
*/
this.add = function(func, deps, onFailure) {
this.taskDependencyMap.set(func, deps);
this.failureCallbackMap.set(func, onFailure || (() => {}));
const deferred = $.Deferred();
this.deferreds.set(func, deferred);
};
/**
* Run all the tasks. Multiple tasks may be run at once.
*
* @return {jQuery.Promise} - Resolved if all tasks succeed, rejected otherwise.
*/
this.execute = function() {
const self = this; // proxy for `this` for use inside functions where `this` is something else
this.taskDependencyMap.forEach((deps, task) => {
const dependencyPromisesArray = deps.map((dep) => self.deferreds.get(dep));
$.when.apply(self.context, dependencyPromisesArray).then(function() {
const result = task.apply(self.context, arguments);
if (result === undefined) { // maybe the function threw, or it didn't return anything
mw.log.error('Morebits.TaskManager: task returned undefined');
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, []);
}
result.then(function() {
self.deferreds.get(task).resolve.apply(self.context, arguments);
}, function() { // task failed
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
}, function() { // one or more of the dependencies failed
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
});
return $.when.apply(null, [...this.deferreds.values()]); // resolved when everything is done!
};
};
/**
* A simple draggable window. No longer uses jQuery UI.
*
* @memberof Morebits
* @class
* @param {number} width
* @param {number} height - The maximum allowable height for the content area.
*/
Morebits.SimpleWindow = function SimpleWindow(width, height) {
const $dialog = $('<div>').addClass('morebits-dialog').attr('role', 'dialog').attr('tabindex', -1);
const $titleBar = $('<div>').addClass('morebits-dialog-titlebar').append(
$('<span>').addClass('morebits-dialog-title'),
$('<button>')
.addClass('morebits-dialog-close')
.text('\u00D7')
.on('click', () => this.close())
);
const $content = $('<div>')
.addClass('morebits-dialog-content')
.attr('id', 'morebits-dialog-content-' + Math.round(Math.random() * 1e15));
const $buttonPane = $('<div>').addClass('morebits-dialog-buttonpane').append(
$('<span>').addClass('morebits-dialog-buttons'),
$('<span>').addClass('morebits-dialog-footerlinks')
);
const $resizer = $('<div>').addClass('morebits-dialog-resizer');
$dialog.append($titleBar, $content, $buttonPane, $resizer)
.css('width', Math.min(parseInt(width || 800, 10), $(window).width()))
.css('height', 'auto')
.css('max-height', height + 20)
.css('top', Math.max(0, window.scrollY + $(window).height() / 2 - (height + 20) / 2)) // Centre it (assume max height)
.css('left', Math.max(0, window.scrollX + $(window).width() / 2 - $dialog.width() / 2)); // Centre it
$dialog.on('focus', () => this.focus()).on('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
// Make dialog draggable and resizable
let isDragging = false, isResizing = false, initialX, initialY, initialWidth, initialHeight;
$titleBar.on('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - $dialog.offset().left;
initialY = e.clientY - $dialog.offset().top;
});
$resizer.on('mousedown', (e) => {
isResizing = true;
$dialog.css('height', $dialog.height());
$dialog.css('max-height', 'none');
initialWidth = $dialog.width();
initialHeight = $dialog.height();
initialX = e.clientX;
initialY = e.clientY;
});
$(document).on('mousemove', (e) => {
if (isDragging) {
$dialog.css('left', Math.max(0, e.clientX - initialX))
.css('top', Math.max(0, e.clientY - initialY));
} else if (isResizing) {
$dialog.css('width', initialWidth + (e.clientX - initialX))
.css('height', initialHeight + (e.clientY - initialY));
}
}).on('mouseup', () => {
isDragging = false;
isResizing = false;
});
this.$dialog = $dialog;
this.content = $content[0];
this.height = height;
};
Morebits.SimpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
/**
* Bring a dialog to the top, when there are other overlapping dialogs open.
*
* @return {Morebits.SimpleWindow}
*/
focus: function() {
if (!this.$dialog.hasClass('morebits-dialog-modal')) {
$('.morebits-dialog:not(.morebits-dialog-modal)').get()
.filter((dialog) => dialog !== this.$dialog[0])
.concat(this.$dialog[0])
.forEach((dialog, idx) => {
dialog.style.zIndex = 501 + idx;
});
}
return this;
},
/**
* Closes the dialog. If this is set as an event handler, it will stop the event
* from doing anything more.
*
* @param {event} [event]
* @return {Morebits.SimpleWindow}
*/
close: function(event) {
if (event) {
event.preventDefault();
}
this.setModality(false);
this.$dialog.remove();
return this;
},
/**
* Shows the dialog. Calling display() on a dialog that has previously been closed
* might work, but it is not guaranteed.
*
* @return {Morebits.SimpleWindow}
*/
display: function() {
if (this.scriptName) {
const $titleBar = this.$dialog.find('.morebits-dialog-title');
$titleBar.find('.morebits-dialog-scriptname').remove();
$titleBar.prepend($('<span>')
.addClass('morebits-dialog-scriptname')
.text(this.scriptName + ' \u00B7 ') // U+00B7 MIDDLE DOT = ·
);
}
if (this.$dialog.find('.morebits-scrollbox').length) {
// quickform needs some extra CSS if it happens to contain a scrollbox.
// CSS :has() selector can be used in the future which is easier and more reliable.
this.$dialog.find('.quickform').addClass('has-scrollbox');
}
this.$dialog.appendTo(document.body);
// Put focus on the first form element in the dialog, or on dialog itself.
const $firstField = this.$dialog.find('input, select, textarea').first();
const $firstFieldOrDialog = $firstField.length ? $firstField : this.$dialog;
$firstFieldOrDialog.trigger('focus');
return this;
},
/**
* Sets the dialog title.
*
* @param {string} title
* @return {Morebits.SimpleWindow}
*/
setTitle: function(title) {
this.$dialog.find('.morebits-dialog-title').text(title);
return this;
},
/**
* Sets the script name, appearing as a prefix to the title to help users determine which
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
*
* @param {string} name
* @return {Morebits.SimpleWindow}
*/
setScriptName: function(name) {
this.scriptName = name;
return this;
},
/**
* Sets the dialog width.
*
* @param {number} width
* @return {Morebits.SimpleWindow}
*/
setWidth: function(width) {
this.$dialog.css('width', width + 'px');
return this;
},
/**
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents.
*
* @param {number} height
* @return {Morebits.SimpleWindow}
*/
setHeight: function(height) {
this.$dialog.css('height', 'auto');
this.$dialog.css('max-height', (height + 20) + 'px');
return this;
},
/**
* Sets the content of the dialog to the given element node, usually from rendering
* a {@link Morebits.QuickForm}.
* Re-enumerates the footer buttons, but leaves the footer links as they are.
* Be sure to call this at least once before the dialog is displayed...
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
setContent: function(content) {
this.purgeContent();
this.addContent(content);
return this;
},
/**
* Adds the given element node to the dialog content.
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
addContent: function(content) {
this.content.appendChild(content);
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
$(this.content).find('input[type="submit"], button[type="submit"]').get().forEach((node) => {
node.style.display = 'none';
const button = document.createElement('button');
button.textContent = node.getAttribute('value') || node.textContent || 'Submit';
button.className = node.className || 'submitButtonProxy';
button.addEventListener('click', () => node.click(), false);
this.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
this.$dialog.find('.morebits-dialog-buttons')
.empty()
.append(...this.buttons)
// Set data-empty attribute only if there are no buttons. Used by CSS.
.attr('data-empty', this.buttons.length ? null : 'data-empty');
return this;
},
/**
* Removes all contents from the dialog, barring any footer links.
*
* @return {Morebits.SimpleWindow}
*/
purgeContent: function() {
this.buttons = [];
// delete all buttons in the buttonpane
this.$dialog.find('.morebits-dialog-buttons').empty();
$(this.content).empty();
return this;
},
/**
* Adds a link in the bottom-right corner of the dialog.
* This can be used to provide help or policy links.
* For example, Twinkle's CSD module adds a link to the CSD policy page,
* as well as a link to Twinkle's documentation.
*
* @param {string} text - Display text.
* @param {string} wikiPage - Link target.
* @return {Morebits.SimpleWindow}
*/
addFooterLink: function(text, wikiPage) {
const $footerlinks = this.$dialog.find('.morebits-dialog-footerlinks');
if (this.hasFooterLinks) {
const bullet = document.createElement('span');
bullet.textContent = ' \u2022 '; // U+2022 BULLET
$footerlinks.append(bullet);
}
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(wikiPage));
link.setAttribute('title', wikiPage);
link.setAttribute('target', '_blank');
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
/**
* Sets whether the window should be modal or not. Modal dialogs create
* an overlay below the dialog but above other page elements. This
* must be used (if necessary) before calling display().
*
* @param {boolean} [modal=false] - If set to true, other items on the
* page will be disabled, i.e., cannot be interacted with.
* @return {Morebits.SimpleWindow}
*/
setModality: function(modal) {
if (modal) {
$('<div>').addClass('morebits-dialog-overlay').appendTo(document.body);
this.$dialog.addClass('morebits-dialog-modal');
} else {
$('.morebits-dialog-overlay').remove();
this.$dialog.removeClass('morebits-dialog-modal');
}
return this;
}
};
/**
* Enables or disables all footer buttons on all {@link Morebits.SimpleWindow}s in the current page.
* This should be called with `false` when the button(s) become irrelevant (e.g. just before
* {@link Morebits.Status.init} is called).
* This is not an instance method so that consumers don't have to keep a reference to the
* original `Morebits.SimpleWindow` object sitting around somewhere. Anyway, most of the time
* there will only be one `Morebits.SimpleWindow` open, so this shouldn't matter.
*
* @memberof Morebits.SimpleWindow
* @param {boolean} enabled
*/
Morebits.SimpleWindow.setButtonsEnabled = function(enabled) {
$('.morebits-dialog-buttons button').prop('disabled', !enabled);
};
// Deprecated class name aliases in camelCase. Please use the PascalCase names instead.
Morebits.batchOperation = Morebits.BatchOperation;
Morebits.date = Morebits.Date;
Morebits.quickForm = Morebits.QuickForm;
Morebits.quickForm.element = Morebits.QuickForm.Element;
Morebits.simpleWindow = Morebits.SimpleWindow;
Morebits.status = Morebits.Status;
Morebits.taskManager = Morebits.TaskManager;
Morebits.unbinder = Morebits.Unbinder;
Morebits.userspaceLogger = Morebits.UserspaceLogger;
Morebits.wiki.api = Morebits.wiki.Api;
Morebits.wiki.page = Morebits.wiki.Page;
Morebits.wiki.preview = Morebits.wiki.Preview;
Morebits.wikitext.page = Morebits.wikitext.Page;
}());
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line.
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if (typeof arguments === 'undefined') { // typeof is here for a reason...
/* global Morebits */
window.SimpleWindow = Morebits.SimpleWindow;
window.QuickForm = Morebits.QuickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.Status;
}
// </nowiki>
b9v3vpgcte5dzvwqiyzs9nk24upyt4t
MediaWiki:Gadget-morebits.css
8
29071
326094
2026-04-04T08:56:29Z
Kannotlogin
29153
nieuw blad: /** * morebits.css * =========== * Styles to support morebits.js. * * The morebits library is maintained by the maintainers of Twinkle. * For queries, suggestions, help, etc., head to [[WT:TW]]. * The latest development source is available at [https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.css]. */ /* Define CSS variables */ html { --morebits-color-status-status: #4682b4; --morebits-color-legend: #31628f; --morebits-color-tooltip: #0000cd; --moreb…
326094
css
text/css
/**
* morebits.css
* ===========
* Styles to support morebits.js.
*
* The morebits library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[WT:TW]].
* The latest development source is available at [https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.css].
*/
/* Define CSS variables */
html {
--morebits-color-status-status: #4682b4;
--morebits-color-legend: #31628f;
--morebits-color-tooltip: #0000cd;
--morebits-color-info: #228b22;
--morebits-color-warning: #ff4500;
--morebits-color-titlebar-links: #3062ad;
--morebits-bgcolor-dialog: #f0f8ff;
--morebits-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* Morebits.Status */
.morebits_status_status {
color: var(--morebits-color-status-status);
}
.morebits_status_info {
color: var(--morebits-color-info);
}
.morebits_status_warn {
color: var(--morebits-color-warning);
}
.morebits_status_error {
color: var(--morebits-color-warning);
font-weight: bold;
}
/* Morebits.QuickForm */
form.quickform {
box-sizing: border-box;
width: 98%;
height: 100%;
min-height: 0;
margin: 0 auto;
padding: 0.5em;
color: var(--color-base, #202122);
}
/* use flex layout for quickform to make the scrollbox resize with
the dialog, and to keep it from obscuring elements below it */
form.quickform.has-scrollbox {
display: flex;
flex-direction: column;
}
form.quickform * {
font-family: sans-serif;
}
form.quickform fieldset {
margin: 0.4em 0 1em;
}
form.quickform legend {
color: var(--morebits-color-legend);
font-weight: bold;
}
form.quickform input[type=text],
form.quickform select {
min-width: 15em;
font-size: 110%;
}
form.quickform select {
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-left: 0.2em;
}
form.quickform input[type=checkbox],
form.quickform input[type=radio] {
height: 13px;
margin-top: 2px;
margin-right: 2px;
margin-bottom: 2px;
padding: 0;
width: 13px;
vertical-align: top;
}
form.quickform div {
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
line-height: 18px;
}
form.quickform h5 {
margin: 0.5em 0 0;
padding: 0.3em 0.2em 0.2em;
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
/* only give the top border to headers with something above them */
form.quickform div + h5,
form.quickform h5 + h5,
form.quickform div + div > h5 {
border-top: 1px solid #88a;
}
form.quickform textarea {
width: 100%;
height: 4em;
font-size: 150%;
}
form.quickform input:disabled + label {
color: var(--color-disabled, #72777d);
}
form.quickform span.quickformDescription {
font-style: italic;
}
form.quickform span.quickformDescription code {
font-style: normal;
font-family: monospace;
}
form.quickform .quickformSubgroup {
margin-bottom: 0.5em;
margin-left: 3em;
}
/* The tooltip button and the content itself */
form.quickform .morebits-tooltipButton {
color: var(--morebits-color-tooltip);
font-weight: bold;
cursor: help;
padding: 0.3em;
}
.morebits-ui-tooltip {
position: absolute;
z-index: 1005;
padding: 4px 6px 4px 6px;
max-width: 300px;
transition: opacity 600ms ease;
opacity: 0;
visibility: hidden;
pointer-events: none;
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
font-size: 13px;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 2px solid var(--border-color-base, #a2ab91);
box-shadow: 0 0 5px #a2ab91;
}
.morebits-ui-tooltip.visible {
opacity: 1;
visibility: visible;
}
/* Scrollbox styles, for use within Morebits.SimpleWindow */
div.morebits-scrollbox {
background: var(--background-color-base, #fff);
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-bottom: 0.6em;
margin-top: 0.6em;
height: auto;
flex-grow: 1;
min-height: 0;
overflow: auto;
padding: 6px 6px 0;
}
div.morebits-scrollbox > h5:first-child {
border: 0;
margin-top: 0;
padding-top: 0;
}
div.morebits-scrollbox > :last-child {
margin-bottom: 6px;
}
/* Previewbox */
div.morebits-previewbox {
border: 2px inset;
margin: 0.4em auto 0.2em;
padding: 0.2em 0.4em;
}
div.morebits-usertext {
border: 1px solid #a2a9b1;
background-color: var(--background-color-neutral-subtle, #f8f9fa);
padding: 5px;
font-size: 95%;
}
/* Morebits.SimpleWindow */
.morebits-dialog {
padding: 0.2em;
border: 1px #666 solid;
font-family: sans-serif;
background-color: var(--morebits-bgcolor-dialog);
display: flex;
flex-direction: column;
position: absolute;
overflow: hidden;
min-width: 150px;
min-height: 150px;
/* top, left, width, height, max-height, and z-index are set via JS */
}
.morebits-dialog-resizer {
position: absolute;
right: -5px;
bottom: -5px;
height: 20px;
width: 20px;
cursor: se-resize;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+Cgk8ZyBzdHJva2U9IiM3MmE3Y2UiPgoJCTxwYXRoIGQ9Im0wIDEwTDEwIDAiLz4KCQk8cGF0aCBkPSJtNCAxMEwxMCA0Ii8+CgkJPHBhdGggZD0ibTggMTBMMTAgOCIvPgoJPC9nPgo8L3N2Zz4K);
}
/* px translations in comments are w.r.t standard browser settings,
in other settings, the sizes would be scaled accordingly */
.skin-vector .morebits-dialog {
font-size: 75%; /* 100% is 16px => 75% is 12px */
}
.skin-timeless .morebits-dialog {
font-size: 79%; /* 100% is 15.2px => 79% is 12.008px */
}
.skin-monobook .morebits-dialog,
.skin-modern .morebits-dialog {
font-size: 120%; /* 100% is 10px => 120% is 12px */
}
.morebits-dialog .morebits-dialog-titlebar {
height: 1em;
min-height: 1em;
background-color: var(--morebits-bgcolor-titlebar);
font: bold 108% sans-serif; /* 100% is 12px (from above) => 108% is 12.96px */
overflow: hidden;
padding: 0.4em 0.3em 0.5em;
white-space: nowrap;
display: flex; /* needed so that align-items can be used */
align-items: center; /* to vertically center the close button */
cursor: move;
position: relative; /* so that the close button can be positioned relative to titlebar */
}
.morebits-dialog-scriptname {
font-weight: normal;
}
.morebits-dialog .morebits-dialog-close {
position: absolute;
right: 0;
background-color: transparent;
border: 0;
font-size: 1.2em;
font-weight: bold;
padding: 5px 10px;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s, color 0.2s;
}
.morebits-dialog .morebits-dialog-close:hover {
background-color: #eee;
color: #000;
}
.morebits-dialog .morebits-dialog-close span {
margin: 0.33em;
}
.morebits-dialog .morebits-dialog-content {
padding: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 100%;
flex-grow: 1;
overflow-y: auto;
min-height: 0;
}
.morebits-dialog .morebits-dialog-buttonpane {
background-color: var(--morebits-bgcolor-titlebar);
margin: auto 0 0;
padding: 0.3em 1.4em 0.5em 1.2em;
}
.morebits-dialog .morebits-dialog-buttonpane button {
margin-top: 0.2em;
}
.morebits-dialog-buttons {
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
.morebits-dialog-footerlinks {
font-size: 97%; /* 100% is 12px (from above) => 97% is 11.64px */
float: right;
margin: 0.7em 0.4em 0 0;
}
.morebits-dialog .morebits-dialog-footerlinks a {
color: var(--morebits-color-titlebar-links);
}
.morebits-dialog-buttons[data-empty] + .morebits-dialog-footerlinks {
margin: 0.1em 0.4em -0.2em 0;
}
.morebits-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
z-index: 700; /* Higher than morebits-dialog but lesser than morebits-dialog-modal */
}
.morebits-dialog.morebits-dialog-modal {
z-index: 701;
}
/* Override select2 silliness */
.select2-morebits,
.select2-morebits .select2-results,
.select2-morebits .select2-results__option[aria-selected=true],
.select2-morebits .select2-selection__rendered,
.select2-morebits .select2-selection--multiple,
.select2-morebits .select2-selection__choice {
background: var(--background-color-base, #fff) !important;
color: inherit !important;
}
1nt3dem73x7ryytxyp84le2lgfeac15
MediaWiki:Gadget-select2.min.js
8
29072
326095
2026-04-04T08:56:46Z
Kannotlogin
29153
nieuw blad: // <nowiki> /*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */ !function(n){"function"==typeof define&&define.amd?define(["jquery"],n):/*"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:*/n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,w;functi…
326095
javascript
text/javascript
// <nowiki>
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):/*"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:*/n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,w;function b(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u<e.length;u++)if("."===(p=e[u]))e.splice(u,1),u-=1;else if(".."===p){if(0===u||1===u&&".."===e[2]||".."===e[u-1])continue;0<u&&(e.splice(u-1,2),u-=2)}e=e.join("/")}if((h||g)&&f){for(u=(n=e.split("/")).length;0<u;u-=1){if(r=n.slice(0,u).join("/"),h)for(d=h.length;0<d;d-=1)if(i=(i=f[h.slice(0,d).join("/")])&&i[r]){o=i,a=u;break}if(o)break;!l&&g&&g[r]&&(l=g[r],c=u)}!o&&l&&(o=l,a=c),o&&(n.splice(0,a,o),e=n.join("/"))}return e}function A(t,n){return function(){var e=a.call(arguments,0);return"string"!=typeof e[0]&&1===e.length&&e.push(null),s.apply(h,e.concat([t,n]))}}function x(t){return function(e){m[t]=e}}function D(e){if(b(v,e)){var t=v[e];delete v[e],_[e]=!0,o.apply(h,t)}if(!b(m,e)&&!b(_,e))throw new Error("No "+e);return m[e]}function c(e){var t,n=e?e.indexOf("!"):-1;return-1<n&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return e?c(e):[]}return e&&e.requirejs||(e?n=e:e={},m={},v={},y={},_={},i=Object.prototype.hasOwnProperty,a=[].slice,w=/\.js$/,f=function(e,t){var n,r=c(e),i=r[0],o=t[1];return e=r[1],i&&(n=D(i=l(i,o))),i?e=n&&n.normalize?n.normalize(e,function(t){return function(e){return l(e,t)}}(o)):l(e,o):(i=(r=c(e=l(e,o)))[0],e=r[1],i&&(n=D(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},g={require:function(e){return A(e)},exports:function(e){var t=m[e];return void 0!==t?t:m[e]={}},module:function(e){return{id:e,uri:"",exports:m[e],config:function(e){return function(){return y&&y.config&&y.config[e]||{}}}(e)}}},o=function(e,t,n,r){var i,o,s,a,l,c,u,d=[],p=typeof n;if(c=S(r=r||e),"undefined"==p||"function"==p){for(t=!t.length&&n.length?["require","exports","module"]:t,l=0;l<t.length;l+=1)if("require"===(o=(a=f(t[l],c)).f))d[l]=g.require(e);else if("exports"===o)d[l]=g.exports(e),u=!0;else if("module"===o)i=d[l]=g.module(e);else if(b(m,o)||b(v,o)||b(_,o))d[l]=D(o);else{if(!a.p)throw new Error(e+" missing "+o);a.p.load(a.n,A(r,!0),x(o),{}),d[l]=m[o]}s=n?n.apply(m[e],d):void 0,e&&(i&&i.exports!==h&&i.exports!==m[e]?m[e]=i.exports:s===h&&u||(m[e]=s))}else e&&(m[e]=n)},t=n=s=function(e,t,n,r,i){if("string"==typeof e)return g[e]?g[e](t):D(f(e,S(t)).f);if(!e.splice){if((y=e).deps&&s(y.deps,y.callback),!t)return;t.splice?(e=t,t=n,n=null):e=h}return t=t||function(){},"function"==typeof n&&(n=r,r=i),r?o(h,e,t,n):setTimeout(function(){o(h,e,t,n)},4),s},s.config=function(e){return s(e)},t._defined=m,(r=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),b(m,e)||b(v,e)||(v[e]=[e,t,n])}).amd={jQuery:!0},e.requirejs=t,e.require=n,e.define=r),e.define("almond",function(){}),e.define("jquery",[],function(){var e=u||$;return null==e&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),e}),e.define("select2/utils",["jquery"],function(o){var i={};function u(e){var t=e.prototype,n=[];for(var r in t){"function"==typeof t[r]&&"constructor"!==r&&n.push(r)}return n}i.Extend=function(e,t){var n={}.hasOwnProperty;function r(){this.constructor=e}for(var i in t)n.call(t,i)&&(e[i]=t[i]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},i.Decorate=function(r,i){var e=u(i),t=u(r);function o(){var e=Array.prototype.unshift,t=i.prototype.constructor.length,n=r.prototype.constructor;0<t&&(e.call(arguments,r.prototype.constructor),n=i.prototype.constructor),n.apply(this,arguments)}i.displayName=r.displayName,o.prototype=new function(){this.constructor=o};for(var n=0;n<t.length;n++){var s=t[n];o.prototype[s]=r.prototype[s]}function a(e){var t=function(){};e in o.prototype&&(t=o.prototype[e]);var n=i.prototype[e];return function(){return Array.prototype.unshift.call(arguments,t),n.apply(this,arguments)}}for(var l=0;l<e.length;l++){var c=e[l];o.prototype[c]=a(c)}return o};function e(){this.listeners={}}e.prototype.on=function(e,t){this.listeners=this.listeners||{},e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t]},e.prototype.trigger=function(e){var t=Array.prototype.slice,n=t.call(arguments,1);this.listeners=this.listeners||{},null==n&&(n=[]),0===n.length&&n.push({}),(n[0]._type=e)in this.listeners&&this.invoke(this.listeners[e],t.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},e.prototype.invoke=function(e,t){for(var n=0,r=e.length;n<r;n++)e[n].apply(this,t)},i.Observable=e,i.generateChars=function(e){for(var t="",n=0;n<e;n++){t+=Math.floor(36*Math.random()).toString(36)}return t},i.bind=function(e,t){return function(){e.apply(t,arguments)}},i._convertData=function(e){for(var t in e){var n=t.split("-"),r=e;if(1!==n.length){for(var i=0;i<n.length;i++){var o=n[i];(o=o.substring(0,1).toLowerCase()+o.substring(1))in r||(r[o]={}),i==n.length-1&&(r[o]=e[t]),r=r[o]}delete e[t]}}return e},i.hasScroll=function(e,t){var n=o(t),r=t.style.overflowX,i=t.style.overflowY;return(r!==i||"hidden"!==i&&"visible"!==i)&&("scroll"===r||"scroll"===i||(n.innerHeight()<t.scrollHeight||n.innerWidth()<t.scrollWidth))},i.escapeMarkup=function(e){var t={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('<ul class="select2-results__options" role="listbox"></ul>');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h('<li role="alert" aria-live="assertive" class="select2-results__option"></li>'),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n<e.results.length;n++){var r=e.results[n],i=this.option(r);t.push(i)}this.$results.append(t)}else 0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"})},r.prototype.position=function(e,t){t.find(".select2-results").append(e)},r.prototype.sort=function(e){return this.options.get("sorter")(e)},r.prototype.highlightFirstItem=function(){var e=this.$results.find(".select2-results__option[aria-selected]"),t=e.filter("[aria-selected=true]");0<t.length?t.first().trigger("mouseenter"):e.first().trigger("mouseenter"),this.ensureHighlightVisible()},r.prototype.setClasses=function(){var t=this;this.data.current(function(e){var r=h.map(e,function(e){return e.id.toString()});t.$results.find(".select2-results__option[aria-selected]").each(function(){var e=h(this),t=f.GetData(this,"data"),n=""+t.id;null!=t.element&&t.element.selected||null==t.element&&-1<h.inArray(n,r)?e.attr("aria-selected","true"):e.attr("aria-selected","false")})})},r.prototype.showLoading=function(e){this.hideLoading();var t={disabled:!0,loading:!0,text:this.options.get("translations").get("searching")(e)},n=this.option(t);n.className+=" loading-results",this.$results.prepend(n)},r.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},r.prototype.option=function(e){var t=document.createElement("li");t.className="select2-results__option";var n={role:"option","aria-selected":"false"},r=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;for(var i in(null!=e.element&&r.call(e.element,":disabled")||null==e.element&&e.disabled)&&(delete n["aria-selected"],n["aria-disabled"]="true"),null==e.id&&delete n["aria-selected"],null!=e._resultId&&(t.id=e._resultId),e.title&&(t.title=e.title),e.children&&(n.role="group",n["aria-label"]=e.text,delete n["aria-selected"]),n){var o=n[i];t.setAttribute(i,o)}if(e.children){var s=h(t),a=document.createElement("strong");a.className="select2-results__group";h(a);this.template(e,a);for(var l=[],c=0;c<e.children.length;c++){var u=e.children[c],d=this.option(u);l.push(d)}var p=h("<ul></ul>",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):i<o&&l.$results.scrollTop(s)}}),t.on("results:focus",function(e){e.element.addClass("select2-results__option--highlighted")}),t.on("results:message",function(e){l.displayMessage(e)}),h.fn.mousewheel&&this.$results.on("mousewheel",function(e){var t=l.$results.scrollTop(),n=l.$results.get(0).scrollHeight-t+e.deltaY,r=0<e.deltaY&&t-e.deltaY<=0,i=e.deltaY<0&&n<=l.$results.height();r?(l.$results.scrollTop(0),e.preventDefault(),e.stopPropagation()):i&&(l.$results.scrollTop(l.$results.get(0).scrollHeight-l.$results.height()),e.preventDefault(),e.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(e){var t=h(this),n=f.GetData(this,"data");"true"!==t.attr("aria-selected")?l.trigger("select",{originalEvent:e,data:n}):l.options.get("multiple")?l.trigger("unselect",{originalEvent:e,data:n}):l.trigger("close",{})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(e){var t=f.GetData(this,"data");l.getHighlightedResults().removeClass("select2-results__option--highlighted"),l.trigger("results:focus",{data:t,element:h(this)})})},r.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},r.prototype.destroy=function(){this.$results.remove()},r.prototype.ensureHighlightVisible=function(){var e=this.getHighlightedResults();if(0!==e.length){var t=this.$results.find("[aria-selected]").index(e),n=this.$results.offset().top,r=e.offset().top,i=this.$results.scrollTop()+(r-n),o=r-n;i-=2*e.outerHeight(!1),t<=2?this.$results.scrollTop(0):(o>this.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("<span></span>")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('<ul class="select2-selection__rendered"></ul>'),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.options.get("disabled")){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n<e.length;n++){var r=e[n],i=this.selectionContainer(),o=this.display(r,i);i.append(o);var s=r.title||r.text;s&&i.attr("title",s),l.StoreData(i[0],"data",r),t.push(i)}var a=this.$selection.find(".select2-selection__rendered");l.appendMany(a,t)}},n}),e.define("select2/selection/placeholder",["../utils"],function(e){function t(e,t,n){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n)}return t.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},t.prototype.createPlaceholder=function(e,t){var n=this.selectionContainer();return n.html(this.display(t)),n.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),n},t.prototype.update=function(e,t){var n=1==t.length&&t[0].id!=this.placeholder.id;if(1<t.length||n)return e.call(this,t);this.clear();var r=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(r)},t}),e.define("select2/selection/allowClear",["jquery","../keys","../utils"],function(i,r,a){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(e){r._handleClear(e)}),t.on("keypress",function(e){r._handleKeyboardClear(e,t)})},e.prototype._handleClear=function(e,t){if(!this.options.get("disabled")){var n=this.$selection.find(".select2-selection__clear");if(0!==n.length){t.stopPropagation();var r=a.GetData(n[0],"data"),i=this.$element.val();this.$element.val(this.placeholder.id);var o={data:r};if(this.trigger("clear",o),o.prevented)this.$element.val(i);else{for(var s=0;s<r.length;s++)if(o={data:r[s]},this.trigger("unselect",o),o.prevented)return void this.$element.val(i);this.$element.trigger("change"),this.trigger("toggle",{})}}}},e.prototype._handleKeyboardClear=function(e,t,n){n.isOpen()||t.which!=r.DELETE&&t.which!=r.BACKSPACE||this._handleClear(t)},e.prototype.update=function(e,t){if(e.call(this,t),!(0<this.$selection.find(".select2-selection__placeholder").length||0===t.length)){var n=this.options.get("translations").get("removeAllItems"),r=i('<span class="select2-selection__clear" title="'+n()+'">×</span>');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></li>');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0<t.length){var n=a.GetData(t[0],"data");r.searchRemoveChoice(n),e.preventDefault()}}}),this.$selection.on("click",".select2-search--inline",function(e){r.$search.val()&&e.stopPropagation()});var o=document.documentMode,s=o&&o<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){s?r.$selection.off("input.search input.searchcheck"):r.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){if(s&&"input"===e.type)r.$selection.off("input.search input.searchcheck");else{var t=e.which;t!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&r.handleSearch(e)}})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var e=this.$search.val();this.trigger("query",{term:e})}this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="";""!==this.$search.attr("placeholder")?e=this.$selection.find(".select2-selection__rendered").width():e=.75*(this.$search.val().length+1)+"em";this.$search.css("width",e)},e}),e.define("select2/selection/eventRelay",["jquery"],function(s){function e(){}return e.prototype.bind=function(e,t,n){var r=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],o=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){if(-1!==s.inArray(e,i)){t=t||{};var n=s.Event("select2:"+e,{params:t});r.$element.trigger(n),-1!==s.inArray(e,o)&&(t.prevented=n.isDefaultPrevented())}})},e}),e.define("select2/translation",["jquery","require"],function(t,n){function r(e){this.dict=e||{}}return r.prototype.all=function(){return this.dict},r.prototype.get=function(e){return this.dict[e]},r.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},r._cache={},r.loadPath=function(e){if(!(e in r._cache)){var t=n(e);r._cache[e]=t}return new r(r._cache[e])},r}),e.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),e.define("select2/data/base",["../utils"],function(r){function n(e,t){n.__super__.constructor.call(this)}return r.Extend(n,r.Observable),n.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},n.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},n.prototype.bind=function(e,t){},n.prototype.destroy=function(){},n.prototype.generateResultId=function(e,t){var n=e.id+"-result-";return n+=r.generateChars(4),null!=t.id?n+="-"+t.id.toString():n+="-"+r.generateChars(4),n},n}),e.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var n=[],r=this;this.$element.find(":selected").each(function(){var e=l(this),t=r.item(e);n.push(t)}),e(n)},n.prototype.select=function(i){var o=this;if(i.selected=!0,l(i.element).is("option"))return i.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;n<i.length;n++){var r=i[n].id;-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")});else{var e=i.id;this.$element.val(e),this.$element.trigger("change")}},n.prototype.unselect=function(i){var o=this;if(this.$element.prop("multiple")){if(i.selected=!1,l(i.element).is("option"))return i.element.selected=!1,void this.$element.trigger("change");this.current(function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n].id;r!==i.id&&-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")})}},n.prototype.bind=function(e,t){var n=this;(this.container=e).on("select",function(e){n.select(e.data)}),e.on("unselect",function(e){n.unselect(e.data)})},n.prototype.destroy=function(){this.$element.find("*").each(function(){a.RemoveData(this)})},n.prototype.query=function(r,e){var i=[],o=this;this.$element.children().each(function(){var e=l(this);if(e.is("option")||e.is("optgroup")){var t=o.item(e),n=o.matches(r,t);null!==n&&i.push(n)}}),e({results:i})},n.prototype.addOptions=function(e){a.appendMany(this.$element,e)},n.prototype.option=function(e){var t;e.children?(t=document.createElement("optgroup")).label=e.text:void 0!==(t=document.createElement("option")).textContent?t.textContent=e.text:t.innerText=e.text,void 0!==e.id&&(t.value=e.id),e.disabled&&(t.disabled=!0),e.selected&&(t.selected=!0),e.title&&(t.title=e.title);var n=l(t),r=this._normalizeItem(e);return r.element=t,a.StoreData(t,"data",r),n},n.prototype.item=function(e){var t={};if(null!=(t=a.GetData(e[0],"data")))return t;if(e.is("option"))t={id:e.val(),text:e.text(),disabled:e.prop("disabled"),selected:e.prop("selected"),title:e.prop("title")};else if(e.is("optgroup")){t={text:e.prop("label"),children:[],title:e.prop("title")};for(var n=e.children("option"),r=[],i=0;i<n.length;i++){var o=l(n[i]),s=this.item(o);r.push(s)}t.children=r}return(t=this._normalizeItem(t)).element=e[0],a.StoreData(e[0],"data",t),t},n.prototype._normalizeItem=function(e){e!==Object(e)&&(e={id:e,text:e});return null!=(e=l.extend({},{text:""},e)).id&&(e.id=e.id.toString()),null!=e.text&&(e.text=e.text.toString()),null==e._resultId&&e.id&&null!=this.container&&(e._resultId=this.generateResultId(this.container,e)),l.extend({},{selected:!1,disabled:!1},e)},n.prototype.matches=function(e,t){return this.options.get("matcher")(e,t)},n}),e.define("select2/data/array",["./select","../utils","jquery"],function(e,f,g){function r(e,t){this._dataToConvert=t.get("data")||[],r.__super__.constructor.call(this,e,t)}return f.Extend(r,e),r.prototype.bind=function(e,t){r.__super__.bind.call(this,e,t),this.addOptions(this.convertToOptions(this._dataToConvert))},r.prototype.select=function(n){var e=this.$element.find("option").filter(function(e,t){return t.value==n.id.toString()});0===e.length&&(e=this.option(n),this.addOptions(e)),r.__super__.select.call(this,n)},r.prototype.convertToOptions=function(e){var t=this,n=this.$element.find("option"),r=n.map(function(){return t.item(g(this)).id}).get(),i=[];function o(e){return function(){return g(this).val()==e.id}}for(var s=0;s<e.length;s++){var a=this._normalizeItem(e[s]);if(0<=g.inArray(a.id,r)){var l=n.filter(o(a)),c=this.item(l),u=g.extend(!0,{},a,c),d=this.option(u);l.replaceWith(d)}else{var p=this.option(a);if(a.children){var h=this.convertToOptions(a.children);f.appendMany(p,h)}i.push(p)}}return i},r}),e.define("select2/data/ajax",["./array","../utils","jquery"],function(e,t,o){function n(e,t){this.ajaxOptions=this._applyDefaults(t.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),n.__super__.constructor.call(this,e,t)}return t.Extend(n,e),n.prototype._applyDefaults=function(e){var t={data:function(e){return o.extend({},e,{q:e.term})},transport:function(e,t,n){var r=o.ajax(e);return r.then(t),r.fail(n),r}};return o.extend({},t,e,!0)},n.prototype.processResults=function(e){return e},n.prototype.query=function(n,r){var i=this;null!=this._request&&(o.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var t=o.extend({type:"GET"},this.ajaxOptions);function e(){var e=t.transport(t,function(e){var t=i.processResults(e,n);i.options.get("debug")&&window.console&&console.error&&(t&&t.results&&o.isArray(t.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),r(t)},function(){"status"in e&&(0===e.status||"0"===e.status)||i.trigger("results:message",{message:"errorLoading"})});i._request=e}"function"==typeof t.url&&(t.url=t.url.call(this.$element,n)),"function"==typeof t.data&&(t.data=t.data.call(this.$element,n)),this.ajaxOptions.delay&&null!=n.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(e,this.ajaxOptions.delay)):e()},n}),e.define("select2/data/tags",["jquery"],function(u){function e(e,t,n){var r=n.get("tags"),i=n.get("createTag");void 0!==i&&(this.createTag=i);var o=n.get("insertTag");if(void 0!==o&&(this.insertTag=o),e.call(this,t,n),u.isArray(r))for(var s=0;s<r.length;s++){var a=r[s],l=this._normalizeItem(a),c=this.option(l);this.$element.append(c)}}return e.prototype.query=function(e,c,u){var d=this;this._removeOldTags(),null!=c.term&&null==c.page?e.call(this,c,function e(t,n){for(var r=t.results,i=0;i<r.length;i++){var o=r[i],s=null!=o.children&&!e({results:o.children},!0);if((o.text||"").toUpperCase()===(c.term||"").toUpperCase()||s)return!n&&(t.data=r,void u(t))}if(n)return!0;var a=d.createTag(c);if(null!=a){var l=d.option(a);l.attr("data-select2-tag",!0),d.addOptions([l]),d.insertTag(r,a)}t.results=r,u(t)}):e.call(this,c,u)},e.prototype.createTag=function(e,t){var n=u.trim(t.term);return""===n?null:{id:n,text:n}},e.prototype.insertTag=function(e,t,n){t.unshift(n)},e.prototype._removeOldTags=function(e){this.$element.find("option[data-select2-tag]").each(function(){this.selected||u(this).remove()})},e}),e.define("select2/data/tokenizer",["jquery"],function(d){function e(e,t,n){var r=n.get("tokenizer");void 0!==r&&(this.tokenizer=r),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){e.call(this,t,n),this.$search=t.dropdown.$search||t.selection.$search||n.find(".select2-search__field")},e.prototype.query=function(e,t,n){var r=this;t.term=t.term||"";var i=this.tokenizer(t,this.options,function(e){var t=r._normalizeItem(e);if(!r.$element.find("option").filter(function(){return d(this).val()===t.id}).length){var n=r.option(t);n.attr("data-select2-tag",!0),r._removeOldTags(),r.addOptions([n])}!function(e){r.trigger("select",{data:e})}(t)});i.term!==t.term&&(this.$search.length&&(this.$search.val(i.term),this.$search.trigger("focus")),t.term=i.term),e.call(this,t,n)},e.prototype.tokenizer=function(e,t,n,r){for(var i=n.get("tokenSeparators")||[],o=t.term,s=0,a=this.createTag||function(e){return{id:e.term,text:e.term}};s<o.length;){var l=o[s];if(-1!==d.inArray(l,i)){var c=o.substr(0,s),u=a(d.extend({},t,{term:c}));null!=u?(r(u),o=o.substr(s+1)||"",s=0):s++}else s++}return{term:o}},e}),e.define("select2/data/minimumInputLength",[],function(){function e(e,t,n){this.minimumInputLength=n.get("minimumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",t.term.length<this.minimumInputLength?this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumInputLength",[],function(){function e(e,t,n){this.maximumInputLength=n.get("maximumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",0<this.maximumInputLength&&t.term.length>this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0<r.maximumSelectionLength&&t>=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('<span class="select2-dropdown"><span class="select2-results"></span></span>');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></span>');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('<li class="select2-results__option select2-results__option--load-more"role="option" aria-disabled="true"></li>'),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f("<span></span>"),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=a<i.top-s,u=l>i.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r<t.length;r++){var i=t[r];i.children?n+=e(i.children):n++}return n}(t.data.results)<this.minimumResultsForSearch)&&e.call(this,t)},e}),e.define("select2/dropdown/selectOnClose",["../utils"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("close",function(e){r._handleSelectOnClose(e)})},e.prototype._handleSelectOnClose=function(e,t){if(t&&null!=t.originalSelect2Event){var n=t.originalSelect2Event;if("select"===n._type||"unselect"===n._type)return}var r=this.getHighlightedResults();if(!(r.length<1)){var i=o.GetData(r[0],"data");null!=i.element&&i.element.selected||null==i.element&&i.selected||this.trigger("select",{data:i})}},e}),e.define("select2/dropdown/closeOnSelect",[],function(){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(e){r._selectTriggered(e)}),t.on("unselect",function(e){r._selectTriggered(e)})},e.prototype._selectTriggered=function(e,t){var n=t.originalEvent;n&&(n.ctrlKey||n.metaKey)||this.trigger("close",{originalEvent:n,originalSelect2Event:t})},e}),e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return 1!=t&&(n+="s"),n},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return 1!=e.maximum&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(c,u,d,p,h,f,g,m,v,y,s,t,_,$,w,b,A,x,D,S,E,C,O,T,q,L,I,j,e){function n(){this.reset()}return n.prototype.apply=function(e){if(null==(e=c.extend(!0,{},this.defaults,e)).dataAdapter){if(null!=e.ajax?e.dataAdapter=w:null!=e.data?e.dataAdapter=$:e.dataAdapter=_,0<e.minimumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,x)),0<e.maximumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,D)),0<e.maximumSelectionLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,S)),e.tags&&(e.dataAdapter=y.Decorate(e.dataAdapter,b)),null==e.tokenSeparators&&null==e.tokenizer||(e.dataAdapter=y.Decorate(e.dataAdapter,A)),null!=e.query){var t=u(e.amdBase+"compat/query");e.dataAdapter=y.Decorate(e.dataAdapter,t)}if(null!=e.initSelection){var n=u(e.amdBase+"compat/initSelection");e.dataAdapter=y.Decorate(e.dataAdapter,n)}}if(null==e.resultsAdapter&&(e.resultsAdapter=d,null!=e.ajax&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,T)),null!=e.placeholder&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,O)),e.selectOnClose&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,I))),null==e.dropdownAdapter){if(e.multiple)e.dropdownAdapter=E;else{var r=y.Decorate(E,C);e.dropdownAdapter=r}if(0!==e.minimumResultsForSearch&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,L)),e.closeOnSelect&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,j)),null!=e.dropdownCssClass||null!=e.dropdownCss||null!=e.adaptDropdownCssClass){var i=u(e.amdBase+"compat/dropdownCss");e.dropdownAdapter=y.Decorate(e.dropdownAdapter,i)}e.dropdownAdapter=y.Decorate(e.dropdownAdapter,q)}if(null==e.selectionAdapter){if(e.multiple?e.selectionAdapter=h:e.selectionAdapter=p,null!=e.placeholder&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,f)),e.allowClear&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,g)),e.multiple&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,m)),null!=e.containerCssClass||null!=e.containerCss||null!=e.adaptContainerCssClass){var o=u(e.amdBase+"compat/containerCss");e.selectionAdapter=y.Decorate(e.selectionAdapter,o)}e.selectionAdapter=y.Decorate(e.selectionAdapter,v)}e.language=this._resolveLanguage(e.language),e.language.push("en");for(var s=[],a=0;a<e.language.length;a++){var l=e.language[a];-1===s.indexOf(l)&&s.push(l)}return e.language=s,e.translations=this._processTranslations(e.language,e.debug),e},n.prototype.reset=function(){function a(e){return e.replace(/[^\u0000-\u007E]/g,function(e){return t[e]||e})}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:y.escapeMarkup,language:{},matcher:function e(t,n){if(""===c.trim(t.term))return n;if(n.children&&0<n.children.length){for(var r=c.extend(!0,{},n),i=n.children.length-1;0<=i;i--)null==e(t,n.children[i])&&r.children.splice(i,1);return 0<r.children.length?r:e(t,r)}var o=a(n.text).toUpperCase(),s=a(t.term).toUpperCase();return-1<o.indexOf(s)?n:null},minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,scrollAfterSelect:!1,sorter:function(e){return e},templateResult:function(e){return e.text},templateSelection:function(e){return e.text},theme:"default",width:"resolve"}},n.prototype.applyFromElement=function(e,t){var n=e.language,r=this.defaults.language,i=t.prop("lang"),o=t.closest("[lang]").prop("lang"),s=Array.prototype.concat.call(this._resolveLanguage(i),this._resolveLanguage(n),this._resolveLanguage(r),this._resolveLanguage(o));return e.language=s,e},n.prototype._resolveLanguage=function(e){if(!e)return[];if(c.isEmptyObject(e))return[];if(c.isPlainObject(e))return[e];var t;t=c.isArray(e)?e:[e];for(var n=[],r=0;r<t.length;r++)if(n.push(t[r]),"string"==typeof t[r]&&0<t[r].indexOf("-")){var i=t[r].split("-")[0];n.push(i)}return n},n.prototype._processTranslations=function(e,t){for(var n=new s,r=0;r<e.length;r++){var i=new s,o=e[r];if("string"==typeof o)try{i=s.loadPath(o)}catch(e){try{o=this.defaults.amdLanguageBase+o,i=s.loadPath(o)}catch(e){t&&window.console&&console.warn&&console.warn('Select2: The language file for "'+o+'" could not be automatically loaded. A fallback will be used instead.')}}else i=c.isPlainObject(o)?new s(o):o;n.extend(i)}return n},n.prototype.set=function(e,t){var n={};n[c.camelCase(e)]=t;var r=y._convertData(n);c.extend(!0,this.defaults,r)},new n}),e.define("select2/options",["require","jquery","./defaults","./utils"],function(r,d,i,p){function e(e,t){if(this.options=e,null!=t&&this.fromElement(t),null!=t&&(this.options=i.applyFromElement(this.options,t)),this.options=i.apply(this.options),t&&t.is("input")){var n=r(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=p.Decorate(this.options.dataAdapter,n)}}return e.prototype.fromElement=function(e){var t=["select2"];null==this.options.multiple&&(this.options.multiple=e.prop("multiple")),null==this.options.disabled&&(this.options.disabled=e.prop("disabled")),null==this.options.dir&&(e.prop("dir")?this.options.dir=e.prop("dir"):e.closest("[dir]").prop("dir")?this.options.dir=e.closest("[dir]").prop("dir"):this.options.dir="ltr"),e.prop("disabled",this.options.disabled),e.prop("multiple",this.options.multiple),p.GetData(e[0],"select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),p.StoreData(e[0],"data",p.GetData(e[0],"select2Tags")),p.StoreData(e[0],"tags",!0)),p.GetData(e[0],"ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),e.attr("ajax--url",p.GetData(e[0],"ajaxUrl")),p.StoreData(e[0],"ajax-Url",p.GetData(e[0],"ajaxUrl")));var n={};function r(e,t){return t.toUpperCase()}for(var i=0;i<e[0].attributes.length;i++){var o=e[0].attributes[i].name,s="data-";if(o.substr(0,s.length)==s){var a=o.substring(s.length),l=p.GetData(e[0],a);n[a.replace(/-([a-z])/g,r)]=l}}d.fn.jquery&&"1."==d.fn.jquery.substr(0,2)&&e[0].dataset&&(n=d.extend(!0,{},e[0].dataset,n));var c=d.extend(!0,{},p.GetData(e[0]),n);for(var u in c=p._convertData(c))-1<d.inArray(u,t)||(d.isPlainObject(this.options[u])?d.extend(this.options[u],c[u]):this.options[u]=c[u]);return this},e.prototype.get=function(e){return this.options[e]},e.prototype.set=function(e,t){this.options[e]=t},e}),e.define("select2/core",["jquery","./options","./utils","./keys"],function(i,c,u,r){var d=function(e,t){null!=u.GetData(e[0],"select2")&&u.GetData(e[0],"select2").destroy(),this.$element=e,this.id=this._generateId(e),t=t||{},this.options=new c(t,e),d.__super__.constructor.call(this);var n=e.attr("tabindex")||0;u.StoreData(e[0],"old-tabindex",n),e.attr("tabindex","-1");var r=this.options.get("dataAdapter");this.dataAdapter=new r(e,this.options);var i=this.render();this._placeContainer(i);var o=this.options.get("selectionAdapter");this.selection=new o(e,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,i);var s=this.options.get("dropdownAdapter");this.dropdown=new s(e,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,i);var a=this.options.get("resultsAdapter");this.results=new a(e,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var l=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(e){l.trigger("selection:update",{data:e})}),e.addClass("select2-hidden-accessible"),e.attr("aria-hidden","true"),this._syncAttributes(),u.StoreData(e[0],"select2",this),e.data("select2",this)};return u.Extend(d,u.Observable),d.prototype._generateId=function(e){return"select2-"+(null!=e.attr("id")?e.attr("id"):null!=e.attr("name")?e.attr("name")+"-"+u.generateChars(2):u.generateChars(4)).replace(/(:|\.|\[|\]|,)/g,"")},d.prototype._placeContainer=function(e){e.insertAfter(this.$element);var t=this._resolveWidth(this.$element,this.options.get("width"));null!=t&&e.css("width",t)},d.prototype._resolveWidth=function(e,t){var n=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==t){var r=this._resolveWidth(e,"style");return null!=r?r:this._resolveWidth(e,"element")}if("element"==t){var i=e.outerWidth(!1);return i<=0?"auto":i+"px"}if("style"!=t)return"computedstyle"!=t?t:window.getComputedStyle(e[0]).width;var o=e.attr("style");if("string"!=typeof o)return null;for(var s=o.split(";"),a=0,l=s.length;a<l;a+=1){var c=s[a].replace(/\s/g,"").match(n);if(null!==c&&1<=c.length)return c[1]}return null},d.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},d.prototype._registerDomEvents=function(){var t=this;this.$element.on("change.select2",function(){t.dataAdapter.current(function(e){t.trigger("selection:update",{data:e})})}),this.$element.on("focus.select2",function(e){t.trigger("focus",e)}),this._syncA=u.bind(this._syncAttributes,this),this._syncS=u.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var e=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=e?(this._observer=new e(function(e){i.each(e,t._syncA),i.each(e,t._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",t._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",t._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",t._syncS,!1))},d.prototype._registerDataEvents=function(){var n=this;this.dataAdapter.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerSelectionEvents=function(){var n=this,r=["toggle","focus"];this.selection.on("toggle",function(){n.toggleDropdown()}),this.selection.on("focus",function(e){n.focus(e)}),this.selection.on("*",function(e,t){-1===i.inArray(e,r)&&n.trigger(e,t)})},d.prototype._registerDropdownEvents=function(){var n=this;this.dropdown.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerResultsEvents=function(){var n=this;this.results.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerEvents=function(){var n=this;this.on("open",function(){n.$container.addClass("select2-container--open")}),this.on("close",function(){n.$container.removeClass("select2-container--open")}),this.on("enable",function(){n.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){n.$container.addClass("select2-container--disabled")}),this.on("blur",function(){n.$container.removeClass("select2-container--focus")}),this.on("query",function(t){n.isOpen()||n.trigger("open",{}),this.dataAdapter.query(t,function(e){n.trigger("results:all",{data:e,query:t})})}),this.on("query:append",function(t){this.dataAdapter.query(t,function(e){n.trigger("results:append",{data:e,query:t})})}),this.on("keypress",function(e){var t=e.which;n.isOpen()?t===r.ESC||t===r.TAB||t===r.UP&&e.altKey?(n.close(),e.preventDefault()):t===r.ENTER?(n.trigger("results:select",{}),e.preventDefault()):t===r.SPACE&&e.ctrlKey?(n.trigger("results:toggle",{}),e.preventDefault()):t===r.UP?(n.trigger("results:previous",{}),e.preventDefault()):t===r.DOWN&&(n.trigger("results:next",{}),e.preventDefault()):(t===r.ENTER||t===r.SPACE||t===r.DOWN&&e.altKey)&&(n.open(),e.preventDefault())})},d.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},d.prototype._syncSubtree=function(e,t){var n=!1,r=this;if(!e||!e.target||"OPTION"===e.target.nodeName||"OPTGROUP"===e.target.nodeName){if(t)if(t.addedNodes&&0<t.addedNodes.length)for(var i=0;i<t.addedNodes.length;i++){t.addedNodes[i].selected&&(n=!0)}else t.removedNodes&&0<t.removedNodes.length&&(n=!0);else n=!0;n&&this.dataAdapter.current(function(e){r.trigger("selection:update",{data:e})})}},d.prototype.trigger=function(e,t){var n=d.__super__.trigger,r={open:"opening",close:"closing",select:"selecting",unselect:"unselecting",clear:"clearing"};if(void 0===t&&(t={}),e in r){var i=r[e],o={prevented:!1,name:e,args:t};if(n.call(this,i,o),o.prevented)return void(t.prevented=!0)}n.call(this,e,t)},d.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},d.prototype.open=function(){this.isOpen()||this.trigger("query",{})},d.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},d.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},d.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},d.prototype.focus=function(e){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},d.prototype.enable=function(e){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=e&&0!==e.length||(e=[!0]);var t=!e[0];this.$element.prop("disabled",t)},d.prototype.data=function(){this.options.get("debug")&&0<arguments.length&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var t=[];return this.dataAdapter.current(function(e){t=e}),t},d.prototype.val=function(e){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==e||0===e.length)return this.$element.val();var t=e[0];i.isArray(t)&&(t=i.map(t,function(e){return e.toString()})),this.$element.val(t).trigger("change")},d.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",u.GetData(this.$element[0],"old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),u.RemoveData(this.$element[0]),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},d.prototype.render=function(){var e=i('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1<i.inArray(t,a)?this:n}}return null==i.fn.select2.defaults&&(i.fn.select2.defaults=t),o}),{define:e.define,require:e.require}}(),t=e.require("jquery.select2");return u.fn.select2.amd=e,t});
// </nowiki>
42xu27lpnwj8a1n5oae6me1jq6szo63
MediaWiki:Gadget-select2.min.css
8
29073
326096
2026-04-04T08:56:59Z
Kannotlogin
29153
nieuw blad: /*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */ .select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidde…
326096
css
text/css
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
g1469rftkzxri386o3a0m1ueea3cqkp
MediaWiki:Gadget-Twinkle.js
8
29074
326097
2026-04-04T08:57:08Z
Kannotlogin
29153
nieuw blad: /** * +-------------------------------------------------------------------------+ * | === WARNING: GLOBAL GADGET FILE === | * | Changes to this page affect many users. | * | Please discuss changes at [[WT:TW]] before editing. | * +-------------------------------------------------------------------------+ * * Imported from github [https://github.com/wikimedia-gadgets/twinkle]. * All ch…
326097
javascript
text/javascript
/**
* +-------------------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes at [[WT:TW]] before editing. |
* +-------------------------------------------------------------------------+
*
* Imported from github [https://github.com/wikimedia-gadgets/twinkle].
* All changes should be made in the repository, otherwise they will be lost.
*
* ----------
*
* This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
* every Wikipedian in between. Visit [[WP:TW]] for more information.
*/
// <nowiki>
/* global Morebits */
(function() {
// Check if account is experienced enough to use Twinkle
if (!Morebits.userIsInGroup('autoconfirmed') && !Morebits.userIsInGroup('confirmed')) {
return;
}
const Twinkle = {};
window.Twinkle = Twinkle; // allow global access
Twinkle.initCallbacks = [];
/**
* Adds a callback to execute when Twinkle has loaded.
*
* @param {Function} func
* @param {string} [name] - name of module used to check if is disabled.
* If name is not given, module is loaded unconditionally.
*/
Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) {
Twinkle.initCallbacks.push({ func: func, name: name });
};
Twinkle.defaultConfig = {};
/**
* This holds the default set of preferences used by Twinkle.
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig = {
// General
userTalkPageMode: 'tab',
dialogLargeFont: false,
disabledModules: [],
disabledSysopModules: [],
// ARV
spiWatchReport: 'yes',
// Block
defaultToBlock64: false,
defaultToPartialBlocks: false,
blankTalkpageOnIndefBlock: false,
// Rollback
autoMenuAfterRollback: false,
openTalkPage: [ 'agf', 'norm', 'vand' ],
openTalkPageOnAutoRevert: false,
rollbackInPlace: false,
markRevertedPagesAsMinor: [ 'vand' ],
watchRevertedPages: [ 'agf', 'norm', 'vand', 'torev' ],
watchRevertedExpiry: '1 month',
offerReasonOnNormalRevert: true,
confirmOnRollback: false,
confirmOnMobileRollback: true,
showRollbackLinks: [ 'diff', 'others' ],
// DI (twinkleimage)
notifyUserOnDeli: true,
deliWatchPage: '1 month',
deliWatchUser: '1 month',
// Protect
watchRequestedPages: 'yes',
watchPPTaggedPages: 'default',
watchProtectedPages: 'default',
// PROD
watchProdPages: '1 month',
markProdPagesAsPatrolled: false,
prodReasonDefault: '',
logProdPages: false,
prodLogPageName: 'PROD log',
// CSD
speedySelectionStyle: 'buttonClick',
watchSpeedyPages: [ 'g3', 'g5', 'g10', 'g11', 'g12' ],
watchSpeedyExpiry: '1 month',
markSpeedyPagesAsPatrolled: false,
watchSpeedyUser: '1 month',
// these next two should probably be identical by default
welcomeUserOnSpeedyDeletionNotification: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ],
notifyUserOnSpeedyDeletionNomination: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ],
warnUserOnSpeedyDelete: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'g15', 'a1', 'a2', 'a3', 'a7', 'a9', 'a10', 'a11', 'c1', 'f1', 'f2', 'f3', 'f7', 'f9', 'r3', 'u6', 'u7' ],
promptForSpeedyDeletionSummary: [],
deleteTalkPageOnDelete: true,
deleteRedirectsOnDelete: true,
deleteSysopDefaultToDelete: false,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: 'CSD log',
noLogOnSpeedyNomination: [ 'u1' ],
// Unlink
unlinkNamespaces: [ '0', '10', '100', '118' ],
// Warn
defaultWarningGroup: '10',
combinedSingletMenus: false,
watchWarnings: '1 month',
oldSelect: false,
customWarningList: [],
// XfD
logXfdNominations: false,
xfdLogPageName: 'XfD log',
noLogOnXfdNomination: [],
xfdWatchDiscussion: 'default',
xfdWatchList: 'no',
xfdWatchPage: '1 month',
xfdWatchUser: '1 month',
xfdWatchRelated: '1 month',
markXfdPagesAsPatrolled: true,
// Hidden preferences
autolevelStaleDays: 3, // Huggle is 3, CBNG is 2
revertMaxRevisions: 50, // intentionally limited
batchMax: 5000,
batchChunks: 50,
// Deprecated options, as a fallback for add-on scripts/modules
summaryAd: ' ([[WP:TW|TW]])',
deletionSummaryAd: ' ([[WP:TW|TW]])',
protectionSummaryAd: ' ([[WP:TW|TW]])',
// Tag
groupByDefault: true,
watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'],
watchTaggedPages: '1 month',
watchMergeDiscussions: '1 month',
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: false,
tagArticleSortOrder: 'cat',
customTagList: [],
customFileTagList: [],
customRedirectTagList: [],
// Welcome
topWelcomes: false,
watchWelcomes: '3 months',
insertUsername: true,
quickWelcomeMode: 'norm',
quickWelcomeTemplate: 'welcome',
customWelcomeList: [],
customWelcomeSignature: true,
// Talkback
markTalkbackAsMinor: false,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: 'New message from ' + mw.config.get('wgUserName'),
mailHeading: "You've got mail!"
};
Twinkle.getPref = function twinkleGetPref(name) {
if (typeof Twinkle.prefs === 'object' && Twinkle.prefs[name] !== undefined) {
return Twinkle.prefs[name];
}
// Old preferences format, used before twinkleoptions.js was a thing
if (typeof window.TwinkleConfig === 'object' && window.TwinkleConfig[name] !== undefined) {
return window.TwinkleConfig[name];
}
if (typeof window.FriendlyConfig === 'object' && window.FriendlyConfig[name] !== undefined) {
return window.FriendlyConfig[name];
}
// Backwards compatibility code because we renamed confirmOnFluff to confirmOnRollback, and confirmOnMobileFluff to confirmOnMobileRollback
if (name === 'confirmOnRollback' && typeof Twinkle.prefs === 'object' && Twinkle.prefs.confirmOnFluff !== undefined) {
return Twinkle.prefs.confirmOnFluff;
} else if (name === 'confirmOnMobileRollback' && typeof Twinkle.prefs === 'object' && Twinkle.prefs.confirmOnMobileFluff !== undefined) {
return Twinkle.prefs.confirmOnMobileFluff;
}
return Twinkle.defaultConfig[name];
};
/**
* Adds a portlet menu to one of the navigation areas on the page.
*
* @return {string} portletId
*/
Twinkle.addPortlet = function() {
/** @type {string} id of the target navigation area (skin dependent, on vector either of "#left-navigation", "#right-navigation", or "#mw-panel") */
let navigation;
/** @type {string} id of the portlet menu to create, preferably start with "p-". */
let id;
/** @type {string} name of the portlet menu to create. Visibility depends on the class used. */
let text;
/** @type {Node} the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. */
let nextnodeid;
switch (mw.config.get('skin')) {
case 'vector':
case 'vector-2022':
navigation = '#right-navigation';
id = 'p-twinkle';
text = 'TW';
// In order to get mw.util.addPortlet to generate a dropdown menu in vector and vector-2022, the nextnodeid must be p-cactions. Any other nextnodeid will generate a non-dropdown portlet instead.
nextnodeid = 'p-cactions';
break;
case 'timeless':
navigation = '#page-tools .sidebar-inner';
id = 'p-twinkle';
text = 'Twinkle';
nextnodeid = 'p-userpagetools';
break;
default:
navigation = null;
id = 'p-cactions';
}
if (navigation === null) {
return id;
}
// make sure navigation is a valid CSS selector
const root = document.querySelector(navigation);
if (!root) {
return id;
}
// if we already created the portlet, return early. we don't want to create it again.
const item = document.getElementById(id);
if (item) {
return id;
}
mw.util.addPortlet(id, text, '#' + nextnodeid);
// The Twinkle dropdown menu has been added to the left of p-cactions, since that is the only spot that will create a dropdown menu. But we want it on the right. Move it to the right.
if (mw.config.get('skin') === 'vector') {
$('#p-twinkle').insertAfter('#p-cactions');
} else if (mw.config.get('skin') === 'vector-2022') {
const $landmark = $('#right-navigation > .vector-page-tools-landmark');
$('#p-twinkle-dropdown').insertAfter($landmark);
// .vector-page-tools-landmark is unstable and could change. If so, log it to console, to hopefully get someone's attention.
if (!$landmark) {
mw.log.warn('Unexpected change in DOM');
}
}
return id;
};
/**
* Builds a portlet menu if it doesn't exist yet, and adds a portlet link. This function runs at the top of every Twinkle module, ensuring that the first module to be loaded adds the portlet, and that every module can add a link to itself to the portlet.
*
* @param {string|Function} task Either a URL for the portlet link or a function to execute.
*/
Twinkle.addPortletLink = function(task, text, id, tooltip) {
// Create a portlet to hold all the portlet links (if not created already). And get the portletId.
const portletId = Twinkle.addPortlet();
// Create a portlet link and add it to the portlet.
const link = mw.util.addPortletLink(portletId, typeof task === 'string' ? task : '#', text, id, tooltip);
// Related to the hidden peer gadget that prevents jumpiness when the page first loads
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
// Add a click listener for the portlet link
if (typeof task === 'function') {
$(link).on('click', (ev) => {
task();
ev.preventDefault();
});
}
// $.collapsibleTabs is a feature of Vector 2010
if ($.collapsibleTabs) {
// Manually trigger a recalculation of what tabs to put where. This is to account for the space that the TW menu we just added is taking up.
$.collapsibleTabs.handleResize();
}
return link;
};
/**
* **************** General initialization code ****************
*/
// Retrieve the user's Twinkle preferences
Morebits.wiki.getCachedPage(`User:${mw.config.get('wgUserName')}/twinkleoptions.js`)
.then((optionsText) => {
if (!optionsText) {
// User has no options
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, '');
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) {
optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, '');
}
try {
const options = JSON.parse(optionsText);
if (options) {
if (options.twinkle || options.friendly) { // Old preferences format
Twinkle.prefs = $.extend(options.twinkle, options.friendly);
} else {
Twinkle.prefs = options;
}
// v2 established after unification of Twinkle/Friendly objects
Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1;
}
} catch (e) {
mw.notify('Could not parse your Twinkle preferences', {type: 'error'});
}
})
.catch(() => {
console.log('Could not load your Twinkle preferences, resorting to default preferences'); // eslint-disable-line no-console
})
.always(() => {
$(Twinkle.load);
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function () {
// Don't activate on special pages other than those listed here, so
// that others load faster, especially the watchlist.
let activeSpecialPageList = [ 'Block', 'Contributions', 'IPContributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsSysop) {
activeSpecialPageList = activeSpecialPageList.concat([ 'DeletedContributions', 'Prefixindex' ]);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
!activeSpecialPageList.includes(mw.config.get('wgCanonicalSpecialPageName'))) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.Api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')');
Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules'));
// Redefine addInitCallback so that any modules being loaded now on are directly
// initialised rather than added to initCallbacks array
Twinkle.addInitCallback = function(func, name) {
if (!name || !Twinkle.disabledModules.includes(name)) {
func();
}
};
// Initialise modules that were saved in initCallbacks array
Twinkle.initCallbacks.forEach((module) => {
Twinkle.addInitCallback(module.func, module.name);
});
// Increases text size in Twinkle dialogs, if so configured
if (Twinkle.getPref('dialogLargeFont')) {
mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' +
'.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }');
}
// Hide the lingering space if the TW menu is empty
const isVector = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022';
if (isVector && Twinkle.getPref('portletType') === 'menu' && $('#p-twinkle').length === 0) {
$('#p-cactions').css('margin-right', 'initial');
}
// If using a skin with space for lots of modules, display a link to Twinkle Preferences
const usingSkinWithDropDownMenu = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022' || mw.config.get('skin') === 'timeless';
if (usingSkinWithDropDownMenu) {
Twinkle.addPortletLink(mw.util.getUrl('Wikipedia:Twinkle/Preferences'), 'Config', 'tw-config', 'Open Twinkle preferences page');
}
};
/**
* Twinkle-specific data shared by multiple modules
* Likely customized per installation
*/
// Custom change tag(s) to be applied to all Twinkle actions, create at Special:Tags
Twinkle.changeTags = 'twinkle';
// Available for actions that don't (yet) support tags
// currently: FlaggedRevs and PageTriage
Twinkle.summaryAd = ' ([[WP:TW|TW]])';
// Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to
// ensure MOS:ORDER
Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)';
/* Twinkle-specific utility functions shared by multiple modules */
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method gets a prefill, either from URL parameters (e.g. &vanarticle=Test) or from data previously stored using Twinkle.setPrefill()
*/
Twinkle.getPrefill = function (key) {
Twinkle.prefill = Twinkle.prefill || {};
if (!Object.prototype.hasOwnProperty.call(Twinkle.prefill, key)) {
Twinkle.prefill[key] = mw.util.getParamValue(key);
}
return Twinkle.prefill[key];
};
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method sets a prefill. This data will be lost if the page is refreshed, unless it is added to the URL as a parameter.
*/
Twinkle.setPrefill = function (key, value) {
Twinkle.prefill = Twinkle.prefill || {};
Twinkle.prefill[key] = value;
};
/*
* Used in XFD and PROD
*/
Twinkle.makeFindSourcesDiv = function makeSourcesDiv(divID) {
if (!$(divID).length) {
return;
}
if (!Twinkle.findSources) {
const parser = new Morebits.wiki.Preview($(divID)[0]);
parser.beginRender('({{Find sources|' + Morebits.pageNameNorm + '}})', 'WP:AFD').then(() => {
// Save for second-time around
Twinkle.findSources = parser.previewbox.innerHTML;
$(divID).removeClass('morebits-previewbox');
});
} else {
$(divID).html(Twinkle.findSources);
}
};
/**
* Used in batch, unlink, and deprod to sort pages by namespace, as
* json formatversion=2 sorts by pageid instead (#1251)
*/
Twinkle.sortByNamespace = function(first, second) {
return first.ns - second.ns || (first.title > second.title ? 1 : -1);
};
/**
* Used in batch listings to link to the page in question with >
*/
Twinkle.generateArrowLinks = function (checkbox) {
const link = Morebits.htmlNode('a', ' >');
link.setAttribute('class', 'tw-arrowpage-link');
link.setAttribute('href', mw.util.getUrl(checkbox.value));
link.setAttribute('target', '_blank');
checkbox.nextElementSibling.append(link);
};
/**
* Used in deprod and unlink listings to link the page title
*/
Twinkle.generateBatchPageLinks = function (checkbox) {
const $checkbox = $(checkbox);
const link = Morebits.htmlNode('a', $checkbox.val());
link.setAttribute('class', 'tw-batchpage-link');
link.setAttribute('href', mw.util.getUrl($checkbox.val()));
link.setAttribute('target', '_blank');
$checkbox.next().prepend([link, ' ']);
};
/**
* remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
*/
Twinkle.removeMoveToCommonsTagsFromWikicode = ( wikicode ) => wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi, '');
}());
// </nowiki>
1pkgkbiooui9ypy5qd4gl4b4c41j2c9
MediaWiki:Gadget-Twinkle.css
8
29075
326098
2026-04-04T08:57:18Z
Kannotlogin
29153
nieuw blad: /* Define CSS variables */ html { --twinkle-bgcolor-dialog: #f0f8ff; --twinkle-bgcolor-titlebar: #bccadf; } @media screen { html.skin-theme-clientpref-night { --twinkle-bgcolor-dialog: #141c26; --twinkle-bgcolor-titlebar: #1c2a52; } } /* stylelint-disable-next-line plugin/no-unsupported-browser-features */ @media screen and (prefers-color-scheme: dark) { html.skin-theme-clientpref-os { --twinkle-bgcolor-dialog: #141c26; --twinkle-bgcolor-titlebar: #1c2a52; } } /…
326098
css
text/css
/* Define CSS variables */
html {
--twinkle-bgcolor-dialog: #f0f8ff;
--twinkle-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/**
* Explicitly set width of TW menu so that we can use a hidden peer gadget
* to add space where the TW menu would go before it loads. See
* twinkle-pagestyles.css
*/
.skin-vector .vector-menu-dropdown #p-twinkle {
width: 3.24em;
}
/**
* In skin vector-2022, open the TW menu when mouse hovers over it. Credit to
* [[en:User:Nardog]] for this fix.
*/
#p-twinkle:hover > .vector-menu-content {
opacity: 1;
visibility: visible;
height: auto;
}
/* The additional box on user skin.js and twinklepreferences.js pages */
#twinkle-config-headerbox {
border: 1px #f60 solid;
background: #fed;
padding: 0.6em;
margin: 0.5em auto;
text-align: center;
color: inherit;
}
/* twinkleoptions.js */
#twinkle-config-headerbox.config-twopt-box {
font-weight: bold;
width: 80%;
border-width: 2px;
}
/* skin-specific js */
#twinkle-config-headerbox.config-userskin-box {
width: 60%;
}
/* WP:TWPREFS */
#twinkle-config-content input[type=checkbox] {
margin-right: 2px;
}
/* Twinkle config */
#twinkle-config {
border: 1px solid #666;
background: var(--twinkle-bgcolor-dialog);
color: inherit;
}
#twinkle-config label {
color: var(--color-base, #202122);
}
#twinkle-config-titlebar {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
}
#twinkle-config-buttonpane {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
padding: 0.5em;
}
.twinkle-config-helptip {
font-size: 90%;
color: var(--color-subtle, #54595d);
}
/* Flexbox gap not supported by old Safari versions. Rendering without gap is okay. */
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
.twinkle-ombox {
display: flex;
align-items: center;
gap: 0.9em;
padding-left: 0.9em;
margin: 4px 10%;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 1px solid #f28500;
}
@media screen {
html.skin-theme-clientpref-night #twinkle-config-headerbox {
background: #532b18;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #twinkle-config-headerbox {
background: #532b18;
}
}
l37en4kfjv61t71egkusbx1xosc8sax
MediaWiki:Gadget-section-Browsn
8
29076
326099
2026-04-04T09:00:14Z
Kannotlogin
29153
nieuw blad: Browsen en Leezn
326099
wikitext
text/x-wiki
Browsen en Leezn
5pmprnbs1ctxrssvc0z9ptoexgjkngh
MediaWiki:Gadget-section-Gebrukersinfo
8
29077
326100
2026-04-04T09:00:31Z
Kannotlogin
29153
nieuw blad: Gebrukersinformoatie
326100
wikitext
text/x-wiki
Gebrukersinformoatie
6b1k6t8i4e3z3qqpbotmirxy9b5h116
MediaWiki:Gadget-section-Bewerkn
8
29078
326102
2026-04-04T09:01:23Z
Kannotlogin
29153
nieuw blad: Bewerkn
326102
wikitext
text/x-wiki
Bewerkn
mds2t6ut4u48lta6fuuqzdjlirompm8
MediaWiki:Gadget-section-Interface
8
29079
326103
2026-04-04T09:01:33Z
Kannotlogin
29153
nieuw blad: Interface en Uutzicht
326103
wikitext
text/x-wiki
Interface en Uutzicht
cylpq7ux8jxyvxnym32sf2856d8g7vw
MediaWiki:Gadget-section-Ounderoud en Beheer
8
29080
326104
2026-04-04T09:01:43Z
Kannotlogin
29153
nieuw blad: Ounderoud en Beheer
326104
wikitext
text/x-wiki
Ounderoud en Beheer
86ff4c778s7zwgbc6gm0lxolsi474ik
MediaWiki:Gadget-Navigation popups
8
29081
326105
2026-04-04T09:02:05Z
Kannotlogin
29153
nieuw blad: '''Navigatie-popups''': bekyk e preview van een artikel en extra opties as je met je muus over e link zweeft.
326105
wikitext
text/x-wiki
'''Navigatie-popups''': bekyk e preview van een artikel en extra opties as je met je muus over e link zweeft.
2fp191mcn8bxd5e77bquabue5zlfvxe
MediaWiki:Gadget-ReferenceTooltips
8
29082
326106
2026-04-04T09:02:42Z
Kannotlogin
29153
nieuw blad: '''Bron-popups (Reference Tooltips)''': toont de bronvermeldienge in e klêen kadertje as je over e referentie (lik [1]) zweeft, zounder da je noa beneen moe scrolln.
326106
wikitext
text/x-wiki
'''Bron-popups (Reference Tooltips)''': toont de bronvermeldienge in e klêen kadertje as je over e referentie (lik [1]) zweeft, zounder da je noa beneen moe scrolln.
b0tikf6oolu3uiycchnpveqhzbcry9a
MediaWiki:Gadget-ImageAnnotator
8
29083
326107
2026-04-04T09:02:59Z
Kannotlogin
29153
nieuw blad: '''Ofbeeldienge-annotoaties''': lat je toe om specifieke nootn by ofbeeldiengn te bekykn.
326107
wikitext
text/x-wiki
'''Ofbeeldienge-annotoaties''': lat je toe om specifieke nootn by ofbeeldiengn te bekykn.
la3re9f9nht4zv5uvjx0eunjdg4h81u
MediaWiki:Gadget-exlinks
8
29084
326108
2026-04-04T09:03:13Z
Kannotlogin
29153
nieuw blad: '''Externe links''': makt d' externe links dudeliker binst 't leezn.
326108
wikitext
text/x-wiki
'''Externe links''': makt d' externe links dudeliker binst 't leezn.
63dj8q6gobn6uwij6r0e4z67v18kgu1
MediaWiki:Gadget-markblocked
8
29085
326109
2026-04-04T09:03:24Z
Kannotlogin
29153
nieuw blad: '''Geblokkeerde gebrukers markeern''': trekt e striepe deure de noame van geblokkeerde gebrukers (byvôorbeeld in de recente wyzigiengn of ip overlegbloadn).
326109
wikitext
text/x-wiki
'''Geblokkeerde gebrukers markeern''': trekt e striepe deure de noame van geblokkeerde gebrukers (byvôorbeeld in de recente wyzigiengn of ip overlegbloadn).
3xbu1dvmialk9kflh3dc4ti3mbhyphs
MediaWiki:Gadget-Gebruikersinfo
8
29086
326110
2026-04-04T09:03:33Z
Kannotlogin
29153
nieuw blad: '''Gebrukersinfo''': toont extra informoatie (lik 't aantal bewerkiengn en rechtn) ounder de noame van e gebruker ip zyn gebrukersblad.
326110
wikitext
text/x-wiki
'''Gebrukersinfo''': toont extra informoatie (lik 't aantal bewerkiengn en rechtn) ounder de noame van e gebruker ip zyn gebrukersblad.
c8lskx2tbvhoyddt520adr1w11pxgjh
MediaWiki:Gadget-XTools-ArticleInfo
8
29087
326111
2026-04-04T09:03:48Z
Kannotlogin
29153
nieuw blad: '''XTools Artikel-info''': toont statistiekn over 't artikel (lik 't aantal bewerkiengn en den anmoaker) bovenoan de pagina.
326111
wikitext
text/x-wiki
'''XTools Artikel-info''': toont statistiekn over 't artikel (lik 't aantal bewerkiengn en den anmoaker) bovenoan de pagina.
kucagiaqitmt2o095shrruvo3av8lap
MediaWiki:Gadget-HotCat
8
29088
326112
2026-04-04T09:04:01Z
Kannotlogin
29153
nieuw blad: '''HotCat''': e rappe maniere vo categorieën toe te voegn, te verandern of weg t' oaln ounderan e pagina, zounder hêel 't artikel te moetn bewerkn.
326112
wikitext
text/x-wiki
'''HotCat''': e rappe maniere vo categorieën toe te voegn, te verandern of weg t' oaln ounderan e pagina, zounder hêel 't artikel te moetn bewerkn.
g7d9b93tc1feyc9ddbjsrqs7ryc5asv
MediaWiki:Gadget-charinsert
8
29089
326113
2026-04-04T09:04:11Z
Kannotlogin
29153
nieuw blad: '''Specioale têkens (CharInsert)''': gift e menubalke ounder 't bewerkiengsvenster vo rap specioale têkens, accentn en wiki-code in te voegn.
326113
wikitext
text/x-wiki
'''Specioale têkens (CharInsert)''': gift e menubalke ounder 't bewerkiengsvenster vo rap specioale têkens, accentn en wiki-code in te voegn.
pqoiu5w93pbu6mbdwakmulyub7dcayb
MediaWiki:Gadget-wikEdDiff
8
29090
326114
2026-04-04T09:04:26Z
Kannotlogin
29153
nieuw blad: '''wikEdDiff''': toont e dudeliker en strakker overzicht (diff) van wa dat er juste veranderd is in een artikel.
326114
wikitext
text/x-wiki
'''wikEdDiff''': toont e dudeliker en strakker overzicht (diff) van wa dat er juste veranderd is in een artikel.
ob8xifxg9afdngkjtq2jql9yx0dllgg
MediaWiki:Gadget-ProveIt
8
29091
326115
2026-04-04T09:04:36Z
Kannotlogin
29153
nieuw blad: '''ProveIt''': e visuele tool vo bronn'n en referenties gemakkik in te voegn en te beheern binst 't bewerkn.
326115
wikitext
text/x-wiki
'''ProveIt''': e visuele tool vo bronn'n en referenties gemakkik in te voegn en te beheern binst 't bewerkn.
tfkna9fpowgsmcwvm7r4ddtqmn3sbgn
MediaWiki:Gadget-UTCLiveClock
8
29092
326116
2026-04-04T09:04:46Z
Kannotlogin
29153
nieuw blad: '''Klokke en Purge''': toont de actuele UTC-tyd rechtsboovn. As je d'r ip klikt, wordt de cache van de pagina gereset (purge).
326116
wikitext
text/x-wiki
'''Klokke en Purge''': toont de actuele UTC-tyd rechtsboovn. As je d'r ip klikt, wordt de cache van de pagina gereset (purge).
lmnek8xe8b30z0v9z8bgqcwl3qxbln3
MediaWiki:Gadget-purgetab
8
29093
326117
2026-04-04T09:04:57Z
Kannotlogin
29153
nieuw blad: '''Purge-knoppe''': gift een extra tabblad bovenoan (neffest 'Geschiedenisse') om de cache van de pagina te leegn.
326117
wikitext
text/x-wiki
'''Purge-knoppe''': gift een extra tabblad bovenoan (neffest 'Geschiedenisse') om de cache van de pagina te leegn.
2elqkq4880rlob3328y6gp87r3n0q8x
MediaWiki:Gadget-edittop
8
29094
326118
2026-04-04T09:05:08Z
Kannotlogin
29153
nieuw blad: '''Inleidienge bewerkn''': gift een extra [bewerk]-knoppe juste neffest den titel van 't artikel, om ollêne d' inleidienge te bewerkn.
326118
wikitext
text/x-wiki
'''Inleidienge bewerkn''': gift een extra [bewerk]-knoppe juste neffest den titel van 't artikel, om ollêne d' inleidienge te bewerkn.
ak7ossycab40g4epjbcmm5afgtol43p
MediaWiki:Gadget-dropdown-menus
8
29095
326119
2026-04-04T09:05:29Z
Kannotlogin
29153
nieuw blad: '''MoreMenu''': voegt e "Mêer"-menu toe met andige links vo bêerders en actieve gebrukers (lik logboekn en statistiekn).
326119
wikitext
text/x-wiki
'''MoreMenu''': voegt e "Mêer"-menu toe met andige links vo bêerders en actieve gebrukers (lik logboekn en statistiekn).
qsixhg7jd5v9fhv9syvefih2ahka83j
MediaWiki:Gadget-Twinkle
8
29096
326120
2026-04-04T09:05:39Z
Kannotlogin
29153
nieuw blad: '''Twinkle''': een uutgebreide set van tools vo 't rap bestrydn van vandalisme, 't nomineern van bloadn vo verwyderienge en 't waarschuuwn van gebrukers.
326120
wikitext
text/x-wiki
'''Twinkle''': een uutgebreide set van tools vo 't rap bestrydn van vandalisme, 't nomineern van bloadn vo verwyderienge en 't waarschuuwn van gebrukers.
n6add8ggpwz20vz2uqb0fiz4xl5exhy
MediaWiki:Gadget-RTRC
8
29097
326121
2026-04-04T09:05:51Z
Kannotlogin
29153
nieuw blad: '''Real-Time Recent Changes (RTRC)''': een uutgebreide, live bygewerkte interface vo de recente wyzigiengn ip te volgn.
326121
wikitext
text/x-wiki
'''Real-Time Recent Changes (RTRC)''': een uutgebreide, live bygewerkte interface vo de recente wyzigiengn ip te volgn.
pekge6zll3mfsx088ixjgkte3ymn6ud
MediaWiki:Grouppage-autoconfirmed
8
29098
326123
2026-04-04T09:17:00Z
Kannotlogin
29153
nieuw blad: {{ns:project}}:Bevestigde gebrukers
326123
wikitext
text/x-wiki
{{ns:project}}:Bevestigde gebrukers
5cq4eq0vk9utyl4imax5lazasrjx16h
MediaWiki:Group-autoconfirmed
8
29099
326124
2026-04-04T09:18:04Z
Kannotlogin
29153
nieuw blad: Automatisch bevestigde gebrukers
326124
wikitext
text/x-wiki
Automatisch bevestigde gebrukers
lep5438fp596u761r3h3llytq20t3tl
MediaWiki:Group-user
8
29100
326125
2026-04-04T09:18:15Z
Kannotlogin
29153
nieuw blad: Gebrukers
326125
wikitext
text/x-wiki
Gebrukers
1cj88b2fdii0e8waccw69mf2qfb38tg
MediaWiki:Grouppage-user
8
29101
326126
2026-04-04T09:20:20Z
Kannotlogin
29153
nieuw blad: {{ns:project}}:Gebrukers
326126
wikitext
text/x-wiki
{{ns:project}}:Gebrukers
r98rtmxn86wfbmwcvsgaowg27ysdn4s
Wikipedia:Gebrukers
4
29102
326127
2026-04-04T09:21:27Z
Kannotlogin
29153
nieuw blad: [[Ofbeeldienge:Crystal Clear action 2leftarrow.png|thumb|right|120px|Iedereen kan e gebruker wordn!]] '''Gebrukers''' (of ''Wikipedioann'') zyn aal de menschn die een account èn angmakt ip de West-Vloamsche Wikipedia. Zodra da je een account anmakt en inlogt, zyt je officieel e gebruker. Da gift je direct e poar andige vôordêeln teegnover anonieme lezers: * J' eit je eign [[Special:MyPage|gebrukersblad]] woarop da je etwot over jounzelve ku schryvn. * J' eit een eign Sp…
326127
wikitext
text/x-wiki
[[Ofbeeldienge:Crystal Clear action 2leftarrow.png|thumb|right|120px|Iedereen kan e gebruker wordn!]]
'''Gebrukers''' (of ''Wikipedioann'') zyn aal de menschn die een account èn angmakt ip de West-Vloamsche Wikipedia.
Zodra da je een account anmakt en inlogt, zyt je officieel e gebruker. Da gift je direct e poar andige vôordêeln teegnover anonieme lezers:
* J' eit je eign [[Special:MyPage|gebrukersblad]] woarop da je etwot over jounzelve ku schryvn.
* J' eit een eign [[Special:MyTalk|overlegblad]] woarop da andere menschn berichten vo joun kunn'n achterloatn.
* Je ku bloadn ip je [[Special:Watchlist|volglyste]] zettn om ze in d' oge t' oudn.
* Je ku meedôen an stemmiengn (as je an de vôorwoardn voldôet).
* Je bewerkiengn wordn gekoppeld an je noame in plekke van an e lomp IP-adres.
Achter 4 doagn en 10 bewerkiengn wordt iddere gebruker ip de achterground vanzèfs gepromoveerd toet [[{{ns:project}}:Bevestigde gebrukers|automatisch bevestigde gebruker]], woardeur da je nog mêer meugt (lik fotto's uploadn).
== Zie ôok ==
* [[Special:ListUsers|Lyste van aal de geregistreerde gebrukers]]
* [[{{ns:project}}:Welkom|Welkom ip de West-Vloamsche Wikipedia]]
[[Categorie:Wikipedia]]
4e0litb9rmla4i2elpo49vey3o1xz31
MediaWiki:Group-temporary-account-viewer
8
29104
326134
2026-04-04T09:29:45Z
Kannotlogin
29153
nieuw blad: Inzoage IP-adressn tydelikke accounts
326134
wikitext
text/x-wiki
Inzoage IP-adressn tydelikke accounts
9x1sg5ipft4vf52lgv7585vbac3pvxs
MediaWiki:Group-temporary-account-viewer-member
8
29105
326135
2026-04-04T09:30:21Z
Kannotlogin
29153
nieuw blad: {{GENDER:$1|Inzoage IP-adressn tydelikke accounts}}
326135
wikitext
text/x-wiki
{{GENDER:$1|Inzoage IP-adressn tydelikke accounts}}
hljmcoqxvxrfds9brj1xlytomiai1x8
MediaWiki:Grouppage-temporary-account-viewer
8
29106
326136
2026-04-04T09:30:42Z
Kannotlogin
29153
nieuw blad: {{ns:project}}:Inzoage IP-adressn tydelikke accounts
326136
wikitext
text/x-wiki
{{ns:project}}:Inzoage IP-adressn tydelikke accounts
rhw65teonlyk6h6qoqnr0rj1ucg13kn
MediaWiki:Group-eventcoordinator-member
8
29107
326137
2026-04-04T09:32:20Z
Kannotlogin
29153
nieuw blad: {{GENDER:$1|Organisator van evenementn}}
326137
wikitext
text/x-wiki
{{GENDER:$1|Organisator van evenementn}}
ogezu2h0pmfc5jk3ihmn2dzjlvr17cl
MediaWiki:Group-event-organizer
8
29110
326142
2026-04-04T09:38:51Z
Kannotlogin
29153
nieuw blad: Organisatoern van evenementn
326142
wikitext
text/x-wiki
Organisatoern van evenementn
l3kiwwgh740epl485464vla65vaxpya
MediaWiki:Group-event-organizer-member
8
29111
326143
2026-04-04T09:39:43Z
Kannotlogin
29153
nieuw blad: evenementnorganisat{{GENDER:$1|or|rice}}
326143
wikitext
text/x-wiki
evenementnorganisat{{GENDER:$1|or|rice}}
rxxadda5t7wi9tr1l9gqhyyikrs6rjy
MediaWiki:Grouppage-event-organizer
8
29112
326144
2026-04-04T09:40:31Z
Kannotlogin
29153
nieuw blad: {{ns:project}}:Evenementcoördinatoorn
326144
wikitext
text/x-wiki
{{ns:project}}:Evenementcoördinatoorn
bxk1hp5s9b0e0bei586tqq6limiwzya
MediaWiki:Statistics-header-pages
8
29113
326145
2026-04-04T09:42:48Z
Kannotlogin
29153
nieuw blad: Paginastatistiekn
326145
wikitext
text/x-wiki
Paginastatistiekn
5k8mik8cjqknmceqqi4ue73fqdpxh7j
MediaWiki:Statistics-articles
8
29114
326146
2026-04-04T09:43:01Z
Kannotlogin
29153
nieuw blad: Inoudelikke bloadn
326146
wikitext
text/x-wiki
Inoudelikke bloadn
a1vvagqig8dphr7njeu6qjgf4kw73ce
MediaWiki:Statistics-pages
8
29115
326147
2026-04-04T09:43:17Z
Kannotlogin
29153
nieuw blad: Bloadn
326147
wikitext
text/x-wiki
Bloadn
meemmgtd3riixmtax15rxi5oqtpln9v
MediaWiki:Statistics-pages-desc
8
29116
326148
2026-04-04T09:43:43Z
Kannotlogin
29153
nieuw blad: Aal de bloadn in de wiki, inclusief overlegbloadn, deureverwyziengn, enzovôorts.
326148
wikitext
text/x-wiki
Aal de bloadn in de wiki, inclusief overlegbloadn, deureverwyziengn, enzovôorts.
8p4hvsv1z71hpomv7x73y9uy0mb8op1
MediaWiki:Statistics-files
8
29117
326149
2026-04-04T09:43:56Z
Kannotlogin
29153
nieuw blad: Bestandn
326149
wikitext
text/x-wiki
Bestandn
briwv0y1l31gnpzlzjgwuc3njm4dp8y
MediaWiki:Statistics-header-edits
8
29118
326150
2026-04-04T09:44:10Z
Kannotlogin
29153
nieuw blad: Bewerkiengsstatistiekn
326150
wikitext
text/x-wiki
Bewerkiengsstatistiekn
ove4k2120slrgdxse9w655ap02cbr9k
MediaWiki:Statistics-edits
8
29119
326151
2026-04-04T09:44:37Z
Kannotlogin
29153
nieuw blad: Bloadnbewerkiengn sins 't begun van Wikipedia
326151
wikitext
text/x-wiki
Bloadnbewerkiengn sins 't begun van Wikipedia
gx3ri1c4bu3395zd7piaaqdhm5bsrn1
MediaWiki:Statistics-edits-average
8
29120
326152
2026-04-04T09:45:06Z
Kannotlogin
29153
nieuw blad: Gemiddeld oantal bewerkiengn per bload
326152
wikitext
text/x-wiki
Gemiddeld oantal bewerkiengn per bload
dpxs1phui1btjjbczh7g9n5vgx3a5df
MediaWiki:Statistics-header-users
8
29121
326153
2026-04-04T09:45:23Z
Kannotlogin
29153
nieuw blad: Gebrukersstatistiekn
326153
wikitext
text/x-wiki
Gebrukersstatistiekn
l83dwaog1hjjj8ufh6tfj3ow9vf9xuv
MediaWiki:Statistics-users
8
29122
326154
2026-04-04T09:45:39Z
Kannotlogin
29153
nieuw blad: Geregistreerde gebrukers
326154
wikitext
text/x-wiki
Geregistreerde gebrukers
jpuf5kdjttd4jhkn3t6dhdzq4xjahiy
MediaWiki:Statistics-users-active
8
29123
326155
2026-04-04T09:45:48Z
Kannotlogin
29153
nieuw blad: Actieve gebrukers
326155
wikitext
text/x-wiki
Actieve gebrukers
a7zeqynb6rchdbedotrnihyo7hqy7dh
MediaWiki:Statistics-users-active-desc
8
29124
326156
2026-04-04T09:46:23Z
Kannotlogin
29153
nieuw blad: Gebrukers die in d' ofgeloopn {{PLURAL:$1|dag|$1 doagn}} een andelienge èn uutgevoerd
326156
wikitext
text/x-wiki
Gebrukers die in d' ofgeloopn {{PLURAL:$1|dag|$1 doagn}} een andelienge èn uutgevoerd
byr4nqe6rop343f7ys6y3jrd88m5f8w
MediaWiki:Statistics-header-hooks
8
29125
326157
2026-04-04T09:46:35Z
Kannotlogin
29153
nieuw blad: Overige statistiekn
326157
wikitext
text/x-wiki
Overige statistiekn
qi9x3tlplpfb0pbdgczl11aj0nunihm
MediaWiki:Cirrussearch-article-words
8
29126
326158
2026-04-04T09:47:28Z
Kannotlogin
29153
nieuw blad: Aantal woordn in aal d' inoudelikke bloadn
326158
wikitext
text/x-wiki
Aantal woordn in aal d' inoudelikke bloadn
izasycvual3na06wgceht090foct5ib
326159
326158
2026-04-04T09:47:40Z
Kannotlogin
29153
326159
wikitext
text/x-wiki
Oantal woordn in aal d' inoudelikke bloadn
dws893pi7rat8mv6fulvhg5b0ko7r9o
MediaWiki:Spelliengsregels
8
29127
326160
2026-04-04T09:52:20Z
Kannotlogin
29153
nieuw blad: Spelliengsreegels
326160
wikitext
text/x-wiki
Spelliengsreegels
qec6blrlbuk7ub7gts8f8qoetfhe0m0
MediaWiki:Mainpage-description
8
29130
326164
2026-04-04T09:55:57Z
Kannotlogin
29153
nieuw blad: Oofdblad
326164
wikitext
text/x-wiki
Oofdblad
3lfivlitmfa2n89t1mrwnga7aujldk0
MediaWiki:Gebrukersportoal
8
29131
326165
2026-04-04T09:56:35Z
Kannotlogin
29153
nieuw blad: Gebrukersportoal
326165
wikitext
text/x-wiki
Gebrukersportoal
okk97wgap8kigzii3d6bsn8usc07g0r
MediaWiki:Uutleg
8
29132
326166
2026-04-04T09:57:27Z
Kannotlogin
29153
nieuw blad: Uutleg
326166
wikitext
text/x-wiki
Uutleg
jbau4mjh81ij0rrawngz9jwmzc9vlwv
MediaWiki:Actions
8
29133
326167
2026-04-04T09:58:34Z
Kannotlogin
29153
nieuw blad: Andeliengn
326167
wikitext
text/x-wiki
Andeliengn
1jvim8anrzs0ulyr974ekcyi12c2pmx
MediaWiki:Recentchangeslinked-toolbox
8
29134
326168
2026-04-04T09:59:14Z
Kannotlogin
29153
nieuw blad: Gerelateerde wyzigiengn
326168
wikitext
text/x-wiki
Gerelateerde wyzigiengn
o6mevqo3rfxonahqxb0dedyl1u1b5sx
MediaWiki:Pageinfo-toolboxlink
8
29135
326169
2026-04-04T10:00:20Z
Kannotlogin
29153
nieuw blad: Bloadgegevens
326169
wikitext
text/x-wiki
Bloadgegevens
g56g6919ipwvsmslny12eohga4rfzdz
MediaWiki:Urlshortener-toolbox
8
29137
326171
2026-04-04T10:01:45Z
Kannotlogin
29153
nieuw blad: Korte URL krygn
326171
wikitext
text/x-wiki
Korte URL krygn
8ynrgptf8xkx3bd74uacozhawenwpxr
MediaWiki:Vector-page-tools-general-label
8
29138
326172
2026-04-04T10:02:22Z
Kannotlogin
29153
nieuw blad: Algemêen
326172
wikitext
text/x-wiki
Algemêen
o9u68gjq9l58kubdahksokjuahmha5g
MediaWiki:Vector-page-tools-actions-label
8
29139
326173
2026-04-04T10:02:41Z
Kannotlogin
29153
nieuw blad: Andeliengn
326173
wikitext
text/x-wiki
Andeliengn
1jvim8anrzs0ulyr974ekcyi12c2pmx
MediaWiki:Vector-page-tools-label
8
29140
326174
2026-04-04T10:02:55Z
Kannotlogin
29153
nieuw blad: Ulpmiddels
326174
wikitext
text/x-wiki
Ulpmiddels
6kmavn01ss7odpi46f8mi2nqqvcdf7e
MediaWiki:Vector-unpin-element-label
8
29141
326175
2026-04-04T10:04:00Z
Kannotlogin
29153
nieuw blad: Versteekn
326175
wikitext
text/x-wiki
Versteekn
k14qwduio9tswk9n3nggmrgmglskhiu
MediaWiki:Vector-main-menu-label
8
29142
326176
2026-04-04T10:22:14Z
Kannotlogin
29153
nieuw blad: Oofdmenu
326176
wikitext
text/x-wiki
Oofdmenu
9d7r0f936d0cugop0ee3hdpvtyf58sx
MediaWiki:Vector-night-mode-issue-reporting-link-label
8
29143
326177
2026-04-04T10:22:27Z
Kannotlogin
29153
nieuw blad: Probleem me dounkere modus meldn
326177
wikitext
text/x-wiki
Probleem me dounkere modus meldn
jugmtp3zid7g0591vp6eozo369gp5ay
MediaWiki:Skin-theme-night-label
8
29144
326178
2026-04-04T10:22:33Z
Kannotlogin
29153
nieuw blad: Dounker
326178
wikitext
text/x-wiki
Dounker
ru2dlhkb7thms3wiydeuxf7oqk54cgw
MediaWiki:Skin-theme-name
8
29145
326179
2026-04-04T10:23:02Z
Kannotlogin
29153
nieuw blad: Kleure
326179
wikitext
text/x-wiki
Kleure
ptsbxsjnf04dszaivpxunrxue4aps4r
MediaWiki:Vector-feature-limited-width-0-label
8
29146
326180
2026-04-04T10:23:08Z
Kannotlogin
29153
nieuw blad: Vulle brêedte
326180
wikitext
text/x-wiki
Vulle brêedte
tcmp1tftn5gcahxc14jrq446w9qw8hi
MediaWiki:Vector-feature-limited-width-1-label
8
29147
326181
2026-04-04T10:23:13Z
Kannotlogin
29153
nieuw blad: Standoard
326181
wikitext
text/x-wiki
Standoard
rdisok3gqnter9hjdj25sckyux4s4b3
MediaWiki:Vector-feature-limited-width-name
8
29148
326182
2026-04-04T10:23:27Z
Kannotlogin
29153
nieuw blad: Bloadbrêedte
326182
wikitext
text/x-wiki
Bloadbrêedte
sqz9v12c0yo5i68dj47vbrxlewxbtuy
MediaWiki:Vector-feature-custom-font-size-2-label
8
29149
326183
2026-04-04T10:23:33Z
Kannotlogin
29153
nieuw blad: Grôot
326183
wikitext
text/x-wiki
Grôot
4hsude6ufvwgngy0rddo98da3kwbl7k
MediaWiki:Vector-feature-custom-font-size-1-label
8
29150
326184
2026-04-04T10:23:39Z
Kannotlogin
29153
nieuw blad: Standoard
326184
wikitext
text/x-wiki
Standoard
rdisok3gqnter9hjdj25sckyux4s4b3
MediaWiki:Vector-feature-custom-font-size-0-label
8
29151
326185
2026-04-04T10:23:45Z
Kannotlogin
29153
nieuw blad: Klêen
326185
wikitext
text/x-wiki
Klêen
iffn9vgyvawifgnv3ylqzip8objupix
MediaWiki:Wp25eastereggs-enable-learn-more-link-label
8
29152
326186
2026-04-04T10:23:53Z
Kannotlogin
29153
nieuw blad: Mêer leezn over de verjoardagsmodus
326186
wikitext
text/x-wiki
Mêer leezn over de verjoardagsmodus
pitp5im681yddown3fx6w3jlvnu0acm
MediaWiki:Wp25eastereggs-enable-1-label
8
29153
326187
2026-04-04T10:23:58Z
Kannotlogin
29153
nieuw blad: Ingeschoakeld
326187
wikitext
text/x-wiki
Ingeschoakeld
emvk1lpqdrlx2e26omw65zz5jxlokt3
MediaWiki:Wp25eastereggs-enable-0-label
8
29154
326188
2026-04-04T10:24:06Z
Kannotlogin
29153
nieuw blad: Uutgeschoakeld
326188
wikitext
text/x-wiki
Uutgeschoakeld
3ink3cm3nlmwa8438ma6l8rszc4on8z
MediaWiki:Wp25eastereggs-enable-name
8
29155
326189
2026-04-04T10:24:17Z
Kannotlogin
29153
nieuw blad: Verjoardagsmodus (Baby Globe) 🎉
326189
wikitext
text/x-wiki
Verjoardagsmodus (Baby Globe) 🎉
o5oupdarulrycc3plqa2yt2j2esvxsk
MediaWiki:Vector-appearance-label
8
29156
326190
2026-04-04T10:24:45Z
Kannotlogin
29153
nieuw blad: Uterlyk
326190
wikitext
text/x-wiki
Uterlyk
rcdk5bkiimfwzm97zfkl0kdu5fhr771
MediaWiki:Wikibase-otherprojects-sources
8
29157
326191
2026-04-04T10:25:02Z
Kannotlogin
29153
nieuw blad: Mêertoalige Wikisource
326191
wikitext
text/x-wiki
Mêertoalige Wikisource
anddxy8wx6viuogu2trpr8toyuobbgi
MediaWiki:Wikibase-otherprojects-outreach
8
29158
326192
2026-04-04T10:25:10Z
Kannotlogin
29153
nieuw blad: Wikimedia-vôorlichtienge
326192
wikitext
text/x-wiki
Wikimedia-vôorlichtienge
mh5pghaq0swyi1jovhs6zwzfix1c1t0
MediaWiki:Wikibase-otherprojects
8
29159
326193
2026-04-04T10:25:22Z
Kannotlogin
29153
nieuw blad: In andere projectn
326193
wikitext
text/x-wiki
In andere projectn
r07pe71rfbcgw3e4hcr2qvwaito744r
MediaWiki:Coll-create a book
8
29161
326195
2026-04-04T10:25:34Z
Kannotlogin
29153
nieuw blad: Boek anmoakn
326195
wikitext
text/x-wiki
Boek anmoakn
bkt6pindu72r1kjuqpsiew74synrdj2
MediaWiki:Coll-print export
8
29162
326196
2026-04-04T10:25:43Z
Kannotlogin
29153
nieuw blad: Ofdrukn/exporteern
326196
wikitext
text/x-wiki
Ofdrukn/exporteern
9mrcltn4d1minsxrufp8lyopszhrdbd
MediaWiki:Wikibase-editlinkstitle
8
29163
326197
2026-04-04T10:25:49Z
Kannotlogin
29153
nieuw blad: Toalkoppeliengn bewerkn
326197
wikitext
text/x-wiki
Toalkoppeliengn bewerkn
e1ukch0cqlmgumcdv5dksukf63j89n3
MediaWiki:Parsermigration-report-bug-toolbox-label
8
29164
326198
2026-04-04T10:25:55Z
Kannotlogin
29153
nieuw blad: Visuele weergavefoute meldn
326198
wikitext
text/x-wiki
Visuele weergavefoute meldn
sznrl4tp84youzuzzgouyip3g5pgwjs
MediaWiki:Parsermigration-use-legacy-parser-toolbox-label
8
29165
326199
2026-04-04T10:26:01Z
Kannotlogin
29153
nieuw blad: Noa d'oude parser overschoakeln
326199
wikitext
text/x-wiki
Noa d'oude parser overschoakeln
ccgbti6wcm8u2k62fbq5ip3taqzxe4q
MediaWiki:Checkuser-ip-auto-reveal-link-sidebar
8
29166
326200
2026-04-04T10:26:08Z
Kannotlogin
29153
nieuw blad: IP-adres automatisch onthulln
326200
wikitext
text/x-wiki
IP-adres automatisch onthulln
700styal6mn0msavfp2xdyyntxaq386
MediaWiki:Coll-download as
8
29167
326201
2026-04-04T10:29:40Z
Kannotlogin
29153
nieuw blad: Downloadn as $1
326201
wikitext
text/x-wiki
Downloadn as $1
qxa9olf49ph248gbvmkzda3bu8097zl
MediaWiki:Cx-campaign-contributionsmenu-mytranslations
8
29168
326202
2026-04-04T10:58:07Z
Kannotlogin
29153
nieuw blad: Vertaliengn
326202
wikitext
text/x-wiki
Vertaliengn
0128emmnld1t3ok80bw0l71e2igsll6
MediaWiki:Pt-userlogout
8
29169
326203
2026-04-04T10:58:23Z
Kannotlogin
29153
nieuw blad: Ofmeldn
326203
wikitext
text/x-wiki
Ofmeldn
509w71w7ebsfbj90xj14k0yjf468xlo
MediaWiki:Prefs-memberingroups
8
29170
326204
2026-04-04T10:58:40Z
Kannotlogin
29153
nieuw blad: {{GENDER:$2|Lid}} van {{PLURAL:$1|groep|groepn}}:
326204
wikitext
text/x-wiki
{{GENDER:$2|Lid}} van {{PLURAL:$1|groep|groepn}}:
o4wsm3so775xnfm9tx5lf8jr8xk8r7p
MediaWiki:Mwoauth-prefs-managegrantslink
8
29171
326205
2026-04-04T10:58:56Z
Kannotlogin
29153
nieuw blad: $1 gekoppelde {{PLURAL:$1|toepassienge|toepassiengn|0=toepassiengn}} beheern
326205
wikitext
text/x-wiki
$1 gekoppelde {{PLURAL:$1|toepassienge|toepassiengn|0=toepassiengn}} beheern
exnrcroygqp7k0cvvrcs6o9r3qcfo3k
MediaWiki:Prefs-edits
8
29172
326206
2026-04-04T10:59:02Z
Kannotlogin
29153
nieuw blad: Aantal bewerkiengn:
326206
wikitext
text/x-wiki
Aantal bewerkiengn:
0xpscx8y3al2ql8m35udgq0juoeb4er
MediaWiki:Prefs-registration
8
29173
326207
2026-04-04T10:59:08Z
Kannotlogin
29153
nieuw blad: Registroatiedoatum:
326207
wikitext
text/x-wiki
Registroatiedoatum:
lhlcqx0k2wsbpcm00dtqv3vutmd63ip
MediaWiki:Prefs-resetpass
8
29174
326208
2026-04-04T10:59:29Z
Kannotlogin
29153
nieuw blad: Wachtwoord verandern
326208
wikitext
text/x-wiki
Wachtwoord verandern
9jevhf3h52r2d23hsiazwlm92yrsoa6
MediaWiki:Prefs-help-yourpassword
8
29175
326209
2026-04-04T10:59:37Z
Kannotlogin
29153
nieuw blad: Accountherstel is ingeschoakeld. Zie $1 vo mêer instelliengn.
326209
wikitext
text/x-wiki
Accountherstel is ingeschoakeld. Zie $1 vo mêer instelliengn.
ci5libdamtrocfbm39yiymrncvnxhlw
MediaWiki:Prefs-user-downloaddata-label
8
29176
326210
2026-04-04T10:59:41Z
Kannotlogin
29153
nieuw blad: Accountgegevens bekykn:
326210
wikitext
text/x-wiki
Accountgegevens bekykn:
sdl8gbss5ybmfmm33skpnfd9oy46vd8
MediaWiki:Prefs-user-downloaddata-info
8
29177
326211
2026-04-04T10:59:52Z
Kannotlogin
29153
nieuw blad: Myn accountgegevens van dit project
326211
wikitext
text/x-wiki
Myn accountgegevens van dit project
9g58bfjnrxhb7gbeoiivwykt400yxpb
MediaWiki:Prefs-user-downloaddata-help-message
8
29178
326212
2026-04-04T11:00:18Z
Kannotlogin
29153
nieuw blad: Je ku noa je vôorkeurn ip [[m:Special:CentralAuth/$1|andere Wikimedia-projectn woaran da j' èt bygedroagn]] goan om je accountgegevens van die projectn te downloadn.
326212
wikitext
text/x-wiki
Je ku noa je vôorkeurn ip [[m:Special:CentralAuth/$1|andere Wikimedia-projectn woaran da j' èt bygedroagn]] goan om je accountgegevens van die projectn te downloadn.
2e1ldvz9b3gujvipalvje8f392otuzk
MediaWiki:Wikimedia-prefs-user-downloaddata-help-message
8
29179
326213
2026-04-04T11:00:29Z
Kannotlogin
29153
nieuw blad: Je ku noa je vôorkeurn ip [[m:Special:CentralAuth/$1|andere Wikimedia-projectn woaran da j' èt bygedroagn]] goan om je accountgegevens van die projectn te downloadn.
326213
wikitext
text/x-wiki
Je ku noa je vôorkeurn ip [[m:Special:CentralAuth/$1|andere Wikimedia-projectn woaran da j' èt bygedroagn]] goan om je accountgegevens van die projectn te downloadn.
2e1ldvz9b3gujvipalvje8f392otuzk
MediaWiki:Prefs-user-restoreprefs-label
8
29180
326214
2026-04-04T11:00:35Z
Kannotlogin
29153
nieuw blad: Instelliengn resetn:
326214
wikitext
text/x-wiki
Instelliengn resetn:
kd92evwv8sskyazt7gxmajrlh32wwcc
MediaWiki:Prefs-user-restoreprefs-info
8
29181
326215
2026-04-04T11:00:41Z
Kannotlogin
29153
nieuw blad: Aal de vôorkeurn erstelln (vo aal d' instelliengn)
326215
wikitext
text/x-wiki
Aal de vôorkeurn erstelln (vo aal d' instelliengn)
hj7xljncgt5f855cyw5ei8dbccttcj4
MediaWiki:Centralauth-prefs-status
8
29182
326216
2026-04-04T11:00:46Z
Kannotlogin
29153
nieuw blad: Globoal account:
326216
wikitext
text/x-wiki
Globoal account:
0fb8p2w2dpsk3joiqeliszxy38spfko
MediaWiki:Centralauth-prefs-view
8
29183
326217
2026-04-04T11:00:51Z
Kannotlogin
29153
nieuw blad: Gegevens over je globoal account bekykn
326217
wikitext
text/x-wiki
Gegevens over je globoal account bekykn
5p0l6fbnyfcq7mkdma8xguschq341rb
MediaWiki:Oathauth-module-recoverycodes-label
8
29184
326218
2026-04-04T11:00:56Z
Kannotlogin
29153
nieuw blad: Erstelcodes
326218
wikitext
text/x-wiki
Erstelcodes
jo1owtvnh8egkr2izrvepjr9xk19es0
MediaWiki:Oathauth-module-totp-label
8
29185
326219
2026-04-04T11:01:08Z
Kannotlogin
29153
nieuw blad: Authenticoatie-app
326219
wikitext
text/x-wiki
Authenticoatie-app
q394y1o0vql4flt6tz3uauclhhmcok9
MediaWiki:Oathauth-prefs-label
8
29186
326220
2026-04-04T11:01:17Z
Kannotlogin
29153
nieuw blad: Twêetrapsauthenticoatie:
326220
wikitext
text/x-wiki
Twêetrapsauthenticoatie:
qsqfy4u4j6dwgb2johnohfhkiy0no64
MediaWiki:Oathauth-ui-manage
8
29187
326221
2026-04-04T11:01:22Z
Kannotlogin
29153
nieuw blad: Beheern
326221
wikitext
text/x-wiki
Beheern
i3r6oyof86ykmjjo9eavzg9666dlxpo
MediaWiki:Globalprefs-info-label
8
29188
326222
2026-04-04T11:01:27Z
Kannotlogin
29153
nieuw blad: Globoale vôorkeurn:
326222
wikitext
text/x-wiki
Globoale vôorkeurn:
rajbdpqfin8g7qdeemsaccnsd6dht29
MediaWiki:Globalprefs-info-help
8
29189
326223
2026-04-04T11:01:38Z
Kannotlogin
29153
nieuw blad: Vôorkeurn ingesteld via 't blad met globoale vôorkeurn zyn van toepassienge ip aal de wiki's.
326223
wikitext
text/x-wiki
Vôorkeurn ingesteld via 't blad met globoale vôorkeurn zyn van toepassienge ip aal de wiki's.
ioof0umqmpqopeae8j07kpf56t3of2g
MediaWiki:Globalprefs-info-link
8
29190
326224
2026-04-04T11:01:43Z
Kannotlogin
29153
nieuw blad: Globoale vôorkeurn instelln
326224
wikitext
text/x-wiki
Globoale vôorkeurn instelln
gauqs4o24tgcu4h29soo9sfd1umtc1t
MediaWiki:Prefs-i18n
8
29191
326225
2026-04-04T11:01:47Z
Kannotlogin
29153
nieuw blad: Toalinstelliengn
326225
wikitext
text/x-wiki
Toalinstelliengn
f12h9x6imkf1nv0n226okkeqqjp4k0w
MediaWiki:Yourgender
8
29192
326226
2026-04-04T11:01:52Z
Kannotlogin
29153
nieuw blad: Oe wil je beschreevn wordn?
326226
wikitext
text/x-wiki
Oe wil je beschreevn wordn?
13dvwpem5v2gk8b3npe3erqyed8v4uf
MediaWiki:Gender-notknown
8
29193
326227
2026-04-04T11:01:58Z
Kannotlogin
29153
nieuw blad: De gebruker bewerkt wiki-bloadn
326227
wikitext
text/x-wiki
De gebruker bewerkt wiki-bloadn
nkmjdh4ss5vgb8onyr38xhs2co15qbl
MediaWiki:Gender-unknown
8
29194
326228
2026-04-04T11:02:09Z
Kannotlogin
29153
nieuw blad: De software gebruukt woa da 't meugelik is geslachtsneutroale woordn as 't over joun goat
326228
wikitext
text/x-wiki
De software gebruukt woa da 't meugelik is geslachtsneutroale woordn as 't over joun goat
lzc14p57mvkgrr1hzyir6tdirvts18f
MediaWiki:Gender-female
8
29195
326229
2026-04-04T11:02:14Z
Kannotlogin
29153
nieuw blad: Zy bewerkt bloadn
326229
wikitext
text/x-wiki
Zy bewerkt bloadn
82oswqy7f9w678kihu4fpi6fb9f2zuv
MediaWiki:Gender-male
8
29196
326230
2026-04-04T11:02:19Z
Kannotlogin
29153
nieuw blad: Ie bewerkt bloadn
326230
wikitext
text/x-wiki
Ie bewerkt bloadn
sc8hu96z6ymyyy8yzmlhp2zv6uaybzn
MediaWiki:Prefs-help-gender
8
29197
326231
2026-04-04T11:02:54Z
Kannotlogin
29153
nieuw blad: Die vôorkeure instelln is optioneel. De software gebruukt die woarde vo joun met 't juuste grammoaticoale geslacht an te spreekn en te vermeldn an andere. Die informoatie is oopnboar.
326231
wikitext
text/x-wiki
Die vôorkeure instelln is optioneel. De software gebruukt die woarde vo joun met 't juuste grammoaticoale geslacht an te spreekn en te vermeldn an andere. Die informoatie is oopnboar.
4q8zxr856c5cc0lad7jikbdh18rahlg
MediaWiki:Ext-uls-language-settings-preferences-link
8
29198
326232
2026-04-04T11:02:58Z
Kannotlogin
29153
nieuw blad: Mêer toalinstelliengn
326232
wikitext
text/x-wiki
Mêer toalinstelliengn
h1qbbl4ocotb7uf3udgodryf2m7z41p
MediaWiki:Prefs-signature
8
29199
326233
2026-04-04T11:03:02Z
Kannotlogin
29153
nieuw blad: Andtêkenienge
326233
wikitext
text/x-wiki
Andtêkenienge
49n9f6erotr3oq9aq6pipt6sr5le03p
MediaWiki:Tog-oldsig
8
29200
326234
2026-04-04T11:03:06Z
Kannotlogin
29153
nieuw blad: Joun udige andtêkenienge:
326234
wikitext
text/x-wiki
Joun udige andtêkenienge:
f9a6r4fycszq59etpmf0gophj5aknn8
MediaWiki:Prefs-help-signature
8
29201
326235
2026-04-04T11:03:23Z
Kannotlogin
29153
nieuw blad: Reacties ip overlegbloadn moetn met "<nowiki>~~~~</nowiki>" oundertekend wordn, wa da omgezet wordt in joun andtêkenienge en e tydstempel.
326235
wikitext
text/x-wiki
Reacties ip overlegbloadn moetn met "<nowiki>~~~~</nowiki>" oundertekend wordn, wa da omgezet wordt in joun andtêkenienge en e tydstempel.
emajd826p0cu9cw5y9n9esy5zcneuc4
MediaWiki:Prefs-changeemail
8
29202
326236
2026-04-04T11:03:29Z
Kannotlogin
29153
nieuw blad: E-mailadres verandern of wegdoen
326236
wikitext
text/x-wiki
E-mailadres verandern of wegdoen
9mkhkgu7ytkfvdjbko18iv5wkuw89p9
MediaWiki:Prefs-help-email-others
8
29203
326237
2026-04-04T11:03:55Z
Kannotlogin
29153
nieuw blad: Je lat ôok andere toe om per e-mail met joun contact ip te pakkn via e link ip je gebrukers- en overlegbloadn. Joun e-mailadres wordt nie prysegegeevn as andere gebrukers contact met joun ippakkn.
326237
wikitext
text/x-wiki
Je lat ôok andere toe om per e-mail met joun contact ip te pakkn via e link ip je gebrukers- en overlegbloadn.
Joun e-mailadres wordt nie prysegegeevn as andere gebrukers contact met joun ippakkn.
9wh7zfoqieau7vdnzgppo6rnbofyn90
MediaWiki:Tog-requireemail
8
29204
326238
2026-04-04T11:04:09Z
Kannotlogin
29153
nieuw blad: Allêne wachtwoordreset-e-mails verstuurn o da zowêl 't e-mailadres as de gebrukersnoame wordn ipgegeevn
326238
wikitext
text/x-wiki
Allêne wachtwoordreset-e-mails verstuurn o da zowêl 't e-mailadres as de gebrukersnoame wordn ipgegeevn
ovazdz4ljkggd99zkhq0zimxfeaj2x4
MediaWiki:Prefs-help-requireemail
8
29205
326239
2026-04-04T11:04:16Z
Kannotlogin
29153
nieuw blad: Da verbetert joun privacy en elpt oungewenste e-mails te vôorkommn.
326239
wikitext
text/x-wiki
Da verbetert joun privacy en elpt oungewenste e-mails te vôorkommn.
3cq3ihz9ylzre812r41uq24qcqs3vyi
MediaWiki:Prefs-emailconfirm-label
8
29206
326240
2026-04-04T11:04:20Z
Kannotlogin
29153
nieuw blad: E-mailbevestigienge:
326240
wikitext
text/x-wiki
E-mailbevestigienge:
34qpaa21j7pgt6dzfnixbc216y5rdce
MediaWiki:Email-allow-new-users-label
8
29207
326241
2026-04-04T11:04:25Z
Kannotlogin
29153
nieuw blad: E-mails van gloednieuwe gebrukers toestoan
326241
wikitext
text/x-wiki
E-mails van gloednieuwe gebrukers toestoan
3c7hcszavusx6g5wbvxn5mruvkm3tgt
MediaWiki:Prefs-help-email-allow-new-users
8
29208
326242
2026-04-04T11:04:42Z
Kannotlogin
29153
nieuw blad: As je die optie inschoakelt, kunn'n ôok gebrukers die nie [[{{MediaWiki:grouppage-autoconfirmed}}|automatisch bevestigd]] zyn joun e-mails steurn.
326242
wikitext
text/x-wiki
As je die optie inschoakelt, kunn'n ôok gebrukers die nie [[{{MediaWiki:grouppage-autoconfirmed}}|automatisch bevestigd]] zyn joun e-mails steurn.
dybuu33yxqrvccsfzj4ldajp59tx9j9
MediaWiki:Email-mutelist-label
8
29209
326243
2026-04-04T11:04:47Z
Kannotlogin
29153
nieuw blad: Vôorkom da die gebrukers e-mails noa myn kunn'n steurn:
326243
wikitext
text/x-wiki
Vôorkom da die gebrukers e-mails noa myn kunn'n steurn:
mq3akembo6unoa1wz3vvzj3e8izx56z
MediaWiki:Mw-widgets-usersmultiselect-placeholder
8
29210
326244
2026-04-04T11:04:51Z
Kannotlogin
29153
nieuw blad: Mêer toevoegn...
326244
wikitext
text/x-wiki
Mêer toevoegn...
6qxi2edlcybsfp7y975bbb76te32wbf
MediaWiki:Tog-enotifwatchlistpages
8
29211
326245
2026-04-04T11:05:02Z
Kannotlogin
29153
nieuw blad: Myn e-mailn by bewerkiengn van bloadn of bestandn ip myn volglyste
326245
wikitext
text/x-wiki
Myn e-mailn by bewerkiengn van bloadn of bestandn ip myn volglyste
77nw6d05lqd3ajpqaqnmn0inoensl3a
MediaWiki:Tog-enotifminoredits
8
29212
326246
2026-04-04T11:05:12Z
Kannotlogin
29153
nieuw blad: Myn e-mailn by klêne bewerkiengn van bloadn en bestandn ip myn volglyste
326246
wikitext
text/x-wiki
Myn e-mailn by klêne bewerkiengn van bloadn en bestandn ip myn volglyste
gojb6ei833dt5attgvh3nefo021ms66
MediaWiki:Prefs-quicksurveyext
8
29213
326247
2026-04-04T11:05:17Z
Kannotlogin
29153
nieuw blad: QuickSurvey-uutbreidienge
326247
wikitext
text/x-wiki
QuickSurvey-uutbreidienge
sq3hxmils5kreyvqzehja7q9hkbmshb
MediaWiki:Ext-quicksurveys-pref-displayquicksurveys-label
8
29214
326248
2026-04-04T11:05:20Z
Kannotlogin
29153
nieuw blad: QuickSurveys toonn
326248
wikitext
text/x-wiki
QuickSurveys toonn
8n2w5cl58wjzj7rdr359nr2hfoonuws
MediaWiki:Ext-quicksurveys-pref-displayquicksurveys-option-enabled
8
29215
326249
2026-04-04T11:05:25Z
Kannotlogin
29153
nieuw blad: Aal d' enquêtes toonn
326249
wikitext
text/x-wiki
Aal d' enquêtes toonn
jbun445ccvmnh2esi4t5jqws0yw5k4r
MediaWiki:Ext-quicksurveys-pref-displayquicksurveys-help
8
29216
326250
2026-04-04T11:05:39Z
Kannotlogin
29153
nieuw blad: Me die functie wordn [https://www.mediawiki.org/wiki/Extension:QuickSurveys korte, vrywillige enquêtes] van de Wikimedia Foundation getoogd. Die zyn nie an joun account gekoppeld.
326250
wikitext
text/x-wiki
Me die functie wordn [https://www.mediawiki.org/wiki/Extension:QuickSurveys korte, vrywillige enquêtes] van de Wikimedia Foundation getoogd. Die zyn nie an joun account gekoppeld.
3kq08mdo31iz4vgch0mh19bltmn2fnp
MediaWiki:Prefs-userpage
8
29217
326251
2026-04-04T11:05:43Z
Kannotlogin
29153
nieuw blad: Gebrukersblad
326251
wikitext
text/x-wiki
Gebrukersblad
6ww1dgpymqo7fn7hz8ti3a945l5z7i4
MediaWiki:Realme-preference-desc
8
29218
326252
2026-04-04T11:05:50Z
Kannotlogin
29153
nieuw blad: URL’s noar externe profieln:
326252
wikitext
text/x-wiki
URL’s noar externe profieln:
awlutww18anvnfcbnc49cv867pipuzo
MediaWiki:Realme-preference-help
8
29219
326253
2026-04-04T11:06:28Z
Kannotlogin
29153
nieuw blad: Voeg êen URL per regel toe. O da die URL's ip je gebrukersblad wordn gebruukt, wordn d' overeenkommende <code><link rel="me"></code> [https://en.wikipedia.org/wiki/HTML_element HTML-tags] an 't blad toegevoegd. Sommige platforms (byvôorbeeld Mastodon) gebruukn dadde om profiellinks as geverifieerd te markeern. Vo mêer informoatie kykt je best noa 't [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:RealMe ulpblad] ip MediaWiki.org.
326253
wikitext
text/x-wiki
Voeg êen URL per regel toe. O da die URL's ip je gebrukersblad wordn gebruukt, wordn d' overeenkommende <code><link rel="me"></code> [https://en.wikipedia.org/wiki/HTML_element HTML-tags] an 't blad toegevoegd. Sommige platforms (byvôorbeeld Mastodon) gebruukn dadde om profiellinks as geverifieerd te markeern. Vo mêer informoatie kykt je best noa 't [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:RealMe ulpblad] ip MediaWiki.org.
najh38ilu5lgq8zwixegjd20ky4zfdi
MediaWiki:Prefs-checkuser-tempaccount
8
29220
326254
2026-04-04T11:06:33Z
Kannotlogin
29153
nieuw blad: IP-adressn tydelikke accounts onthulln
326254
wikitext
text/x-wiki
IP-adressn tydelikke accounts onthulln
efuq8o2olc0sxs18h4mmaxe05pl8i6i
MediaWiki:Checkuser-tempaccount-enable-preference-description
8
29221
326255
2026-04-04T11:08:14Z
Kannotlogin
29153
nieuw blad: Vôoda je die instellienge inschoakelt, <b>moe je 't “[https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Access_to_temporary_account_IP_addresses Beleid toegoang toet IP-adressn tydelikke accounts]” leezn en d'rmee akkôord goan</b>. In 't byzounder: Je mag gin informoatie over IP-adressn van tydelikke accounts inzien, gebruukn of oopnboar moakn, uutgezounderd as da redelikerwyze nôodzakelik is vo 't <b>ounderzoek noa of d' handhoavienge teegn vandalisme, mis…
326255
wikitext
text/x-wiki
Vôoda je die instellienge inschoakelt, <b>moe je 't “[https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Access_to_temporary_account_IP_addresses Beleid toegoang toet IP-adressn tydelikke accounts]” leezn en d'rmee akkôord goan</b>. In 't byzounder:
Je mag gin informoatie over IP-adressn van tydelikke accounts inzien, gebruukn of oopnboar moakn, uutgezounderd as da redelikerwyze nôodzakelik is vo 't <b>ounderzoek noa of d' handhoavienge teegn vandalisme, misbruuk, spam, intimidoatie, stôornd gedrag en andere schendiengn van 't beleid van de Wikimedia Foundation of de gemêenschap.</b> As je de gegevens mè andere dêelt, moe je vôorzichtig zyn met woar en oe da je da doet en de gegevens wegdoen as d'r gin redelikke nôodzoake mêer is da andere ze zien.
Andere gebrukers me toegoang toet de IP-adressn van tydelikke accounts goan kunn'n zien da j' die vôorkeure èt ingeschoakeld.
pnmsh9b00k0znb3qa8fl59afoe2spw0
MediaWiki:Wikimedia-checkuser-tempaccount-enable-preference-description
8
29222
326256
2026-04-04T11:08:34Z
Kannotlogin
29153
nieuw blad: Vôoda je die instellienge inschoakelt, <b>moe je 't “[https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Access_to_temporary_account_IP_addresses Beleid toegoang toet IP-adressn tydelikke accounts]” leezn en d'rmee akkôord goan</b>. In 't byzounder: Je mag gin informoatie over IP-adressn van tydelikke accounts inzien, gebruukn of oopnboar moakn, uutgezounderd as da redelikerwyze nôodzakelik is vo 't <b>ounderzoek noa of d' handhoavienge teegn vandalisme, mis…
326256
wikitext
text/x-wiki
Vôoda je die instellienge inschoakelt, <b>moe je 't “[https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Access_to_temporary_account_IP_addresses Beleid toegoang toet IP-adressn tydelikke accounts]” leezn en d'rmee akkôord goan</b>. In 't byzounder:
Je mag gin informoatie over IP-adressn van tydelikke accounts inzien, gebruukn of oopnboar moakn, uutgezounderd as da redelikerwyze nôodzakelik is vo 't <b>ounderzoek noa of d' handhoavienge teegn vandalisme, misbruuk, spam, intimidoatie, stôornd gedrag en andere schendiengn van 't beleid van de Wikimedia Foundation of de gemêenschap.</b> As je de gegevens mè andere dêelt, moe je vôorzichtig zyn met woar en oe da je da doet en de gegevens wegdoen as d'r gin redelikke nôodzoake mêer is da andere ze zien.
Andere gebrukers me toegoang toet de IP-adressn van tydelikke accounts goan kunn'n zien da j' die vôorkeure èt ingeschoakeld.
pnmsh9b00k0znb3qa8fl59afoe2spw0
MediaWiki:Checkuser-tempaccount-enable-preference
8
29223
326257
2026-04-04T11:08:40Z
Kannotlogin
29153
nieuw blad: Activeer de meugelikheid vo 't onthulln van IP-adressn van tydelikke accounts
326257
wikitext
text/x-wiki
Activeer de meugelikheid vo 't onthulln van IP-adressn van tydelikke accounts
83aqxeudiei9x2wzaxsp0rjfqncjtxv
MediaWiki:Globalprefs-set-local-exception
8
29224
326258
2026-04-04T11:08:46Z
Kannotlogin
29153
nieuw blad: Stel e lokoale uutzounderienge in vo die [[Special:GlobalPreferences#$1|globoale vôorkeure]].
326258
wikitext
text/x-wiki
Stel e lokoale uutzounderienge in vo die [[Special:GlobalPreferences#$1|globoale vôorkeure]].
halr8sfm3pdqhqzi7qmn8lq5aaazl1w
MediaWiki:Prefs-ipinfo
8
29225
326259
2026-04-04T11:08:50Z
Kannotlogin
29153
nieuw blad: IP-informoatie
326259
wikitext
text/x-wiki
IP-informoatie
31wqwae2shzu6f6umegv4cupondlr92
MediaWiki:Ipinfo-preference-use-agreement
8
29226
326260
2026-04-04T11:09:01Z
Kannotlogin
29153
nieuw blad: Ik goan akkôord met 't gebruuk van da ulpmiddel in overêenstemmienge met de [https://foundation.wikimedia.org/wiki/Special:MyLanguage/Legal:IP_Information_tool_guidelines/nl richtlynn] en goa 't ollêne gebruukn vo 't ounderzoekn of vôorkommn van vandalisme, misbruuk of andere schendiengn van richtlynn van de Wikimedia Foundation of de gemêenschap
326260
wikitext
text/x-wiki
Ik goan akkôord met 't gebruuk van da ulpmiddel in overêenstemmienge met de [https://foundation.wikimedia.org/wiki/Special:MyLanguage/Legal:IP_Information_tool_guidelines/nl richtlynn] en goa 't ollêne gebruukn vo 't ounderzoekn of vôorkommn van vandalisme, misbruuk of andere schendiengn van richtlynn van de Wikimedia Foundation of de gemêenschap
hauu1hstknhihfrporxtzhlkr83spz6
MediaWiki:Wikimedia-ipinfo-preference-use-agreement
8
29227
326261
2026-04-04T11:09:08Z
Kannotlogin
29153
nieuw blad: Ik goan akkôord met 't gebruuk van da ulpmiddel in overêenstemmienge met de [https://foundation.wikimedia.org/wiki/Special:MyLanguage/Legal:IP_Information_tool_guidelines/nl richtlynn] en goa 't ollêne gebruukn vo 't ounderzoekn of vôorkommn van vandalisme, misbruuk of andere schendiengn van richtlynn van de Wikimedia Foundation of de gemêenschap
326261
wikitext
text/x-wiki
Ik goan akkôord met 't gebruuk van da ulpmiddel in overêenstemmienge met de [https://foundation.wikimedia.org/wiki/Special:MyLanguage/Legal:IP_Information_tool_guidelines/nl richtlynn] en goa 't ollêne gebruukn vo 't ounderzoekn of vôorkommn van vandalisme, misbruuk of andere schendiengn van richtlynn van de Wikimedia Foundation of de gemêenschap
hauu1hstknhihfrporxtzhlkr83spz6
MediaWiki:Prefs-homepage
8
29228
326262
2026-04-04T11:09:19Z
Kannotlogin
29153
nieuw blad: Bewerkiengsfuncties vo nieuwkommers
326262
wikitext
text/x-wiki
Bewerkiengsfuncties vo nieuwkommers
5pxvzqmd0f6gy87colsrnnx3l2kvjyz
MediaWiki:Growthexperiments-homepage-enable
8
29229
326263
2026-04-04T11:09:23Z
Kannotlogin
29153
nieuw blad: Toog nieuwkommersstartblad
326263
wikitext
text/x-wiki
Toog nieuwkommersstartblad
azv5k5uxbq2wmeb186ooaoyz8u3k91p
MediaWiki:Growthexperiments-help-panel-tog-help-panel
8
29230
326264
2026-04-04T11:09:30Z
Kannotlogin
29153
nieuw blad: Schoakel 't ulppanêel vo bewerkers in
326264
wikitext
text/x-wiki
Schoakel 't ulppanêel vo bewerkers in
s87s50nhepgq0t466uzm9twsgvcnmpj
MediaWiki:Prefs-campaignevents-invitations
8
29231
326265
2026-04-04T11:09:36Z
Kannotlogin
29153
nieuw blad: Uutnôodigiengslystn
326265
wikitext
text/x-wiki
Uutnôodigiengslystn
0x05ron2wvj6ggr60lfek2a88q9fpfe
MediaWiki:Campaignevents-invitationlist-preference
8
29232
326266
2026-04-04T11:09:50Z
Kannotlogin
29153
nieuw blad: Voeg myn toe an d' [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CampaignEvents/Invitation_lists uutnôodigiengslystn]
326266
wikitext
text/x-wiki
Voeg myn toe an d' [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CampaignEvents/Invitation_lists uutnôodigiengslystn]
pxep5f0fkvw3gsxsmqvjcgcpf0b82tp