Jump to content
Wikipedia The Free Encyclopedia

User:Chlod/Scripts/Coordinator.js

From Wikipedia, the free encyclopedia
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Chlod/Scripts/Coordinator.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
 /*
  * Coordinator
  *
  * More information on the userscript itself can be found at [[User:Chlod/Scripts/Coordinator]].
  */
 // <nowiki>

 mw.loader.using([
 "oojs-ui-core",
 "oojs-ui-windows",
 "oojs-ui-widgets",
 "mediawiki.api",
 "mediawiki.util"
 ],asyncfunction(){

 // =============================== STYLES =================================

 mw.util.addCSS(`
  #coordinates.coord-missing > a {
  font-style: italic;
  }

  #coordinator {
  text-align: left;
  }

  #coordinator .map {
  width: 100%;
  height: 70vh;
  max-height: 300px;
  }

  #coordinator .coordinator-section {
  width: 100%;
  display: flex;
  margin-bottom: 8px;
  overflow: hidden;
  }

  #coordinator .coordinator-section-header b {
  text-transform: uppercase;
  text-align: center;
  flex: 4;
  margin: 0 8px;
  border-bottom: 1px solid gray;
  }

  #coordinator .coordinator-section-decimal .oo-ui-textInputWidget {
  flex: 4;
  }

  #coordinator .coordinator-section-dms .oo-ui-textInputWidget {
  flex: 1;
  }

  #coordinator .coordinator-section-options {
  align-items: end;
  }

  #coordinator .coordinator-section-options .oo-ui-fieldLayout {
  margin-top: 0;
  flex: 1;
  }

  #coordinator .coordinator-section-options .oo-ui-fieldLayout + .oo-ui-fieldLayout {
  margin-left: 12px;
  }

  #coordinator .coordinator-section-action {
  justify-content: end;
  }

  #coordinator .coordinator-section-options .coordinator-fieldGroup-display {
  flex: initial;
  }
  `);

 // ============================== CONSTANTS ===============================

 /**
  * Advert for edit summaries.
  * @type {string}
  */
 constadvert="([[User:Chlod/Scripts/Coordinator|coordinator]])";

 /**
  * URL to the Leaflet JS file. This should be using the Toolforge
  * CDNJS mirror for privacy assurance.
  * @type {string}
  */
 constleafletJS="https://tools-static.wmflabs.org/cdnjs/ajax/libs/leaflet/1.7.1/leaflet.js";
 /**
  * URL to the Leaflet CSS file. This should be using the Toolforge
  * CDNJS mirror for privacy assurance.
  * @type {string}
  */
 constleafletCSS="https://tools-static.wmflabs.org/cdnjs/ajax/libs/leaflet/1.7.1/leaflet.css";
 /**
  * URL to a JavaScript file that provides a ParsoidDocument.
  * @type {string}
  */
 constparsoidDocumentJS="https://en.wikipedia.org/wiki/User:Chlod/Scripts/ParsoidDocument.js?action=raw&ctype=text/javascript";

 /**
  * Tile server URL and attribution information.
  * @type {{url: string, attribution: string}}
  */
 consttileServer={
 url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
 attribution:"&copy; <a href=\"https://openstreetmap.org/copyright\">OpenStreetMap contributors</a>"
 };

 /**
  * {{coord missing}} or {{coord}} template.
  */
 letcoord=document.querySelector("#coordinates");

 /**
  * Internationalization strings.
  * @type {Object}
  */
 consti18n={
 add:"add coordinates",
 edit:"edit",
 add_pre:"(",
 add_post:")",
 switch_dms:"DMS",
 switch_dms_long:"Use degrees, minutes, and seconds",
 latitude:"latitude",
 longitude:"longitude",
 inline:"Inline",
 title:"Title",
 display:"Display",
 parameters:"Coordinate parameters",
 parameters_help:"Template:Coord#Coordinate parameters",
 name:"Name",
 no_input:"Please move the marker or enter coordinates in the provided fields.",
 template_not_found:`We couldn't find the {{{{{0}}}}} template anywhere in the page. Please edit the page manually.`,
 save_error:"Something wrong happened when saving the page: {{{0}}}",
 summary_add:`Adding page coordinates ${advert}`,
 summary_modify:`Adding page coordinates ${advert}`
 };

 /**
  * Default options for the editing popup.
  * @type {Object}
  */
 constdefaults={
 dms:false
 };

 /**
  * Relevant templates for operation. These MUST begin with `Template:`
  * no matter the language; MediaWiki will automatically handle namespace
  * localization.
  * @type {{coord: string, coord_missing: string}}
  */
 consttemplates={
 coord:"Template:Coord",
 coord_missing:"Template:Coord missing"
 };

 // =========================== HELPER FUNCTIONS ===========================

 /**
  * Converts decimal-form degrees (0.12345) to degree-minute-second form. This avoids
  * JavaScript numerical errors, which affect division and modulo operations.
  *
  * @param {number} decimal The decimal to convert
  * @returns {[number, number, number, number]} The coordinates in sign-degree-minute-second form.
  */
 functiondecToDMS(decimal){
 // -37.7891
 constsign=Math.sign(decimal);// -1.0
 constdegrees=Math.floor(Math.abs(decimal));// 37.0
 constminutesDecimal=Math.abs(decimal)-degrees;// 0.7891
 constminutesFull=minutesDecimal*60;// 47.346
 constminutes=Math.floor(minutesFull);// 47.0
 constsecondsDecimal=minutesFull-minutes;// 0.346
 constsecondsFull=secondsDecimal*60;// 20.76
 constseconds=Math.round(secondsFull);// 21.0

 returnisNaN(degrees+minutes+seconds)?NaN:[sign,degrees,minutes,seconds];// [37, 47, 21]
 }

 /**
  * Converts degree-minute-second form to decimal-form degrees (0.12345). This avoids
  * JavaScript numerical errors, which affect division and modulo operations.
  *
  * @param {[number, number, number]} dms A tuple containing the degree, minute,
  * and second to convert (respectively)
  * @returns {number} The coordinates in decimal form.
  */
 functiondmsToDec(dms){
 const[degree,minutes,seconds]=dms;
 returndegree+(minutes/60)+(seconds/3600);
 }

 /**
  * Extracts the number value from the start of a string.
  * @param {string} string A string containing a number (or decimal) at the start.
  * @param {boolean} allowDecimal `true` if decimals are allowed.
  */
 functionextractNumberValue(string,allowDecimal=true){
 returnstring.replace(newRegExp(allowDecimal?"[^\\d.]":"[^\\d]","g"),"");
 }

 // ============================== SINGLETONS ==============================

 /**
  * The WindowManager for this userscript.
  */
 constwindowManager=newOO.ui.WindowManager();
 document.body.appendChild(windowManager.$element[0]);

 /**
  * MediaWiki API class.
  * @type {mw.Api}
  */
 constapi=newmw.Api();

 /**
  * The PopupWidget that handles the editor.
  */
 letpopupWidget;

 /**
  * The Leaflet map used to graphically select a coordinate.
  */
 letmap;

 /**
  * The Leaflet map marker.
  */
 letmarker;

 /**
  * The current coordinate values
  */
 letcurrent={
 lat:null,
 lon:null,
 dms:false,
 inline:false,
 title:true,
 name:null,
 parameters:null,
 fromMissing:false,
 notes:null,
 qid:null
 };

 /**
  * The ParsoidDocument for this page.
  * @type ParsoidDocument
  */
 letparsoidDocument;

 /**
  * The redirects for the Coord and Coord missing templates.
  */
 lettemplateRedirects={};

 // =========================== PROCESS FUNCTIONS ==========================

 /**
  * Generates the "params" object for an element's `data-mw`.
  */
 functionconstructCoordParameters(){
 constpositionalParameters=[];
 if(current.dms){
 const[latSign,latDegree,latMinute,latSeconds]=decToDMS(current.lat);
 const[lonSign,lonDegree,lonMinute,lonSeconds]=decToDMS(current.lon);
 positionalParameters.push(
 latDegree,latMinute,latSeconds,latSign===-1?"S":"N",
 lonDegree,lonMinute,lonSeconds,lonSign===-1?"W":"E"
 );
 }else{
 positionalParameters.push(
 current.lat.toFixed(5),current.lon.toFixed(5)
 );
 }
 if(current.parameters!=null)
 positionalParameters.push(current.parameters);

 constparameters={};
 for(leti=0;i<positionalParameters.length;i++){
 parameters[`${i+1}`]={wt:`${positionalParameters[i]}`};
 }

 if(current.inline&&current.title){
 parameters["display"]={wt:"inline,title"};
 }elseif(!current.inline){
 parameters["display"]={wt:"title"}
 }
 if(current.name!=null)
 parameters["name"]={wt:current.name};
 if(current.notes!=null)
 parameters["notes"]={wt:current.notes};
 if(current.qid!=null)
 parameters["qid"]={wt:current.qid};

 returnparameters;
 }

 /**
  * Loads the aliases for the coord and coord_missing templates.
  * @returns {Promise<void>}
  */
 asyncfunctionloadTemplateAliases(){
 returnapi.get({
 action:"query",
 format:"json",
 prop:"linkshere",
 titles:Object.values(templates).join("|"),
 utf8:1,
 formatversion:"2",
 lhprop:"title",
 lhshow:"redirect",
 lhlimit:"500"
 }).then(({query})=>{
 query["pages"].forEach((page)=>{
 templateRedirects[page.title]=page["linkshere"].map(v=>v.title);
 });
 });
 }

 /**
  * Initialize the ParsoidDocument for this userscript.
  * @returns {Promise<void>}
  */
 asyncfunctionparsoidStartup(){
 if(parsoidDocument==null){
 parsoidDocument=newParsoidDocument();
 document.body.appendChild(parsoidDocument.buildFrame());
 }else{
 parsoidDocument.resetFrame();
 }
 awaitparsoidDocument.loadFrame(mw.config.get("wgPageName"));
 }

 /**
  * Saves the new coordinates to the most suitable template.
  * @returns {Promise<boolean>}
  */
 asyncfunctionsaveToCoordTemplate(){
 awaitparsoidStartup();

 if(current.lat==null||current.lon==null){
 OO.ui.alert(
 i18n.no_input
 );
 returnfalse;
 }

 if(current.fromMissing){
 consttarget=parsoidDocument.document.querySelector("#coordinates.coord-missing[data-mw]");
 if(target==null){
 OO.ui.alert(
 i18n.template_not_found
 .replace("{{{0}}}",templates.coord_missing.replace(/^Template:/,""))
 );
 returnfalse;
 }

 /** @type {{parts: any[]}} */
 constmwData=JSON.parse(target.getAttribute("data-mw"));

 constpart=mwData.parts.find(part=>{
 returnpart.template!=null
 &&[templates.coord_missing,...templateRedirects[templates.coord_missing]]
 .map(v=>"./"+v.replace(/\s/g,"_").toLowerCase())
 .includes(part.template.target.href.toLowerCase());
 });

 if(!part){
 OO.ui.alert(i18n.template_not_found.replace("{{{0}}}",templates.coord_missing.replace(/^Template:/,"")));
 return;
 }

 part.template.target.wt=templates.coord.replace(/^Template:/,"");
 part.template.params=constructCoordParameters();
 target.setAttribute("data-mw",JSON.stringify(mwData));
 }elseif(parsoidDocument.document.querySelector("#coordinates")!=null){
 consttarget=parsoidDocument.findParsoidNode(
 parsoidDocument.document.querySelector("#coordinates")
 );
 if(target==null){
 OO.ui.alert(
 i18n.template_not_found
 .replace("{{{0}}}",templates.coord.replace(/^Template:/,""))
 );
 returnfalse;
 }

 /** @type {{parts: any[]}} */
 constmwData=JSON.parse(target.getAttribute("data-mw"));

 constpart=mwData.parts.find(part=>{
 returnpart.template!=null
 &&[templates.coord,...templateRedirects[templates.coord]]
 .map(v=>"./"+v.replace(/\s/g,"_").toLowerCase())
 .includes(part.template.target.href.toLowerCase());
 });

 if(!part){
 OO.ui.alert(i18n.template_not_found.replace("{{{0}}}",templates.coord.replace(/^Template:/,"")));
 returnfalse;
 }

 part.template.params=constructCoordParameters();
 target.setAttribute("data-mw",JSON.stringify(mwData));
 }else{
 // Guess the best place for the coordinates.
 constbestSpot=(()=>{
 functionlast(array){returnarray[array.length-1];}

 constpd=parsoidDocument.document;
 /**
  * This order is based on [[WP:ORDER]]. It looks for the lowest place it can be
  * put.
  * @type {[InsertPosition, HTMLElement|null][]}
  */
 constpossibleSpots=[
 // Before {{DEFAULTSORT}}
 ["beforebegin",pd.querySelector("[property=\"mw:PageProp/categorydefaultsort\"]")],
 // Before categories
 ["beforebegin",pd.querySelector("[rel=\"mw:PageProp/Category\"]:not([about])")],
 // Before stub templates
 ["beforebegin",pd.querySelector(".stub")],
 // After {{authority control}}
 ["afterend",last(pd.querySelectorAll(".authority-control"))],
 // After {{taxon bar}}
 ["afterend",last(pd.querySelectorAll(".navbox[aria-labelledby=\"Taxon_identifiers\"]"))],
 // After {{portal bar}}
 ["afterend",last(pd.querySelectorAll(".portal-bar"))],
 // After the last navbox
 ["afterend",last(pd.querySelectorAll(".navbox"))],
 // After the succession box
 ["afterend",last(pd.querySelectorAll(".succession-box"))],
 // The very bottom of the page.
 ["beforeend",last(pd.querySelectorAll("section"))]
 ];

 for(constspotofpossibleSpots){
 if(spot[1]!=null)
 returnspot;
 }
 returnnull;
 })();

 consttemplate=document.createElement("span");
 template.setAttribute("about",`N${Math.floor(Math.random*1000)}`);
 template.setAttribute("typeof","mw:Transclusion");
 template.setAttribute("data-mw",JSON.stringify({
 parts:[{
 template:{
 target:{
 wt:templates.coord.replace(/^Template:/,""),
 href:"./"+templates.coord.replace(/\s/g,"_")
 },
 params:constructCoordParameters(),
 i:0
 }
 }]
 }));

 (parsoidDocument.findParsoidNode(bestSpot[1])||bestSpot[1])
 .insertAdjacentElement(bestSpot[0],template);
 }

 constwikitext=awaitparsoidDocument.toWikitext();

 returnapi.postWithEditToken({
 action:"edit",
 format:"json",
 title:mw.config.get("wgPageName"),
 utf8:1,
 formatversion:"2",
 text:wikitext,
 summary:current.fromMissing?i18n.summary_add:i18n.summary_modify
 }).then(()=>true);
 }

 /**
  * Spawns the editing popup. Should only be run once.
  * @returns {Promise<HTMLElement>}
  */
 asyncfunctionspawnEditingPopup(){
 constpopupContent=document.createElement("div");
 popupContent.style.marginRight="8px";

 constheaderSection=document.createElement("div");
 headerSection.classList.add("coordinator-section");
 headerSection.classList.add("coordinator-section-header");
 constheaderLatitude=document.createElement("b");
 headerLatitude.innerText=i18n.latitude;
 constheaderLongitude=document.createElement("b");
 headerLongitude.innerText=i18n.longitude;
 headerSection.appendChild(headerLatitude);
 headerSection.appendChild(headerLongitude);
 popupContent.appendChild(headerSection);

 constdecimalSection=document.createElement("div");
 decimalSection.classList.add("coordinator-section");
 decimalSection.classList.add("coordinator-section-decimal");
 constlatDecimal=newOO.ui.TextInputWidget({value:"0",validate:/^[0-9.\-]+$/g});
 constlonDecimal=newOO.ui.TextInputWidget({value:"0",validate:/^[0-9.\-]+$/g});

 /**
  * Update the decimal fields from a latitude and longitude.
  */
 functionupdateDecimalFields(lat,lon){
 latDecimal.setValue(lat.toFixed(4));
 lonDecimal.setValue(lon.toFixed(4));
 latDecimal.setValidityFlag();
 lonDecimal.setValidityFlag();
 }

 /**
  * Updates the map and DMS fields from the decimal values.
  */
 functionupdateFromDecimalFields(){
 constlat=+latDecimal.getValue();
 constlon=+lonDecimal.getValue();
 current.lat=lat;
 current.lon=lon;
 updateDMSFields(decToDMS(lat),decToDMS(lon));
 marker.setLatLng([lat,lon]);
 }

 [latDecimal,lonDecimal].forEach((textInput)=>{
 consttextInputElement=textInput.$element[0].querySelector("input");
 textInputElement.addEventListener("keydown",(event)=>{
 constallow=
 // Permit most control keys
 event.key.length>1
 ||/^[0-9.\-]$/.test(event.key);// Only allow -, ., and numbers.
 if(!allow)event.preventDefault();
 returnallow;
 });
 textInputElement.addEventListener("keypress",()=>{
 // Map marker modifier is placed here since this is not affected by external changes.
 setTimeout(()=>{
 // Placed in setTimeout to allow keypress event to finish propagating.
 updateFromDecimalFields();
 });
 });
 decimalSection.appendChild(textInput.$element[0]);
 });
 popupContent.appendChild(decimalSection);

 constdmsSection=document.createElement("div");
 dmsSection.classList.add("coordinator-section");
 dmsSection.classList.add("coordinator-section-dms");
 constlatDMSDegree=newOO.ui.TextInputWidget({value:"0"});
 constlatDMSMinute=newOO.ui.TextInputWidget({value:"0"});
 constlatDMSSecond=newOO.ui.TextInputWidget({value:"0"});
 constlatDMSDirection=newOO.ui.TextInputWidget({value:"N"});
 constlonDMSDegree=newOO.ui.TextInputWidget({value:"0"});
 constlonDMSMinute=newOO.ui.TextInputWidget({value:"0"});
 constlonDMSSecond=newOO.ui.TextInputWidget({value:"0"});
 constlonDMSDirection=newOO.ui.TextInputWidget({value:"E"});

 /**
  * Update the DMS fields from a DMS-format latitude and longitude.
  */
 functionupdateDMSFields(
 [latSign,latDegree,latMinute,latSecond],
 [lonSign,lonDegree,lonMinute,lonSecond]
 ){
 latDMSDegree.setValue(Math.abs(latDegree));
 latDMSMinute.setValue(latMinute);
 latDMSSecond.setValue(latSecond);
 latDMSDirection.setValue(latSign===-1?"S":"N");
 lonDMSDegree.setValue(Math.abs(lonDegree));
 lonDMSMinute.setValue(lonMinute);
 lonDMSSecond.setValue(lonSecond);
 lonDMSDirection.setValue(lonSign===-1?"W":"E");
 [
 latDMSDegree,latDMSMinute,latDMSSecond,latDMSDirection,
 lonDMSDegree,lonDMSMinute,lonDMSSecond,lonDMSDirection
 ].forEach((textInput)=>{
 textInput.setValidityFlag();
 });
 }
 /**
  * Updates the map and decimal fields from the DMS values.
  */
 functionupdateFromDMSFields(){
 constlat=dmsToDec([
 +extractNumberValue(latDMSDegree.getValue()),
 +extractNumberValue(latDMSMinute.getValue()),
 +extractNumberValue(latDMSSecond.getValue())
 ]);
 constlon=dmsToDec([
 +extractNumberValue(lonDMSDegree.getValue()),
 +extractNumberValue(lonDMSMinute.getValue()),
 +extractNumberValue(lonDMSSecond.getValue())
 ]);

 if(!isNaN(lat+lon)){
 current.lat=lat;
 current.lon=lon;
 updateDecimalFields(lat,lon);
 marker.setLatLng([lat,lon]);
 }
 }

 constdmsDirectionFields=[[latDMSDirection,"NS"],[lonDMSDirection,"WE"]];
 dmsDirectionFields.forEach(([textInput,allowedDirections])=>{
 consttextInputElement=textInput.$element[0].querySelector("input");
 textInputElement.addEventListener("keydown",(event)=>{
 constallow=
 // Permit most control keys
 event.key.length>1
 ||newRegExp(`^[${allowedDirections}]$`,"i").test(event.key);
 if(!allow)event.preventDefault();// Banned key
 elseif(event.code.startsWith("Key")){
 // Due to the earlier check, we can assume that this is a cardinal direction key.
 textInputElement.value=event.key.toUpperCase();
 // Prevent the letter from actually being input.
 event.preventDefault();
 }
 returnallow;
 });
 textInputElement.addEventListener("keypress",()=>{
 // Map marker modifier is placed here since this is not affected by external changes.
 setTimeout(()=>{
 // Placed in setTimeout to allow keypress event to finish propagating.
 updateFromDMSFields();
 });
 });
 });

 /** @type [any, string, boolean][] */
 constdmsLatFields=[
 [latDMSDegree,"\u00b0",false],
 [latDMSMinute,"\u2032",false],
 [latDMSSecond,"\u2033",true]
 ];
 constdmsLonFields=[
 [lonDMSDegree,"\u00b0",false],
 [lonDMSMinute,"\u2032",false],
 [lonDMSSecond,"\u2033",true],
 ];
 functionupgradeDMSFieldset(fieldset){
 fieldset.forEach(([textInput,symbol,allowsDecimal])=>{
 constallowsDecimalRegex=allowsDecimal?"[^\\d.]":"[^\\d]"

 consttextInputElement=textInput.$element[0].querySelector("input");
 textInputElement.addEventListener("keydown",(event)=>{
 constallow=
 // Prevent all letter keys first. This will leave symbols and numbers
 // as the only remaining possible `event.key` values.
 !/^Key/.test(event.code)
 &&!(newRegExp(`^${allowsDecimalRegex}$`)).test(event.key);
 if(!allow)event.preventDefault();

 returnallow;
 });
 textInputElement.addEventListener("keypress",()=>{
 // Map marker modifier is placed here since this is not affected by external changes.
 setTimeout(()=>{
 // Placed in setTimeout to allow keypress event to finish propagating.
 updateFromDMSFields();
 });
 });
 textInput.on("change",()=>{
 // Reformat the TextInputWidget to remove extraneous letters.
 constvalue=extractNumberValue(textInput.getValue(),allowsDecimal);
 textInput.setValue(value+symbol);
 });

 letvalue=textInput.getValue();
 if(!value.endsWith(symbol)){
 textInput.setValue(value+symbol);
 }

 dmsSection.appendChild(textInput.$element[0]);
 });
 }
 upgradeDMSFieldset(dmsLatFields);
 dmsSection.appendChild(dmsDirectionFields[0][0].$element[0]);
 upgradeDMSFieldset(dmsLonFields);
 dmsSection.appendChild(dmsDirectionFields[1][0].$element[0]);

 popupContent.appendChild(dmsSection);

 constcoordOptionsSection=document.createElement("div");
 coordOptionsSection.classList.add("coordinator-section");
 coordOptionsSection.classList.add("coordinator-section-options");
 constcoordFormatSwitch=newOO.ui.ToggleButtonWidget({
 label:i18n.switch_dms,
 title:i18n.switch_dms_long
 });
 constcoordDisplayInline=newOO.ui.ToggleButtonWidget({label:i18n.inline});
 constcoordDisplayTitle=newOO.ui.ToggleButtonWidget({label:i18n.title,value:true});
 constcoordDisplayGroup=newOO.ui.ButtonGroupWidget({
 items:[
 coordDisplayInline,
 coordDisplayTitle
 ],
 title:i18n.display
 });
 constcoordParameters=newOO.ui.TextInputWidget({
 placeholder:i18n.parameters_help
 });
 constcoordName=newOO.ui.TextInputWidget({
 placeholder:mw.config.get("wgPageName").replace(/_/," ")
 });

 coordFormatSwitch.on("change",(dms)=>toggleFormat(dms));
 coordDisplayInline.on("change",(state)=>{current.inline=state;});
 coordDisplayTitle.on("change",(state)=>{current.title=state;});
 coordParameters.on("change",(text)=>{current.parameters=text.length===0?null:text});
 coordName.on("change",(text)=>{current.name=text.length===0?null:text});

 coordOptionsSection.appendChild(coordFormatSwitch.$element[0]);
 coordOptionsSection.appendChild(newOO.ui.FieldLayout(coordDisplayGroup,{
 classes:["coordinator-fieldGroup-display"],
 label:i18n.display,
 align:"top"
 }).$element[0]);
 coordOptionsSection.appendChild(newOO.ui.FieldLayout(coordParameters,{
 label:i18n.parameters,
 align:"top"
 }).$element[0]);
 coordOptionsSection.appendChild(newOO.ui.FieldLayout(coordName,{
 label:i18n.name,
 align:"top"
 }).$element[0]);
 popupContent.appendChild(coordOptionsSection);

 constcoordActionSection=document.createElement("div");
 coordActionSection.classList.add("coordinator-section");
 coordActionSection.classList.add("coordinator-section-action");
 constcoordSave=newOO.ui.ButtonWidget({
 label:"Save",
 flags:["primary","progressive"]
 });

 coordSave.on("click",async()=>{
 coordSave.setDisabled(true);
 popupWidget.setDisabled(true);
 try{
 constsuccess=awaitsaveToCoordTemplate();
 if(success){
 popupWidget.toggle(false);
 window.location.reload();
 }
 }catch(e){
 OO.ui.alert(i18n.save_error.replace("{{{0}}}",e.message));
 console.error(e);
 }
 coordSave.setDisabled(false);
 popupWidget.setDisabled(false);
 });

 coordActionSection.appendChild(coordSave.$element[0]);
 popupContent.appendChild(coordActionSection);

 /**
  * Switch between active input fields depending on the format being used.
  * @param {boolean} dms Whether or not the decimal-minute-second format will be used.
  */
 functiontoggleFormat(dms){
 current.dms=dms;
 [...dmsLatFields,...dmsLonFields,...dmsDirectionFields].forEach(([dmsField])=>{
 dmsField.setDisabled(!dms);
 });
 latDecimal.setDisabled(dms);
 lonDecimal.setDisabled(dms);
 decimalSection.style.height=decimalSection.style.margin=dms?"0":"";
 dmsSection.style.height=dmsSection.style.margin=dms?"":"0";
 coordFormatSwitch.setValue(dms);
 }

 toggleFormat(defaults.dms);

 constmapElement=document.createElement("div");
 mapElement.classList.add("map");
 popupContent.appendChild(mapElement);

 popupWidget=newOO.ui.PopupWidget({
 $content:$(popupContent),
 padded:true,
 width:600,
 head:true,
 id:"coordinator",
 hideWhenOutOfView:false
 });

 map=L.map(mapElement).setView([0,0],2);
 popupWidget.on("ready",()=>{
 coord.classList.toggle("coordinator-loading",false);
 map.invalidateSize();
 });
 popupWidget.on("toggle",()=>map.invalidateSize());

 L.tileLayer(tileServer.url,{
 maxZoom:19,
 attribution:tileServer.attribution
 }).addTo(map);

 // Add scale bar.
 L.control.scale({imperial:true,metric:true}).addTo(map);

 // Create draggable marker.
 marker=L.marker([0,0],{
 draggable:true
 });
 marker.addTo(map);
 marker.addEventListener("drag",()=>{
 const{lat,lng:lon}=marker.getLatLng();

 current.lat=lat;
 current.lon=lon;
 updateDecimalFields(lat,lon);
 updateDMSFields(decToDMS(lat),decToDMS(lon));
 });

 /**
  * Updates all fields from a set latitude and longitude.
  * @param lat
  * @param lon
  */
 functionupdateAll(lat,lon){
 updateDecimalFields(lat,lon);
 updateDMSFields(decToDMS(lat),decToDMS(lon));
 marker.setLatLng([lat,lon]);
 }

 map.addEventListener("load",()=>map.invalidateSize());
 document.addEventListener("scroll",()=>map.invalidateSize());

 if(mw.config.get("wgCoordinates")){
 // Get template data from Parsoid.
 awaitparsoidStartup();
 constnode=parsoidDocument.findParsoidNode(parsoidDocument.document.querySelector("#coordinates"));
 constmwData=JSON.parse(node.getAttribute("data-mw"));
 constpart=mwData.parts.find(part=>{
 returnpart.template!=null
 &&[templates.coord,...templateRedirects[templates.coord]]
 .map(v=>"./"+v.replace(/\s/g,"_").toLowerCase())
 .includes(part.template.target.href.toLowerCase());
 });

 const{lat,lon}=mw.config.get("wgCoordinates");
 current.lat=lat;
 current.lon=lon;
 updateAll(lat,lon);

 if(part.template.params["format"]){
 current.dms=part.template.params["format"].wt==="dms";
 }else{
 current.dms=document.querySelector("#coordinates .geo-dms")!=null;
 }
 coordFormatSwitch.setValue(current.dms);

 if(part.template.params["display"]){
 current.inline=part.template.params["display"].wt.includes("inline")
 ||part.template.params["display"]==="t";
 current.title=part.template.params["display"].wt.includes("title")
 ||part.template.params["display"]==="it";
 }
 coordDisplayInline.setValue(current.inline);
 coordDisplayTitle.setValue(current.title);

 for(const[key,value]ofObject.entries(part.template.params)){
 // noinspection JSCheckFunctionSignatures
 if(!isNaN(+key)&&/type|scale|dim|region|globe|source/.test(value.wt)){
 current.parameters=value.wt;
 coordParameters.setValue(current.parameters);
 break;
 }
 }

 if(part.template.params["name"]){
 current.name=part.template.params["name"].wt;
 coordName.setValue(current.name);
 }
 // Leave these untouched
 if(part.template.params["notes"])
 current.notes=part.template.params["notes"].wt;
 if(part.template.params["qid"])
 current.qid=part.template.params["qid"].wt;
 }

 returnpopupWidget.$element[0];
 }

 asyncfunctionopenEditingPopup(){
 // Load Leaflet
 mw.loader.load(leafletCSS,"text/css");
 awaitPromise.all([
 mw.loader.getScript(parsoidDocumentJS),
 mw.loader.getScript(leafletJS),
 loadTemplateAliases()
 ]);

 coord.classList.toggle("coordinator-loading",true);
 if(window.ParsoidDocument==null)
 awaitnewPromise((res)=>{document.addEventListener("parsoidDocument:load",res);});

 coord.appendChild(awaitspawnEditingPopup());
 popupWidget.toggle(true);
 }

 // ============================== INITIALIZE ==============================
 constnewCoord=coord==null;
 if(newCoord){
 // Generate "coord-missing" element for detection.
 coord=document.createElement("span");
 coord.setAttribute("id","coordinates");
 coord.classList.add("coord-missing");

 document.querySelector(".mw-parser-output").appendChild(coord);
 current.fromMissing=false;
 }else{
 current.fromMissing=coord.classList.contains("coord-missing");
 }

 mw.hook("wikipage.content").add(()=>{
 if(![0,118].includes(mw.config.get("wgNamespaceNumber"))||mw.config.get("wgPageName")==="Main_Page")
 // Don't show except on mainspace and draftspace
 return;

 // Don't add if already appended
 if(document.querySelector(".mw-body-content #coordinates #coordinator"))
 return;

 constcoord_a=document.createElement("a");
 coord_a.setAttribute("id","coordinator");
 coord_a.setAttribute("href","javascript:void(0)");
 coord_a.addEventListener("click",()=>{
 openEditingPopup();
 });
 coord_a.innerText=current.fromMissing?i18n.add:(newCoord?i18n.add:i18n.edit);

 coord.append(" "+i18n.add_pre);
 coord.appendChild(coord_a);
 coord.append(i18n.add_post);
 });

 // noinspection JSUnusedGlobalSymbols
 window.Coordinator={
 openEditingPopup:openEditingPopup
 };
 });
 // </nowiki>
 /*
  * Copyright 2021 Chlod
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  *
  * Also licensed under the Creative Commons Attribution-ShareAlike 3.0
  * Unported License, a copy of which is available at
  *
  * https://creativecommons.org/licenses/by-sa/3.0
  *
  */

AltStyle によって変換されたページ (->オリジナル) /