Benutzer:Nw520/AddressTools.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
- Opera: Strg+F5
//<nowiki>
$.when( mw.loader.using( [ 'mediawiki.util' ] ), $.ready ).then( function () {
const addressTools = {
/**
* Whether to automatically lookup zipcodes if addresses are missing them.
*/
AUTOLOOKUP_ZIPCODES: false,
/**
* @param {string} address
* @param {Set<string>} countries
* @return {boolean} Whether the address includese a zip code for at least one of the listed countries, `null` if no country is supported.
*/
containsZipCode: function ( address, countries ) {
let found = false;
let supported = false;
if ( countries.has( 'at' ) ) {
found = found || address.match( /, \d{4} .+/ ) !== null;
supported = true;
}
if ( countries.has( 'de' ) ) {
found = found || address.match( /, \d{5} .+/ ) !== null;
supported = true;
}
if ( countries.has( 'lu' ) ) {
found = found || address.match( /, (L-)?\d{4} .+/ ) !== null;
supported = true;
}
if ( !supported ) {
return null;
} else {
return found;
}
},
/**
* Looks up the address via Nominatim.
*
* @param {string} address
* @param {string} city
* @return {Object}
*/
lookup: async function ( address, city ) {
const baseUrl = 'https://nominatim.openstreetmap.org/search';
const headers = new Headers();
headers.append( 'X-Referer', 'https://de.wikivoyage.org/wiki/User:Nw520/AddressTools.js' );
headers.append( 'X-User-Agent', 'AddressTools, https://de.wikivoyage.org/wiki/User_Discussion:Nw520/AddressTools.js' );
const params = new URLSearchParams( {
addressdetails: 1,
format: 'json',
q: `${address}, ${city}`
} );
const response = await fetch( new Request( `${baseUrl}?${params.toString()}`, {
cache: 'default',
headers: headers,
method: 'GET',
mode: 'cors'
} ) );
const data = await response.json();
if ( ( data ?? null ) === null || data.length === 0 ) {
return null;
}
return data;
},
/**
* Looks up the address via Nominatim and returns its zip code.
*
* @param {string} address
* @param {string} city
* @return {?string} Zip code and city of the address.
*/
zipCode: async function ( address, city ) {
const data = await addressTools.lookup( address, city );
const filteredData = data?.filter( ( i ) => {
return ( i?.address?.postcode ?? null ) !== null && ( i?.address?.city ?? i?.address?.town ?? i?.address?.village ?? null ) !== null;
} ) ?? null;
// No results
if ( filteredData === null || filteredData.length === 0 ) {
return null;
}
console.log(`${data[ 0 ].address.postcode} ${data[ 0 ].address.city ?? data[ 0 ].address.town ?? data[ 0 ].address.village}`)
return `${data[ 0 ].address.postcode} ${data[ 0 ].address.city ?? data[ 0 ].address.town ?? data[ 0 ].address.village}`;
}
};
const strings = {
'ext-addresstools-cityname-prompt': {
de: 'Bitte gib den Namen der Stadt an.',
en: 'Please enter the city\'s name.'
},
'ext-addresstools-clipboard-suggestion-fail': {
de: 'Postleitzahl konnte nicht in die Zwischenablage kopiert werden.',
en: 'Failed to copy zipcode to clipboard.'
},
'ext-addresstools-clipboard-suggestion-success': {
de: 'Postleitzahl in die Zwischenablage kopiert: $1',
en: 'Zipcode copied to clipboard: $1'
},
'ext-addresstools-indicator-tooltip-suggestion': {
de: 'In der Adresse fehlt die Postleitzahl, bei Klick wird ein Vorschlag in die Zwischenablage kopiert',
en: 'The address is missing a zipcode, on click a suggestion is copied to your clipboard'
},
'ext-addresstools-indicator-tooltip-nozip': {
de: 'In der Adresse fehlt die Postleitzahl',
en: 'The address is missing a zipcode'
},
'ext-addresstools-portlet-lookup': {
de: 'Postleitzahlen nachschlagen',
en: 'Lookup zipcodes'
},
};
const memory = {};
let city = null;
let portlet = null;
let toLookup = [];
async function main() {
setupStrings();
setupCss();
const countries = await getCountries();
if ( countries.size <= 0 ) {
return;
}
mw.hook( 'wikipage.content' ).add( async ( $content ) => {
// Reset vCards to lookup
toLookup = [];
// Find relevant vCards and add indicator
for ( const vcard of $content[ 0 ].querySelectorAll( '.vcard' ) ) {
const addressEl = vcard.querySelector( '.listing-address' );
if ( addressEl === null ) {
continue;
}
const address = addressEl.textContent;
if ( addressTools.containsZipCode( address, countries ) === false ) {
const indicator = document.createElement( 'span' );
indicator.classList.add( 'ext-addresstools-indicator' );
indicator.classList.add( 'ext-addresstools-indicator-nozip' );
indicator.classList.add( 'ext-addresstools-indicator-unchecked' );
indicator.textContent = '〒';
indicator.title = mw.msg( 'ext-addresstools-indicator-tooltip-nozip' );
indicator.addEventListener( 'click', ( e ) => {
e.preventDefault();
if ( indicator.classList.contains( 'ext-addresstools-indicator-unchecked' ) ) {
lookupVcard( address, indicator, vcard, true );
}
} );
vcard.insertAdjacentElement( 'beforeend', indicator );
toLookup.push( {
address,
indicator,
vcard
} );
}
}
// Lookup addresses that are missing zipcode
toLookup = await lookupVcards( toLookup, addressTools.AUTOLOOKUP_ZIPCODES );
updatePortlet();
} );
}
/**
* @param {string} title
* @param {Array<string>} terminalCategories
* @param {mw.Api} api
* @return {Set<string>}
*/
async function getCategories( title, terminalCategories, api ) {
const formattedTerminalCategories = terminalCategories.map( ( category ) => `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:${category}` );
let toRequest = [ title ];
let categories = new Set();
let fullfilled = new Set();
while ( toRequest.length > 0 ) {
for ( const item of toRequest ) {
fullfilled.add( item );
}
const res = await api.get( {
"action": "query",
"format": "json",
"prop": "categories",
"titles": toRequest.join( '|' ),
"formatversion": "2",
"clshow": "!hidden",
"cllimit": "max"
} );
toRequest = [];
// Collect categories
const pageCategories = res.query?.pages.flatMap( ( pageRes ) => {
return pageRes.categories?.map( ( categoryRes ) => {
return categoryRes.title;
} );
} ).filter( ( category ) => ( category ?? null ) !== null );
// Recurse down
for ( const pageCategory of pageCategories ) {
categories.add( pageCategory );
if ( !formattedTerminalCategories.includes( pageCategory ) ) {
toRequest.push( pageCategory );
}
}
toRequest = toRequest.filter( ( category ) => !fullfilled.has( category ) );
}
return categories;
}
/**
* @return {Set<string>}
*/
async function getCountries() {
const api = new mw.Api();
const countries = new Set();
const categories = await getCategories( mw.config.get( 'wgTitle' ), [ 'Deutschland', 'Luxemburg', 'Österreich' ], api );
if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Österreich` ) ) {
countries.add( 'at' );
}
if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Schweiz` ) ) {
countries.add( 'ch' );
}
if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Deutschland` ) ) {
countries.add( 'de' );
}
if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Frankreich` ) ) {
countries.add( 'fr' );
}
if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Luxemburg` ) ) {
countries.add( 'lu' );
}
return countries;
}
/**
* @param {string} address
* @param {HTMLElement} indicator
* @param {HTMLElement} vcard
* @param {boolean} allowNominatim
* @return {{found: boolean, requeue: boolean}}
*/
async function lookupVcard( address, indicator, vcard, allowNominatim ) {
let lookupZip = null;
// Unable to fulfill
if ( memory[ address ] === undefined && !allowNominatim ) {
indicator.classList.remove( 'ext-addresstools-indicator-working' );
return {
found: null,
requeue: true
};
}
if ( memory[ address ] !== undefined ) {
lookupZip = memory[ address ];
} else {
if ( city === null || city === '' ) {
city = await OO.ui.prompt( mw.msg( 'ext-addresstools-cityname-prompt' ), {
textInput: {
value: mw.config.get( 'wgTitle' )
}
} );
if ( city === null || city === '' ) {
return;
}
}
lookupZip = await addressTools.zipCode( address, city );
}
// Update indicator
indicator.classList.add( 'ext-addresstools-indicator-checked' );
indicator.classList.remove( 'ext-addresstools-indicator-unchecked' );
indicator.classList.remove( 'ext-addresstools-indicator-working' );
// No result
if ( lookupZip === null ) {
return {
found: false,
requeue: false
};
}
// Some result
memory[ address ] = lookupZip;
indicator.dataset.result = lookupZip;
indicator.classList.add( 'ext-addresstools-indicator-suggestion' );
indicator.title = mw.msg( 'ext-addresstools-indicator-tooltip-suggestion' );
indicator.addEventListener( 'click', async ( e ) => {
e.preventDefault();
try {
await navigator.clipboard.writeText( `, ${lookupZip}` );
mw.notify( mw.msg( 'ext-addresstools-clipboard-suggestion-success', lookupZip ), {
tag: 'ext-addresstools',
title: 'AddressTools',
type: 'success'
} );
} catch ( ex ) {
console.error( ex );
mw.notify( mw.msg( 'ext-addresstools-clipboard-suggestion-fail', lookupZip ), {
tag: 'ext-addresstools',
title: 'AddressTools',
type: 'error'
} );
}
} );
return {
found: true,
requeue: false
};
}
/**
* @param {Array<Object>} vcards
* @param {boolean} allowNominatim
* @return {Array<Object>} List of skipped vCards
*/
async function lookupVcards( vcards, allowNominatim ) {
for ( const { address, indicator, vcard } of vcards ) {
indicator.classList.add( 'ext-addresstools-indicator-working' );
}
vcards = vcards.reverse(); // reverse, so that list behaves like a queue when popping
const requeue = [];
while ( vcards.length > 0 ) {
const { address, indicator, vcard } = vcards.pop();
const result = await lookupVcard( address, indicator, vcard, allowNominatim );
if ( result.requeue === true ) {
requeue.push( { address, indicator, vcard } );
}
}
return requeue;
}
function setupCss() {
mw.util.addCSS( `.ext-addresstools-indicator {
background-color: white;
border: solid thin #6c757d;
border-radius: 5px;
color: #6c757d;
font-weight: bold;
margin-left: .1rem;
padding: 0 0.35em;
}
.ext-addresstools-indicator-nozip {
border-color: #990000;
color: #990000;
}
.ext-addresstools-indicator-checked.ext-addresstools-indicator-nozip:not(.ext-addresstools-indicator-suggestion) {
border-color: rgba(153, 0, 0, .5);
color: rgba(153, 0, 0, .5);
}
.ext-addresstools-indicator-working {
animation-duration: 1s;
animation-iteration-count: infinite;
animation-name: ext-addresstools-working;
}
.ext-addresstools-indicator-suggestion {
border-color: orangered;
color: orangered;
cursor: pointer;
}
@keyframes ext-addresstools-working {
0% {
background-color: white;
}
50% {
background-color: #eee;
}
100% {
background-color: white;
}
}` );
}
function setupStrings() {
const lang = mw.config.get( 'wgUserLanguage' );
mw.messages.set( Object.fromEntries( Object.keys( strings ).map( ( stringKey ) => {
return [ stringKey, strings[ stringKey ][ lang ] ?? strings[ stringKey ].en ];
} ) ) );
}
function updatePortlet() {
if ( toLookup.length > 0 && !addressTools.AUTOLOOKUP_ZIPCODES && portlet === null ) {
portlet = mw.util.addPortletLink( 'p-tb', '#', mw.msg( 'ext-addresstools-portlet-lookup' ) );
portlet.addEventListener( 'click', async ( e ) => {
e.preventDefault();
portlet.remove();
portlet = null;
await lookupVcards( toLookup, true );
} );
} else if ( toLookup.length === 0 && portlet !== null ) {
portlet.remove();
portlet = null;
}
}
if ( window.nw520 === undefined ) {
window.nw520 = {};
}
window.nw520.addressTools = addressTools;
mw.hook( 'ext.addresstools.loaded' ).fire( addressTools );
main();
} );
//</nowiki>