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
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
//<nowiki> /******************************************************************************* * ListingInfo v1.6 * Date: 2025-01-11 * This script is called as a gadget * Presents a dialog showing characteristics of a single listing * Original author: Roland Unger * Support of desktop and mobile views * Documentation: https://de.wikivoyage.org/wiki/Wikivoyage:ListingInfo.js ******************************************************************************/ /* eslint-disable mediawiki/class-doc */ ( function ( $, mw ) { 'use strict'; var listingPopup = function() { /******************* Internationalization *****************************/ const version = '2025-01-11'; // strings depending on user language const userStrings = { de: { // headers booking: 'Buchung, Vergleich und Bewertung', contact: 'Kontakt', credit: 'Kreditkarten', features: 'Ausstattung', figure: 'Bild', hours: 'Zeiten', map: 'Lagekarte', // contacts email: 'E-Mail', fax: 'Fax', mobile: 'Mobil', phone: 'Tel.', skype: 'Skype', tollfree: 'Tel. gebührenfrei', web: 'Internet', // actionButtons buttonText: 'info', buttonTooltip: 'Öffnet ein Popup-Fenster mit den wichtigsten vCard-Daten und teilweise mit Buchungsmöglichkeiten an', bookingTooltip: 'Buchungslinks anzeigen', notCompleteHint: 'Die nachfolgende Liste erhebt keinen Anspruch auf Vollständigkeit.', closeTooltip: 'Dialogfenster schließen', contactTooltip: 'Kontakt anzeigen', featuresTooltip: 'Ausstattung anzeigen', figureTooltip: 'Abbildung anzeigen', mapTooltip: 'Karte anzeigen', taxiTooltip: 'Bringen Sie mich zu', rssTooltip: 'RSS-Feed der Einrichtung', urlTooltip: 'Website der Einrichtung', extraMaps: '→ Weitere Karten', extraMapsTitle: 'Spezialseite mit weiteren Kartenlinks' }, en: { // headers booking: 'Booking, comparison, and evaluation', contact: 'Contact', credit: 'Credit cards', features: 'Features', figure: 'Image', hours: 'Hours', map: 'Position map', // contacts email: 'Email', fax: 'Fax', mobile: 'Mobile', phone: 'Phone', skype: 'Skype', tollfree: 'Tollfree', web: 'Internet', // actionButtons buttonText: 'info', buttonTooltip: 'Opens a pop-up window with the most important listing data and partly with booking opportunities', bookingTooltip: 'Shows booking links', notCompleteHint: 'The following list does not claim to be complete.', closeTooltip: 'Closes the dialog', contactTooltip: 'Shows contacts', featuresTooltip: 'Shows features', figureTooltip: 'Shows an image', mapTooltip: 'Shows a position map', taxiTooltip: 'Please take me to', rssTooltip: 'RSS feed of this institution', urlTooltip: 'Website of this institution', extraMaps: '→ Extra maps', extraMapsTitle: 'Special page with additional map sources' }, es: { // headers booking: 'Reserva, comparación y valoración', contact: 'Contacto', credit: 'Tarjetas de crédito', features: 'Características', figure: 'Imagen', hours: 'Horas', map: 'Mapa de posiciones', // contacts email: 'Correo electrónico', fax: 'Fax', mobile: 'Móvil', phone: 'Teléfono', skype: 'Skype', tollfree: 'Número gratuito', web: 'Internet', // actionButtons buttonText: 'info', buttonTooltip: 'Abre una ventana emergente con los datos más importantes del listado y en parte con oportunidades de reserva.', bookingTooltip: 'Mostrar enlaces de reserva', notCompleteHint: 'La siguiente lista no pretende ser completa.', closeTooltip: 'Cerrar ventana de diálogo', contactTooltip: 'Mostrar contacto', featuresTooltip: 'Mostrar aracterísticas', figureTooltip: 'Mostrar imagen', mapTooltip: 'Mostrar apa de posiciones', taxiTooltip: 'Please take me to', rssTooltip: 'Canal RSS de esta institución', urlTooltip: 'Sitio web de esta institución', extraMaps: '→ Mapas adicionales', extraMapsTitle: 'Página especial con fuentes de mapas adicionales' } }; const translations = { de: { takeRequest: 'Bitte bringen Sie mich zu…', name: 'Name', comment: 'Kommentar', address: 'Anschrift', directions: 'Wegbeschreibung' }, en: { takeRequest: 'Please take me to…', name: 'Name', comment: 'Comment', address: 'Address', directions: 'Directions' }, af: { takeRequest: 'Neem my asseblief na …', name: 'Naam', comment: 'Opmerking', address: 'Adres', directions: 'Hoe om daar te kom' }, // native speaker ar: { takeRequest: 'من فضلك ، أريد الذهاب إلى…', name: 'الاسم', comment: 'التعليق', address: 'العنوان', directions: 'الموقع والاتجاه' }, // native speaker az: { takeRequest: 'Xahiş edirəm məni … aparın', name: 'Ad', comment: 'Şərh', address: 'Ünvan', directions: 'Yer və necə getməli' }, // native speaker be: { takeRequest: 'Калі ласка, вазьміце мяне да …' }, // Weißrussisch bg: { takeRequest: 'Моля, вземете ме до …' , name: 'Име', comment: 'Коментар', address: 'Адрес', directions: 'Местоположение и пристигане' }, // native speaker bn: { takeRequest: 'আমাকে নিয়ে যান …', name: 'নাম', comment: 'মন্তব্য', address: 'ঠিকানা', directions: 'দিকনির্দেশ' }, bs: { takeRequest: 'Molim te, vodi me do …', name: 'Ime', comment: 'Komentar', address: 'Adresa', directions: 'Mjesto i odredište' }, // native speaker ca: { takeRequest: 'Porta’m a …' }, cs: { takeRequest: 'Vezměte mě prosím na toto místo:', name: 'Jméno', comment: 'Komentář', address: 'Adresa', directions: 'Cíl a cesta' }, // native speaker da: { takeRequest: 'Jeg vil gerne til …', name: 'Navn', comment: 'Kommentar', address: 'Adresse', directions: 'Kørselsvejledning' }, // native speaker el: { takeRequest: 'Παρακαλώ να με πάτε στο(ν)/στη(ν) …', name: 'Όνομα', comment: 'Σχόλιο', address: 'Διεύθυνση', directions: 'Τοποθεσία και άφιξη' }, // native speaker es: { takeRequest: 'Por favor, lléveme a…', name: 'Nombre', comment: 'Comentario', address: 'Dirección', directions: 'Indicaciones' }, // native speaker et: { takeRequest: 'Palun viige mind …', name: 'Nimi', comment: 'Kommentaar', address: 'Aadress', directions: 'Asukoht ja saabumine' }, // native speaker fa: { takeRequest: 'لطفا من را ببر به…', name: 'نام', comment: 'یادداشت', address: 'نشانی', directions: 'مسیرها' }, // native speaker fi: { takeRequest: 'Vie minut …', name: 'Nimi', comment: 'Kommentti', address: 'Osoite', directions: 'Ohjeet' }, // native speaker fr: { takeRequest: 'Pouvez-vous m’emmenez à/au…', name: 'Nom', comment: 'Commentaire', address: 'Adresse', directions: 'Direction' }, // native speaker ga: { takeRequest: 'Tabhair dom go …' }, // Irisch gd: { takeRequest: 'Thoir dhomh gu …' }, // Schottisch-gälisch gu: { takeRequest: 'કીરપા કરકે મૈના લે જો …' , // Gujarati provided by an Indian guy name: 'નાબ', comment: 'પાસ', address: 'પતા', directions: 'ઢીશા' }, he: { takeRequest: 'בבקשה תיקח אותי ל…', name: 'שם', comment: 'תגובה', address: 'כתובת', directions: 'הוראות' }, hi: { takeRequest: 'कृपया मुझे वहाँ ले जाएं…' , name: 'नाम', comment: 'पास', address: 'पता', directions: 'दीशा' }, //Hindi provided by an Indian guy hr: { takeRequest: 'Molim Vas, odvedi me …' }, hu: { takeRequest: 'Kérem, vigyen a/az …-hoz/hez/höz', name: 'Név', comment: 'Megjegyzés', address: 'Cím', directions: 'Odajutás' }, // native speaker hy: { takeRequest: 'Խնդրում եմ ինձ տանել …', name: 'Անուն', comment: 'Բացատրություն', address: 'Հասցե', directions: 'Վայր եւ ցուցումներ' }, // Armenisch, native speaker id: { takeRequest: 'Tolong hantarkan saya ke …', name: 'Nama', comment: 'Komen', address: 'Alamat', directions: 'Arah' }, // Indonesian: same like malay (statement of a Sabahan native Malay speaker) is: { takeRequest: 'Vinsamlegast taktu mig til …' }, it: { takeRequest: 'Per favore, mi porti a…', name: 'Nome', comment: 'Commento', address: 'Indirizzo', directions: 'Posizione e arrivo' }, // Italian native speaker ja: { takeRequest: '… までお願いします。', name: '場所', comment: 'コメント', address: '住所', directions: 'アクセス' }, // Japanese native speaker ka: { takeRequest: 'გთხოვთ მიმიყვანოთ…', name: 'დასახელება', comment: 'კომენტარი', address: 'მისამართი', directions: 'დანიშნულების ადგილი' }, // Georgisch, native speaker kk: { takeRequest: 'Маған …' }, // Kasachisch km: { takeRequest: 'សូមជូនខ្ញុំទៅ …', name: 'ឈ្មោះ', comment: 'នែនាំ', address: 'អាសយដ្ឋាន', directions: 'ទិសដៅ' }, // Khmer: by native speaker from Phnom Penh ko: { takeRequest: '… 로 가주세요', name: '이름', comment: '호텔 안내', address: '주소', directions: '찾아가는 길' }, // Korean: verified by a native speaker from Seoul ky: { takeRequest: 'Суранам, мени алып…' }, // Kirgisisch lb: { takeRequest: 'Huelt mech op …' }, lt: { takeRequest: 'Prašau, paimk mane …' }, lv: { takeRequest: 'Lūdzu, aizvediet mani uz …', name: 'Nosaukums', comment: 'Komentars', address: 'Adrese', directions: 'Virzieni' }, // Lettisch -- native speaker mk: { takeRequest: 'Ве молам, земи ме …' }, // Mazedonisch mn: { takeRequest: 'Намайг аваарай …' }, ms: { takeRequest: 'Tolong hantarkan saya ke …', name: 'Nama', comment: 'Komen', address: 'Alamat', directions: 'Arah' }, // Malay: verified by a Sabahan native speaker mt: { takeRequest: 'Jekk jogħġbok ħudni …' }, my: { takeRequest: 'ငါ့ကိုအယူကို ကျေးဇူးပြု. ...' }, // Birmanisch nan: { takeRequest: '請帶我去 …', name: '姓名', comment: '評語', address: '住址', directions: '抵達方式' }, // Taiwanesisch: verified by a Taiwanese native speaker nb: { takeRequest: 'Kan du kjøre meg til …', name: 'Navn', comment: 'Kommentar', address: 'Adresse', directions: 'Sted og tid' }, // Bokmål, native speaker ne: { takeRequest: 'कृपया मुझे वहाँ ले जाएं…' , name: 'नाम', comment: 'पास', address: 'पता', directions: 'दीशा' }, // Nepalese provided by an Indian guy nl: { takeRequest: 'Breng me alstublieft naar…', name: 'Naam', comment: 'Commentaar', address: 'Adres', directions: 'Route' }, // native speaker no: { takeRequest: 'Kan du kjøre meg til …', name: 'Navn', comment: 'Kommentar', address: 'Adresse', directions: 'Sted og tid' }, // Bokmål, native speaker pl: { takeRequest: 'Proszę mnie zabrać do…', name: 'Nazwa', comment: 'Komentarz', address: 'Adres', directions: 'Wskazówki dojuzdu' }, // native speaker pt: { takeRequest: 'Por favor, leve-me para …', name: 'Nome', comment: 'Comentário', address: 'Endereço', directions: 'Direções' }, // native speaker pu: { takeRequest: 'ਕਿਰਪਾ ਕਰਕੇ ਮੈਨੂ ਲੈ ਚਲੌं…' , name: 'ਨਾਮ', comment: 'ਢੇ ਕੋਲ', address: 'ਪਤਾ', directions: 'ਵਲ' }, //Punjabie provided by an Indian guy ro: { takeRequest: 'Te rog să mă dai la …', name: 'Nume', comment: 'Comentariu', address: 'Adresă', directions: 'Indicații' }, ru: { takeRequest: 'Пожалуйста, отвезите меня в…', name: 'Название', comment: 'Комментарий', address: 'Адрес', directions: 'Пояснения' }, // Russian native speaker sk: { takeRequest: 'Prosím, môžete ma vziať na miesto …', name: 'Menom', comment: 'Komentár', address: 'Adresa', directions: 'v oblasti' }, // native spaker sl: { takeRequest: 'Prosim, vzemite me …' }, sq: { takeRequest: 'Ju lutem më dërgoni në …', name: 'Emri', comment: 'Komenti', address: 'Adresa', directions: 'Vendndodhja dhe Mbërritja' }, // Albanisch, native speaker sr: { takeRequest: 'Молим те, води ме до …', name: 'Име', comment: 'Коментар', address: 'Адреса', directions: 'Место и одредиште' }, // native speaker sv: { takeRequest: 'Snälla ta mig till …', name: 'Namn', comment: 'Kommentar', address: 'Adress', directions: 'Vägbeskrivning' }, // native speaker tg: { takeRequest: 'Лутфан маро ба …' }, th: { takeRequest: 'กรุณาพาฉันไปที่ …' , name: 'ชื่อ', comment: 'แนะนำ', address: 'ที่อยู่', directions: 'สถานที่ตั้ง' }, //Thai: by native speakerfrom Chiang Mai, North Thailand tl: { takeRequest: 'Pakiusap dalhin mo ako sa …', name: 'Pangalan', comment: 'Komento', address: 'Address', directions: 'Paano pumunta doon' }, // Tagalog (Filipino) provided by a native speaker from anywhere in Luzon tr: { takeRequest: 'Lütfen beni … götür', name: 'Ad', comment: 'Özellik', address: 'Adres', directions: 'Yer ve varış noktası' }, // native speaker uk: { takeRequest: 'Будь ласка, відвезіть мене до …', name: 'Назва', comment: 'Коментар', address: 'Адреса', directions: 'Як дістатись' }, // native speaker uz: { takeRequest: 'Iltimos, meni olib boring …' }, vi: { takeRequest: 'Xin hãy đưa tôi đến …', name: 'Tên', comment: 'Chú thích', address: 'Địa chỉ', directions: 'Chỉ đường' }, yue: { takeRequest: '请带我去……', name: '名字', comment: '评论', address: '地址', directions: '方向' }, // Cantonese: from a native speaker from city of Guangzhou zh: { takeRequest: '您好,请您带我去……', name: '名称', comment: '评价与备注', address: '地址', directions: '如何到达酒店' }, // Mandarin: from a native speaker from city of Hefei }; const sites = [ { data: 'data-agoda-com', site: 'Agoda.com', title: 'Hotel auf Agoda.com', formatter: 'https://www.agoda.com/de-de/$1.html', grClass: 'group1' }, { data: 'data-booking-com', site: 'Booking.com', title: 'Hotel auf Booking.com', formatter: 'https://www.booking.com/hotel/$1.de.html', grClass: 'group1' }, { data: 'data-expedia-com', site: 'Expedia.com', title: 'Hotel auf Expedia.com', formatter: 'https://www.expedia.com/$1.Hotel-Information', grClass: 'group1' }, { data: 'data-expedia-com', site: 'Expedia.de', title: 'Hotel auf Expedia.de', formatter: 'https://www.expedia.de/$1.Hotel-Beschreibung', grClass: 'group1' }, { data: 'data-historic-hotels-america', site: 'HistoricHotels.org', title: 'Hotel auf HistoricHotels.org', formatter: 'https://www.historichotels.org/hotels-resorts/$1', grClass: 'group1' }, { data: 'data-historic-hotels-europe', site: 'HistoricHotelsOfEurope.com', title: 'Hotel auf HistoricHotelsOfEurope.com', formatter: 'https://www.historichotelsofeurope.com/property-details.html/$1', grClass: 'group1' }, { data: 'data-historic-hotels-worldwide', site: 'HistoricHotelsWorldwide.com', title: 'Hotel auf HistoricHotelsWorldwide.com', formatter: 'http://www.historichotelsworldwide.com/hotels-resorts/$1', grClass: 'group1' }, { data: 'data-hotels-com', site: 'Hotels.com', title: 'Hotel auf Hotels.com', formatter: 'https://de.hotels.com/$1/', grClass: 'group1' }, { data: 'data-hostelworld-com', site: 'Hostelworld.com', title: 'Hostel auf Hostelworld.com', formatter: 'https://www.hostelworld.com/hosteldetails.php/_/_/$1', grClass: 'group1' }, { data: 'data-kayak-com', site: 'Kayak.com', title: 'Hotel auf Kayak.com', formatter: 'https://www.kayak.de/hotels/-h$1-details/', grClass: 'group1' }, { data: 'data-leading-hotels', site: 'LHW.com', title: 'Hotel auf Leading Hotels of the World', formatter: 'https://www.lhw.com/hotel/$1', grClass: 'group1' }, { data: 'data-preferred-hotels', site: 'PreferredHotels.com', title: 'Hotel auf PreferredHotels.com', formatter: 'https://preferredhotels.com/destinations/$1', grClass: 'group1' }, { data: 'data-recreation-gov', site: 'Recreation.gov facility', title: 'Einrichtung auf Recreation.gov', formatter: 'https://www.recreation.gov/recreationalAreaDetails.do?facilityId=$1', grClass: 'group1' }, { data: 'data-relais-chateaux', site: 'RelaisChateaux.com', title: 'Einrichtung auf RelaisChateaux.com', formatter: 'https://www.relaischateaux.com/us/wd/$1', grClass: 'group1' }, { data: 'data-skyscanner-com', site: 'Skyscanner.com', title: 'Metasuche auf Skyscanner.com', formatter: 'https://www.skyscanner.de/hotels/_/_/_/ht-$1', grClass: 'group1' }, { data: 'data-trip-com', site: 'Trip.com', title: 'Einrichtung auf Trip.com', formatter: 'https://www.trip.com/hotels/_-hotel-detail-$1', grClass: 'group1' }, { data: 'data-tripadvisor-com', site: 'Tripadvisor.com', title: 'Einrichtung auf Tripadvisor.com', formatter: 'https://www.tripadvisor.com/$1', grClass: 'group1' }, { data: 'data-alpenverein-de', site: 'Alpenverein.de', title: 'Schutzhütte auf Alpenverein.de', formatter: 'https://www.alpenverein.de/DAV-Services/Huettensuche/wd/$1', grClass: 'group1' }, { data: 'data-alpenverein-at', site: 'Alpenverein.at', title: 'Schutzhütte auf Alpenverein.at', formatter: 'https://www.alpenverein.at/huetten/index.php?huette_nr=$1', grClass: 'group1' }, { data: 'data-pzs-si', site: 'PZS.si', title: 'Schutzhütte auf im Verzeichnis des Alpenvereins Sloweniens', formatter: 'https://en.pzs.si/koce.php?pid=$1', grClass: 'group1' }, { data: 'data-sac-cas-ch', site: 'SAC-CAS.ch', title: 'Schutzhütte und Gipfel auf im Verzeichnis des Schweizer Alpen-Clubs', formatter: 'https://beta.sac-cas.ch/de/huetten-und-touren/tourenportal/$1/', grClass: 'group1' }, { data: 'data-station-number', site: 'Abfahrtstafel Deutsche Bahn', title: 'Abfahrtstafel der Deutschen Bahn', formatter: 'https://reiseauskunft.bahn.de/bin/bhftafel.exe/dn?rt=1&input=$1&boardType=dep&time=actual&productsFilter=1111111111&start=yes', grClass: 'group1' }, //de -> en { data: 'data-station-number', site: 'Abfahrtstafel Deutsche Bahn', title: 'Abfahrtstafel der Deutschen Bahn', formatter: 'https://reiseauskunft.bahn.de/bin/bhftafel.exe/en?rt=1&input=$1&boardType=dep&time=actual&productsFilter=1111111111&start=yes', grClass: 'group1' }, { data: 'data-station-number', site: 'Auskunft Deutsche Bahn', title: 'Auskunft und Buchung der Deutschen Bahn', formatter: 'https://www.bahn.de/buchung/start?intern=1&so=$1', grClass: 'group1' }, { data: 'data-osm-relation-id', site: 'OpenStreetMap.org (Relation)', title: 'Einrichtung auf OpenStreetMap', formatter: 'https://www.openstreetmap.org/relation/$1', grClass: 'group2' }, { data: 'data-osm-way-id', site: 'OpenStreetMap.org (Umriss)', title: 'Einrichtung auf OpenStreetMap', formatter: 'https://www.openstreetmap.org/way/$1', grClass: 'group2' }, { data: 'data-osm-node-id', site: 'OpenStreetMap.org (Knoten)', title: 'Einrichtung auf OpenStreetMap', formatter: 'https://www.openstreetmap.org/node/$1', grClass: 'group2' }, { data: 'data-foursquare-id', site: 'Foursquare.com', title: 'Einrichtung auf Foursquare.com', formatter: 'https://www.foursquare.com/v/$1', grClass: 'group2' }, { data: 'data-google-maps-cid', site: 'Maps.google.com', title: 'Einrichtung auf Google Maps', formatter: 'https://maps.google.com/?cid=$1', grClass: 'group2' } ]; // technical constants const contactKeys = [ 'phone', 'mobile', 'tollfree', 'fax', 'email', 'skype' ], fallbackLang = 'en', allowedNamespaces = [ 0, // Main 2, // User 4 // Project ]; // separators for translate function const separators = { header: '<br />', section: ' / ' }; const selectors = { background: '#voy-info-background', kartographerLink: '.mw-kartographer-maplink', listing: '.vcard', infoDialog: '#voy-listing-info', metadata: 'span.listing-metadata-items' }; const classes = { address: 'listing-address', alt: 'listing-alt', checkin: 'listing-checkin', checkout: 'listing-checkout', comment: 'listing-comment', commons: 'listing-sister-commons', credit: 'listing-credit', directions: 'listing-directions', email: 'listing-email', fax: 'listing-fax', features: 'listing-subtype', hours: 'listing-hours', icon: 'listing-icon', mobile: 'listing-mobile', name: 'listing-name', phone: 'listing-landline', tollfree: 'listing-tollfree', skype: 'listing-skype', socialMedia: 'listing-social-media', prefix: 'voy-info-', booking: 'voy-info-booking', button: 'voy-info-button', buttonImage: 'voy-info-button-img', buttonPane: 'voy-info-button-pane', container: 'voy-info-container', image: 'voy-info-image', infoPane: 'voy-info-pane', isMobile: 'voy-info-mobile', map: 'voy-info-map', mapCation: 'voy-info-map-caption', extraMaps: 'voy-info-extra-maps', placesList: 'voy-info-places-list', background: 'ui-widget-overlay' }; const data = { addressLocal: 'data-address-local', color: 'data-color', directionsLocal: 'data-directions-local', image: 'data-image', lat: 'data-lat', lon: 'data-lon', lang: 'data-lang', name: 'data-name', nameLocal: 'data-name-local', rss: 'data-rss', type: 'data-group', // other wikis: 'data-type' url: 'data-url', zoom: 'data-zoom' }; const makiIcons = { area: 'land-use', buy: 'shop', 'do': 'swimming', drink: 'bar', eat: 'restaurant', error: 'cross', go: 'suitcase', health: 'hospital', nature: 'park', other: 'star-stroked', religion: 'circle-stroked', see: 'town-hall', sleep: 'lodging', populated: 'town', view: 'camera', }; // internal use const pageLang = mw.config.get( 'wgPageContentLanguage' ), userLang = mw.config.get( 'wgUserLanguage' ), isMobile = ( /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( navigator.userAgent.toLowerCase() ) ); // isMobile = true; // dialog move const position = { mouse: {}, dialog: {} }; // map support const mapParams = { map: null, url: `https://${pageLang}.wikivoyage.org/w/index.php?title=Special%3AMapsources¶ms=` }; /********************* String management ******************************/ var messages = {}; // copying translation strings to messages depending on chain languages function addMessages( strings, chain ) { for ( var i = chain.length - 1; i >= 0; i-- ) { if ( strings.hasOwnProperty( chain[ i ] ) ) { $.extend( messages, strings[ chain[ i ] ] ); } } } // copying translation strings to messages function setupMessages() { const chain = userLang == pageLang ? [ pageLang, fallbackLang ] : [ userLang, pageLang, fallbackLang ]; addMessages( userStrings, chain ); } /************************** Dialog ************************************/ // Opening the dialog function open() { close(); const width = 400, height = 300, id = selectors.infoDialog.substring( 1 ); var left = ( document.body.scrollWidth - width ) / 2 + $( document ).scrollLeft(); left = left < 0 ? 0 : left; var top = ( window.innerHeight - height ) / 2 + $( document ).scrollTop(); top = top < 0 ? 0 : top; const infoDialog = $( '<div/>', { id: id, 'class': 'mw-parser-output' + ( isMobile ? ( ' ' + classes.isMobile ) : '' ), role: 'dialog', tabindex: '-1', 'aria-modal': 'true', 'aria-labelledby': id, 'data-version': version, css: { width: width, height: height, display: 'flex', left: left, top: top } }) .keydown( handleKeyCodes ); // Handle TAB and ESC keycodes $( 'body' ) .append( $( '<div/>', { id: selectors.background.substring( 1 ), 'class': classes.background }) ) .append( infoDialog ) .click( handleOutsideClick ); return infoDialog; } // Key code event handler for TAB and ESC keys function handleKeyCodes( event ) { switch( event.keyCode ) { case 9: // TAB const tabbables = $( event.delegateTarget ).find( ':visible' ).filter( 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' ), first = tabbables.filter( ':first' ), last = tabbables.filter( ':last' ); if ( event.target === last[ 0 ] && !event.shiftKey ) { event.preventDefault(); first.focus(); } else if ( event.target === first[ 0 ] && event.shiftKey ) { event.preventDefault(); last.focus(); } break; case 27: // ESC event.preventDefault(); close(); } } // Click event handler if clicked outside the dialog function handleOutsideClick( event ) { // Real ouside click? if ( !$( event.target ).closest( selectors.infoDialog ).length ) { event.preventDefault(); close(); } else { const infoDialog = $( selectors.infoDialog ), disabledButton = $( 'button:disabled', infoDialog ); if ( disabledButton.length ) { disabledButton.next().focus(); } else { $( 'button', infoDialog ).first().focus(); } } } // Closing the dialog function close() { $( 'body' ).off( 'click', handleOutsideClick ); $( selectors.infoDialog ).remove(); $( selectors.background ).remove(); } // Focus the button of the following info pane function focusNextButton() { $( selectors.infoDialog + ' button:disabled' ).next().focus(); } // Creating header with the opportunity to use it as a move tool function makeHeader( text, move ) { const header = $( '<h2/>', { css: { flex: 'none' } }).html( text ); if ( move ) { header.css( 'cursor', 'move' ) .mouseup( function( e ) { $( this ).off( 'mousemove', dialogMove ).css( 'cursor', 'move' ); focusNextButton(); }) .mouseout( function( e ) { $( this ).off( 'mousemove', dialogMove ).css( 'cursor', 'move' ); focusNextButton(); }) .mousedown( function( e ) { var dialog = $( selectors.infoDialog ); $( this ).on( 'mousemove', dialogMove ).css( 'cursor', 'grabbing' ); position.mouse.X = e.clientX; position.mouse.Y = e.clientY; position.dialog.X = parseInt( dialog.css( 'left' ) ); position.dialog.Y = parseInt( dialog.css( 'top' ) ); }); } return header; } // Event handler for mouse move function dialogMove( e ) { $( selectors.infoDialog ) .css( { left: position.dialog.X - position.mouse.X + e.clientX, top: position.dialog.Y - position.mouse.Y + e.clientY } ); } /***************** Info panes and selection ***************************/ // Making buttons with event handlers // Image name is used as an id, too // cancel = true means it is the cancel button function makeButton( buttonPane, img, tooltip, cancel ) { const button = $( '<button/>', { 'class': classes.button, title: messages[ tooltip ], id: img, css: { clear: 'right', float: 'right' } }) .append( $( '<div/>', { 'class': classes.buttonImage }) ); if ( cancel ) { button.click( function() { close(); }); } else { button.click( function( e ) { changeInfoPane( e, buttonPane ); }); } button.appendTo( buttonPane ); } // Make an info pane distinguishable by id function makeInfoPane( id ) { return $( '<div/>', { id: classes.prefix + id, 'class': classes.infoPane, css: { display: 'none' } }); } // Select an info pane by id and disable its button function selectInfoPane( buttonPane, id ) { const buttons = buttonPane.children(); if ( buttons.length < 2 ) { $( '.voy-info-pane' ).show(); return; } if ( !id ) { id = buttons.first().attr( 'id' ); } buttons.each( function() { $( this ).prop( 'disabled', $( this ).attr( 'id' ) === id ); }); buttonPane.siblings().each( function() { if ( $( this ).attr( 'id' ) === classes.prefix + id ) { $( this ).css( { display: 'flex', 'flex-direction': 'column' } ) .trigger( 'voy:show' ); } else { $( this ).css( { display: 'none' } ); } }); buttons.filter( ':disabled' ).next().focus(); } // Event handler for info-pane change triggered by clicking on its // belonging buttons or button child images function changeInfoPane( e, buttonPane ) { const button = $( e.target ).closest( 'button' ), id = button.length ? button.attr( 'id' ) : ''; if ( id ) { selectInfoPane( buttonPane, id ); } } /********************** Helper function *******************************/ // Specifying language of a text and wrap it with right-to-left mark // if necessary function langSpan( text, lang ) { if ( !text || text === '' ) { return ''; } const r2l = { ar: 1, dv: 1, fa: 1, he: 1, ms: 1, ur: 1, }; const dir = lang in r2l ? ' dir="rtl"' : '', t =`<span lang="${lang}"${dir}>${text}</span>`; return lang in r2l ? `‏${t}‎` : t; } // Getting HTML text from a wrapper tag specified by aClass. // context is the dialog itself function getHTML( aClass, context ) { const span = $( '.' + aClass, context ); return span.length ? span.first().html() : ''; } function getOuterHTML( aClass, context ) { const span = $( '.' + aClass, context ); return span.length ? span.first().prop( 'outerHTML' ) : ''; } // Making a header with wikilang and country lang identifiers function translate( lang, wikiLang, id, separator ) { var s = translations[ wikiLang ][ id ]; if ( wikiLang !== lang ) { var t = ''; if ( lang && lang in translations && id in translations[ lang ] ) { t = langSpan( translations[ lang ][ id ], lang ); } else if ( wikiLang !== 'en' ) { t = translations.en[ id ]; } s += t ? separator + t : ''; } return s; } // replace spaces and entities function replaceEntities( s ) { return $( '<span />' ).html( s.replace( /\s/g, '_' ) ).text(); } /**************** Making and filling dialog ***************************/ // Creating the dialog // Event handler called by listingPopup.init function dialog( element ) { const listing = element.closest( selectors.listing ), listingName = $( '.' + classes.name, listing ).first(), place = {}, link = $( 'a', listingName ).first(); const dialog = open(), buttonPane = $( '<div/>', { id: classes.buttonPane }); place.lang = listing.attr( data.lang ); const at = place.lang.indexOf( '-' ); if ( at > -1 ) { place.lang = place.lang.substring( 0, at ); } place.name = listing.attr( data.name ); if ( !place.name ) { place.name = link.length ? link.text() : listingName.text(); } if ( link.length ) { place.nameHTML = `<a href="${link.attr( 'href' )}" title="${place.name}" target="_blank">${place.name}</a>`; } const s = listing.attr( data.nameLocal ); place.nameLocal = s ? langSpan( s, place.lang ) : getHTML( classes.alt, listing ); place.nameAll = place.nameHTML || place.name; if ( place.nameLocal ) { place.nameAll += '<br />' + place.nameLocal; } // adding pages for ( var i = 0; i < pages.length; i++ ) { pages[ i ]( dialog, buttonPane, listing, place ); } // combining elements if ( $( 'button', buttonPane ).length < 2 ) { buttonPane.empty(); } makeButton( buttonPane, 'cancelImg', 'closeTooltip', true ); dialog.append( buttonPane ); selectInfoPane( buttonPane ); } const pages = []; /****** "Bring me to …" page ******/ function bringMeToPage( dialog, buttonPane, listing, place ) { function placeInfo( container, key, keyLocal, wikiLang ) { const global = getHTML( classes[ key ], listing ); var s = keyLocal ? listing.attr( data[ keyLocal ] ) : null; if ( global && s && global.toLowerCase() == s.toLowerCase() ) { s = null; } const local = s ? langSpan( s, place.lang ) : ''; if ( global || local ) { s = translate( place.lang, wikiLang, key, separators.section ); var t = local; if ( global ) { t += local ? '<br />' + global : global; } container.append( `<dl><dt>${s}</dt><dd>${t}</dd></dl>` ); } } const buttonId = 'taxiImg'; var wikiLang = pageLang; if ( userLang && translations[ userLang ] ) { wikiLang = userLang; } var s = translate( place.lang, wikiLang, 'takeRequest', separators.header ); const container = $( '<div/>', { 'class': classes.container }); const infoPane = makeInfoPane( buttonId ) .append( makeHeader( s, true ) ) .append( container ); s = translate( place.lang, wikiLang, 'name', separators.section ); var t = place.nameLocal; t += place.nameLocal ? '<br />' + place.name : place.name; container.append( `<dl><dt>${s}</dt><dd>${t}</dd></dl>` ); placeInfo( container, 'comment', null, wikiLang ); placeInfo( container, 'address', 'addressLocal', wikiLang ); placeInfo( container, 'directions', 'directionsLocal', wikiLang ); dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'taxiTooltip', false ); } pages.push( bringMeToPage ); /****** Image page ******/ function imagePage( dialog, buttonPane, listing, place ) { const buttonId = 'figureImg'; var image = replaceEntities( listing.attr( data.image ) || '' ); if ( image ) { var s = place.nameAll; image = 'https://commons.wikimedia.org/wiki/Special:FilePath/' + mw.html.escape( image ) + '?width=700'; image = `<img src="${image}" title="${place.name}" />`; var infoPane = makeInfoPane( buttonId ) .append( `<div class="${classes.image}">${image}</div>` ) .append( `<p><span class="voy-info-name">${s}</span> ` + `${getOuterHTML( classes.commons, listing )}</p>` ); // map support mapParams.thumb = image; dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'figureTooltip', false ); } } pages.push( imagePage ); /****** Map page ******/ // Creating a Kartographer map function createMap() { // see also: https://www.mediawiki.org/wiki/Help:Extension:Kartographer/Developer_guide mw.loader.using( [ 'ext.kartographer.box' ], function () { const kartoBox = mw.loader.require( 'ext.kartographer.box' ); mapParams.map = kartoBox.map( { container: $( '#' + classes.map )[ 0 ], center: [ mapParams.lat, mapParams.lon ], captionText: mapParams.title, zoom: 15, allowFullScreen: true, alwaysInteractive: true, isFullScreen: false, featureType: 'mapframe' } ); const mapData = [ { 'type': 'Feature', properties: { 'marker-color': mapParams.color, 'marker-size': 'medium', 'marker-symbol': makiIcons[ mapParams.type ] || '', title: mapParams.title, description: mapParams.thumb }, geometry: { 'type': 'Point', coordinates: [ mapParams.lon, mapParams.lat ] } } ]; const layerOptions = { name: 'Position' }; mapParams.map.addGeoJSONLayer( mapData, layerOptions ); } ); } function extraMaps() { const scales = [ 500000000, 250000000, 150000000, 70000000, 35000000, 15000000, 10000000, 4000000, 2000000, 1000000, 500000, 250000, 150000, 70000, 35000, 15000, 8000, 4000, 2000, 1000 ], scale = scales[ mapParams.zoom ] || 17; var lat = parseFloat( mapParams.lat ), lon = parseFloat( mapParams.lon ), url = mapParams.url + Math.abs( lat ) + ( lat < 0 ? '_S_' : '_N_' ) + Math.abs( lon ) + ( lon < 0 ? '_W' : '_E' ) + `_scale%3A${scale}&locname=${encodeURI( mapParams.title.replace( / /g, '+' ) )}`; return `<div class="${classes.extraMaps}" title="${messages.extraMapsTitle}"><a href="${url}" target="_blank">${messages.extraMaps}</a></div>`; } function mapPage( dialog, buttonPane, listing, place ) { mapParams.map = null; const link = $( selectors.kartographerLink, listing ).first(); if ( link.length ) { const buttonId = 'mapImg'; var s = place.nameAll; mapParams.title = place.name; mapParams.type = listing.attr( data.type ); mapParams.lat = link.attr( data.lat ); mapParams.lon = link.attr( data.lon ); mapParams.color = listing.attr( data.color ); mapParams.zoom = listing.attr( data.zoom ); if ( !mapParams.zoom ) { mapParams.zoom = 17; } const infoPane = makeInfoPane( buttonId ) .append( $( '<div/>', { id: classes.map }) ) .append( `<p class="${classes.mapCation}">${s}</p>` ) .append( extraMaps() ) .on( 'voy:show', function( event ) { // map is created later if dialog is visible if ( !mapParams.map ) { createMap(); } }); dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'mapTooltip', false ); } } pages.push( mapPage ); /****** Contact page ******/ function contactPage( dialog, buttonPane, listing, place ) { function makeImgLink( linkType, link, title ) { return `<span class="${classes.icon} listing-${linkType}" title="${title}">` + `<a class="external text" href="${link}" rel="nofollow" target="_blank">` + `<span style="color-adjust:exact;-webkit-print-color-adjust:exact;print-color-adjust:exact">${linkType}</span>` + '</a></span> '; } const buttonId = 'contactImg'; const container = $( '<div/>', { 'class': classes.container }); var c, i, s; for ( i = 0; i < contactKeys.length; i++ ) { c = contactKeys[ i ]; s = getHTML( classes[ c ], listing ); if ( s ) { container.append( `<p><span class="voy-info-section">${messages[ c ]}:</span> ${s}</p>` ); } } s = ''; const url = listing.attr( data.url ); s = url ? makeImgLink( 'url', url, messages.urlTooltip ) : ''; const rss = listing.attr( data.rss ); s += rss ? makeImgLink( 'rss', rss, messages.rssTooltip ) : ''; const socialMedia = $( '.' + classes.socialMedia, listing ); if ( socialMedia.length ) { socialMedia.each( function() { var icon = $( this ).prop( 'outerHTML' ) .replace( '<a ', '<a target="_blank" ' ); s += icon; }); } if ( s ) { container.append( `<p><span class="voy-info-section">${messages.web}:</span> ${s}</p>` ); } if ( $( 'p', container ).length ) { const infoPane = makeInfoPane( buttonId ) .append( makeHeader( messages.contact, true ) ) .append( container ); dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'contactTooltip', false ); } } pages.push( contactPage ); /****** Features page ******/ function featuresPage( dialog, buttonPane, listing, place ) { const buttonId = 'featuresImg'; const infoPane = makeInfoPane( buttonId ) .css( { 'overflow-y': 'auto' } ); var move = true; var features = getHTML( classes.features, listing ); if ( features ) { infoPane.append( makeHeader( messages.features, move ) ); move = false; infoPane.append( `<p>${features}</p>` ); } var credit = getHTML( classes.credit, listing ); if ( credit ) { infoPane.append( makeHeader( messages.credit, move ) ); move = false; infoPane.append( `<p>${credit}</p>` ); } const params = { hours: getHTML( classes.hours, listing ), checkin: getHTML( classes.checkin, listing ), checkout: getHTML( classes.checkout, listing ) }; if ( params.hours + params.checkin + params.checkout ) { infoPane.append( makeHeader( messages.hours, move ) ); var key, value; for ( key in params ) { value = params[ key ]; if ( value ) { infoPane.append( `<p>${value}</p>` ); } } } if ( $( 'h2', infoPane ).length ) { dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'featuresTooltip', false ); } } pages.push( featuresPage ); /****** Booking, comparison, and rating page ******/ function bookingPage( dialog, buttonPane, listing, place ) { var count = 0, i, li, s, site; const ul = $( '<ul/>', { 'class': classes.booking }); for ( i = 0; i < sites.length; i++ ) { site = sites[ i ]; s = listing.attr( site.data ); if ( s ) { s = site.formatter.replace( '$1', s ); li = $( '<li/>', { 'class': selectors.infoDialog.substring( 1 ) + '-' + site.grClass, }) .append( $( '<a/>', { 'class': 'external text', target: '_blank', href: s, title: site.title, text: site.site }) ); ul.append( li ); count += 1; } } if ( count ) { const buttonId = 'bookingImg'; const container = $( '<div/>', { 'class': classes.container }); const infoPane = makeInfoPane( buttonId ) .append( makeHeader( messages.booking, true ) ) .append( container ); container.append( `<p>${messages.notCompleteHint}</p>` ); container.append( `<ul class="${classes.placesList}"><li>${place.nameHTML || place.name}</li></ul>` ) .append( ul ); dialog.append( infoPane ); makeButton( buttonPane, buttonId, 'bookingTooltip', false ); } } pages.push( bookingPage ); /*********************** Initialization *******************************/ // Check if namespace and action is allowed function checkIfAllowed() { const namespace = mw.config.get( 'wgNamespaceNumber' ); return allowedNamespaces.includes( namespace ); } // Adding "info" buttons and event handlers after vCard text function init() { if ( !checkIfAllowed() ) { return; } setupMessages(); var popupButton = $( '<button/>', { title: messages.buttonTooltip, text: messages.buttonText } ) .click( function( e ) { e.stopImmediatePropagation(); dialog( $( this ) ); }); popupButton = $( '<span/>', { 'class': 'listing-metadata-item listing-info-button voy-timeless-no-emoji noprint' }) .append( popupButton ); $( selectors.metadata ).append( popupButton ); } return { init: init }; } (); $( listingPopup.init ); } ( jQuery, mediaWiki ) ); //</nowiki>