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&section=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]]&nbsp; [[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&nbsp;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&nbsp;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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); } // 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, '&lt;') .replace(/>/g, '&gt;') .replace(/:/g, '&#58;') .replace(/\[/g, '&#91;') .replace(/]/g, '&#93;'); } function htmlescape_attr(s) { return htmlescape_text(s).replace(/'/g, '&#39;').replace(/"/g, '&quot;'); } // 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) + '&nbsp;' + (num != 1 ? popupString('wikiLinks') : popupString('wikiLink')); } function popupFilterCountImages(data) { const num = countImages(data); return String(num) + '&nbsp;' + (num != 1 ? popupString('images') : popupString('image')); } function popupFilterCountCategories(data) { const num = countCategories(data); return ( String(num) + '&nbsp;' + (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, '&nbsp;'); } 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 + '&nbsp;' + 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='&shy;'; return this.split('&') .join('&amp;') .split('<') .join('&lt;') .split('>') .join('&gt;' /*+shy*/) .split('"') .join('&quot;'); }; // 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, '&amp;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); }; 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('&nbsp;'), 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('&nbsp;'), 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('&nbsp;'), 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 + '">&rarr;</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('&nbsp;'), 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 += '&nbsp;|&nbsp;'; 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 += '&nbsp;|&nbsp;'; 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&section=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&section=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&section=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', ' &sdot; '); 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&nbsp;revision': 'this&nbsp;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&nbsp;page': 'user&nbsp;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, '&#x7B;&#x7B;$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&section=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&amprefix=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&amp;page=" + encodeURIComponent(mw.config.get("wgFormattedNamespaces")[2] + ":" + user.name) + "&amp;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&amp;type=newusers&amp;dir=prev&amp;limit=1&amp;user=" + et + "'>" + UserinfoJsFormatDateRel(registration) + "</a> actief"; } else { statusText += ". Sinds <a href='" + mw.config.get("wgScriptPath") + "/index.php?title=Special:ListUsers&amp;limit=1&amp;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) + "&amp;project=nl.wikipedia.org&amp;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() === '.&nbsp;&nbsp;') { $result.html('&nbsp;.&nbsp;'); } else if ($result.html() === '&nbsp;.&nbsp;') { $result.html('&nbsp;&nbsp;.'); } else { $result.html('.&nbsp;&nbsp;'); } }, 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.[\[+]] &nbsp; <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&section=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, '&#34;') + '">' + 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 &lt;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 = &middot; ); } 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={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};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">&times;</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()+'">&times;</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>&lt;link rel="me"&gt;</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>&lt;link rel="me"&gt;</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