User:Aoppo/Globstory.js
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.
// GlobStory Wikipedia Integration for common.js with improved location and year detection
mw.loader.using(['mediawiki.util', 'mediawiki.api'], function() {
mw.hook('wikipage.content').add(function($content) {
'use strict';
// Check if the script has already been executed
if (window.globStoryInitialized) {
return;
}
window.globStoryInitialized = true;
// Main function to initialize GlobStory
var initGlobStory = function() {
// Create and add CSS
const style = document.createElement('style');
style.textContent = `
/* Main container styles */
.globstory-container {
position: fixed;
right: 0;
top: 0;
height: 100vh;
width: 30%;
background: white;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
transition: width 0.3s ease, top 0.3s ease, bottom 0.3s ease;
}
/* One-time popup styles */
.globstory-popup {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
z-index: 1300;
max-width: 300px;
text-align: center;
}
.globstory-popup.active {
display: block;
}
.globstory-popup-close {
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
font-weight: bold;
}
/* Collapsed state */
.globstory-container.collapsed {
width: 40px;
}
/* Adjust Wikipedia content when map is shown */
.globstory-active #content {
margin-right: 30%;
transition: margin-right 0.3s ease;
width: auto !important;
}
.globstory-active.collapsed #content {
margin-right: 40px;
width: auto !important;
}
/* Ensure content is responsive when tool is hidden */
.globstory-active #content,
.globstory-active.collapsed #content {
max-width: calc(100% - 40px);
box-sizing: border-box;
}
/* Adjust content padding when map is collapsed */
.globstory-active.collapsed #mw-content-text {
padding-right: 40px;
}
/* Ensure the toggle button doesn't overlap content */
.globstory-container.collapsed .globstory-toggle {
z-index: 1001;
}
/* Toggle button */
.globstory-toggle {
position: absolute;
left: -40px;
top: 50%;
background: #007BFF;
color: white;
width: 40px;
height: 80px;
border: none;
border-radius: 5px 0 0 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
}
/* Toggle button on hover */
.globstory-toggle:hover {
background: #0056b3;
}
/* Map container */
.globstory-map {
flex: 1;
border: none;
width: 100%;
}
/* Controls bar */
.globstory-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
background-color: #f1f1f1;
border-bottom: 1px solid #ddd;
}
/* Control buttons */
.globstory-btn {
padding: 6px 10px;
margin: 0 3px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.globstory-btn:hover {
background-color: #0056b3;
}
/* Year input */
.globstory-year-input {
padding: 6px;
width: 70px;
text-align: center;
border: 1px solid #ccc;
border-radius: 4px;
margin: 0 5px;
}
/* Help and Settings buttons */
.globstory-help,
.globstory-settings {
background: #FFA500;
color: white;
width: 30px;
height: 30px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-left: 5px;
}
.globstory-button-controls {
display: flex;
align-items: center;
}
.globstory-year-controls {
display: flex;
align-items: center;
flex-grow: 1;
justify-content: center;
}
/* Help and Settings modals */
.globstory-help-modal,
.globstory-settings-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.2);
z-index: 1200;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.globstory-help-modal.active,
.globstory-settings-modal.active {
display: block;
}
/* Settings styles */
.globstory-settings {
margin-left: auto;
}
.globstory-settings-modal label {
display: block;
margin-top: 10px;
}
/* Modal overlay */
.globstory-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1100;
}
.globstory-overlay.active {
display: block;
}
/* Close button for modal */
.globstory-close {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: #333;
}
/* Highlighted text */
.globstory-country,
.globstory-year {
cursor: pointer;
}
/* Styles for different year types */
.globstory-year.decade {
background-color: rgba(144, 238, 144, 0.3);
}
.globstory-year.bc-era {
background-color: rgba(255, 222, 173, 0.3);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.globstory-container {
width: 100%;
height: 50vh;
right: 0;
transition: top 0.3s ease, bottom 0.3s ease;
}
.globstory-container.collapsed {
height: 40px;
width: 100%;
}
.globstory-toggle {
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 40px;
}
.globstory-active #content {
margin-right: 0;
transition: margin-top 0.3s ease, margin-bottom 0.3s ease;
}
.globstory-active.collapsed #content {
margin-bottom: 40px;
margin-right: 0;
}
.globstory-container.top {
top: 0;
bottom: auto;
}
.globstory-container.bottom {
top: auto;
bottom: 0;
}
.globstory-container.top .globstory-toggle {
top: auto;
bottom: -40px;
border-radius: 0 0 5px 5px;
}
.globstory-container.bottom .globstory-toggle {
top: -40px;
bottom: auto;
border-radius: 5px 5px 0 0;
}
.globstory-active .globstory-container.top ~ #content {
margin-top: 50vh;
margin-bottom: 0;
}
.globstory-active .globstory-container.bottom ~ #content {
margin-top: 0;
margin-bottom: 50vh;
}
}
`;
document.head.appendChild(style);
// Create main container
const container = document.createElement('div');
container.className = 'globstory-container';
container.id = 'globstory-container';
// Create toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'globstory-toggle';
toggleBtn.innerHTML = '«';
toggleBtn.title = 'Toggle GlobStory Map';
container.appendChild(toggleBtn);
// Create controls
const controls = document.createElement('div');
controls.className = 'globstory-controls';
// Year navigation buttons
const yearMinus100 = createButton('<<', 'Jump 100 years back');
const yearMinus10 = createButton('<', 'Jump 10 years back');
const yearInput = document.createElement('input');
yearInput.type = 'number';
yearInput.className = 'globstory-year-input';
yearInput.value = new Date().getFullYear();
const yearPlus10 = createButton('>', 'Jump 10 years forward');
const yearPlus100 = createButton('>>', 'Jump 100 years forward');
// Help button
const helpBtn = createButton('?', 'GlobStory Help');
helpBtn.className = 'globstory-help';
// Settings button
const settingsBtn = createButton('⚙', 'GlobStory Settings');
settingsBtn.className = 'globstory-settings';
// Create control groups
const yearControls = document.createElement('div');
yearControls.className = 'globstory-year-controls';
yearControls.appendChild(yearMinus100);
yearControls.appendChild(yearMinus10);
yearControls.appendChild(yearInput);
yearControls.appendChild(yearPlus10);
yearControls.appendChild(yearPlus100);
const buttonControls = document.createElement('div');
buttonControls.className = 'globstory-button-controls';
buttonControls.appendChild(helpBtn);
buttonControls.appendChild(settingsBtn);
// Append control groups
controls.appendChild(yearControls);
controls.appendChild(buttonControls);
container.appendChild(controls);
// Create settings modal
const settingsModal = document.createElement('div');
settingsModal.className = 'globstory-settings-modal';
settingsModal.innerHTML = `
<span class="globstory-close">×</span>
<h2>GlobStory Settings</h2>
<label>
<input type="checkbox" id="globstory-auto-open" ${localStorage.getItem('globstory-auto-open') === 'true' ? 'checked' : ''}>
Open map by default
</label>
<label>
<input type="checkbox" id="globstory-map-position-top" ${localStorage.getItem('globstory-map-position-top') === 'true' ? 'checked' : ''}>
Show map at the top on mobile devices
</label>
`;
document.body.appendChild(settingsModal);
// Create one-time popup
const popup = document.createElement('div');
popup.className = 'globstory-popup';
popup.innerHTML = `
<span class="globstory-popup-close">×</span>
<p>On mobile devices, long-press highlighted words to interact with the GlobStory map.</p>
`;
document.body.appendChild(popup);
// Create map iframe
const mapIframe = document.createElement('iframe');
mapIframe.className = 'globstory-map';
mapIframe.src = `https://embed.openhistoricalmap.org/#map=3/43.021/7.471&layers=O&date=${new Date().getFullYear()}-12-08`;
mapIframe.frameBorder = '0';
mapIframe.scrolling = 'no';
container.appendChild(mapIframe);
// Variables to track current map state
let currentMapZoom = "3"; // Default zoom level
let currentMapLat = "43.021"; // Default latitude
let currentMapLon = "7.471"; // Default longitude
// Create help modal
const helpModal = document.createElement('div');
helpModal.className = 'globstory-help-modal';
helpModal.innerHTML = `
<span class="globstory-close">×</span>
<h2>Welcome to GlobStory Wikipedia Integration</h2>
<p>This extension enhances your Wikipedia experience by adding an interactive historical map that dynamically responds to content.</p>
<h3>How to use:</h3>
<ul>
<li><strong>Highlighted Elements:</strong>
<ul>
<li>Country and place names are highlighted in <span style="background-color: #ffff9980;">yellow</span></li>
<li>Years and dates are highlighted in <span style="background-color: #90ee9080;">green</span></li>
<li>On desktop: Hover over highlighted text for 1 second to see the corresponding location or time period on the map</li>
<li>On mobile: Long-press (0.5 seconds) on highlighted text to interact with the map</li>
</ul>
</li>
<li><strong>Map Navigation:</strong>
<ul>
<li>Use the year controls to navigate through different time periods</li>
<li>The map will update to show historical borders for the selected year</li>
</ul>
</li>
<li><strong>Toggle the Map:</strong>
<ul>
<li>Click the toggle button to hide or show the map</li>
<li>On mobile, you can choose to display the map at the top or bottom of the screen in the settings</li>
</ul>
</li>
<li><strong>Settings:</strong>
<ul>
<li>Click the settings icon (⚙) to access additional options</li>
<li>You can set the map to open by default and choose its position on mobile devices</li>
</ul>
</li>
</ul>
<p>To learn more about GlobStory, visit <a href="https://globstory.it" target="_blank">globstory.it</a></p>
`;
// Create overlay
const overlay = document.createElement('div');
overlay.className = 'globstory-overlay';
// Add elements to the page
document.body.appendChild(container);
document.body.appendChild(helpModal);
document.body.appendChild(overlay);
// Check if the map should be open by default
if (localStorage.getItem('globstory-auto-open') !== 'true') {
container.classList.add('collapsed');
document.body.classList.add('collapsed');
}
// Add the 'globstory-active' class to the body
document.body.classList.add('globstory-active');
// Initialize hover functionality for country and year detection
initializeHighlighting();
// Event listeners
toggleBtn.addEventListener('click', toggleMap);
yearMinus100.addEventListener('click', () => adjustYear(-100));
yearMinus10.addEventListener('click', () => adjustYear(-10));
yearPlus10.addEventListener('click', () => adjustYear(10));
yearPlus100.addEventListener('click', () => adjustYear(100));
yearInput.addEventListener('change', () => {
const year = parseInt(yearInput.value, 10);
updateMapYear(year);
});
helpBtn.addEventListener('click', () => {
helpModal.classList.add('active');
overlay.classList.add('active');
});
settingsBtn.addEventListener('click', () => {
settingsModal.classList.add('active');
overlay.classList.add('active');
});
document.querySelectorAll('.globstory-close, .globstory-popup-close').forEach(closeBtn => {
closeBtn.addEventListener('click', closeModals);
});
overlay.addEventListener('click', closeModals);
document.getElementById('globstory-auto-open').addEventListener('change', (e) => {
localStorage.setItem('globstory-auto-open', e.target.checked);
});
document.getElementById('globstory-map-position-top').addEventListener('change', (e) => {
localStorage.setItem('globstory-map-position-top', e.target.checked);
updateMapPosition();
});
// Show one-time popup if it hasn't been shown before
if (localStorage.getItem('globstory-popup-shown') !== 'true') {
popup.classList.add('active');
localStorage.setItem('globstory-popup-shown', 'true');
}
// Update map position based on user preference
updateMapPosition();
// Helper functions
function createButton(text, title) {
const button = document.createElement('button');
button.className = 'globstory-btn';
button.textContent = text;
button.title = title;
return button;
}
function toggleMap() {
container.classList.toggle('collapsed');
document.body.classList.toggle('collapsed');
if (container.classList.contains('collapsed')) {
toggleBtn.innerHTML = '»';
} else {
toggleBtn.innerHTML = '«';
}
}
function adjustYear(delta) {
const currentYear = parseInt(yearInput.value, 10);
const newYear = currentYear + delta;
yearInput.value = newYear;
updateMapYear(newYear);
}
// Helper function to extract current state from iframe
function updateCurrentMapStateFromIframe() {
try {
const src = mapIframe.src;
if (src.includes('#')) {
const hashParams = src.split('#')[1].split('&');
hashParams.forEach(param => {
if (param.startsWith("map=")) {
const mapValues = param.split("=")[1].split("/");
if (mapValues.length >= 3) {
currentMapZoom = mapValues[0] || currentMapZoom;
currentMapLat = mapValues[1] || currentMapLat;
currentMapLon = mapValues[2] || currentMapLon;
}
}
});
}
} catch (e) {
console.error("Error extracting map state:", e);
}
}
// Helper function to update the map with current state
function updateMapWithCurrentState(year) {
const dateParam = year ? `&date=${year}-12-08` : '';
mapIframe.src = `https://embed.openhistoricalmap.org/#map=${currentMapZoom}/${currentMapLat}/${currentMapLon}&layers=O${dateParam}`;
}
function updateMapYear(year) {
// Try to get the current state from the iframe src
updateCurrentMapStateFromIframe();
// Use current state variables
updateMapWithCurrentState(year);
}
function closeModals() {
helpModal.classList.remove('active');
settingsModal.classList.remove('active');
overlay.classList.remove('active');
popup.classList.remove('active');
}
function updateMapPosition() {
const isTop = localStorage.getItem('globstory-map-position-top') === 'true';
container.classList.toggle('top', isTop);
container.classList.toggle('bottom', !isTop);
}
// IMPROVED HIGHLIGHTING FUNCTION
function initializeHighlighting() {
// Select all paragraph elements in the content
const contentElement = document.getElementById('mw-content-text');
if (!contentElement) return;
const paragraphs = contentElement.querySelectorAll('p');
paragraphs.forEach(paragraph => {
// Process text nodes to identify places and years
processTextNodes(paragraph);
});
// Add hover and long-press functionality for highlighted elements
let hoverTimeout;
let longPressTimeout;
contentElement.addEventListener('mouseover', handleMouseOver);
contentElement.addEventListener('mouseout', handleMouseOut);
contentElement.addEventListener('touchstart', handleTouchStart);
contentElement.addEventListener('touchend', handleTouchEnd);
function handleMouseOver(e) {
const target = e.target;
if (target.classList.contains('globstory-country') || target.classList.contains('globstory-year')) {
clearTimeout(hoverTimeout);
hoverTimeout = setTimeout(() => {
handleInteraction(target);
}, 1000); // Trigger after 1 second hover
}
}
function handleMouseOut() {
clearTimeout(hoverTimeout);
}
function handleTouchStart(e) {
const target = e.target;
if (target.classList.contains('globstory-country') || target.classList.contains('globstory-year')) {
clearTimeout(longPressTimeout);
longPressTimeout = setTimeout(() => {
handleInteraction(target);
}, 500); // Trigger after 0.5 second long-press
}
}
function handleTouchEnd() {
clearTimeout(longPressTimeout);
}
function handleInteraction(target) {
if (target.classList.contains('globstory-country')) {
// Handle country interaction
const countryName = target.dataset.place || target.dataset.country;
updateMapLocation(countryName);
} else if (target.classList.contains('globstory-year')) {
// Handle year interaction
const year = parseInt(target.dataset.year, 10);
// Set the year input value to the numerical year (including negative for BC)
yearInput.value = year;
updateMapYear(year);
}
}
}
// IMPROVED TEXT PROCESSING FUNCTION
function processTextNodes(container) {
// Create a TreeWalker to find all text nodes in the container
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
// Skip script and style contents
if (node.parentNode.nodeName === 'SCRIPT' ||
node.parentNode.nodeName === 'STYLE') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
const textNodes = [];
let currentNode;
while (currentNode = walker.nextNode()) {
textNodes.push(currentNode);
}
// Process each text node found
textNodes.forEach(node => {
const text = node.textContent;
if (!text.trim()) return; // Skip empty nodes
// Collect all matches for places and years
const allMatches = [];
// 1. Detect places (including multi-word places)
// Matches: Single capitalized words + Multi-word places with specific connectors
const placeNameRegex = /\b(([A-Z][a-z]{2,})(\s+([A-Z][a-z]+|de|del|di|da|von|van|am|auf|la|le|el|al|der|den|das|du|des|do|of|on|in|by|sur|sous|aux)){0,3})\b/g;
let match;
while ((match = placeNameRegex.exec(text)) !== null) {
allMatches.push({
text: match[0],
index: match.index,
type: 'place'
});
}
// 2. Detect decades (e.g., "1990s", "20s")
const decadeRegex = /\b([0-9]{2,4})s\b/g;
while ((match = decadeRegex.exec(text)) !== null) {
const yearValue = parseInt(match[1], 10);
if (yearValue >= 0 && yearValue <= 2100) {
allMatches.push({
text: match[0],
index: match.index,
type: 'year',
year: yearValue,
isDecade: true,
isBCEra: false
});
}
}
// 3. Detect BC/BCE years
const bcRegex = /\b([0-9]{1,5})\s*(BC|BCE|a\.C\.|AEC)\b/gi;
while ((match = bcRegex.exec(text)) !== null) {
const yearValue = parseInt(match[1], 10);
if (yearValue >= 0 && yearValue <= 10000) {
allMatches.push({
text: match[0],
index: match.index,
type: 'year',
year: -yearValue, // Store as negative for BC years
isDecade: false,
isBCEra: true,
originalText: match[0]
});
}
}
// 4. Detect standard years (e.g., "1990", "2023")
const standardYearRegex = /\b(-?[0-9]{1,5})\b/g;
while ((match = standardYearRegex.exec(text)) !== null) {
const yearValue = parseInt(match[1], 10);
// Reasonable year range for historical context
if (yearValue >= -10000 && yearValue <= 2100) {
// Check if this match overlaps with any existing match to avoid duplicates
const overlaps = allMatches.some(existing =>
(match.index >= existing.index && match.index < existing.index + existing.text.length) ||
(existing.index >= match.index && existing.index < match.index + match[0].length)
);
if (!overlaps) {
allMatches.push({
text: match[0],
index: match.index,
type: 'year',
year: yearValue,
isDecade: false,
isBCEra: yearValue < 0
});
}
}
}
// If no matches were found, return without changes
if (allMatches.length === 0) return;
// Sort matches by index to maintain order of text
allMatches.sort((a, b) => a.index - b.index);
// Create a document fragment to replace the text node
const fragment = document.createDocumentFragment();
let lastIndex = 0;
// Process each match and create the appropriate spans
allMatches.forEach(match => {
// Add text before this match
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
}
// Create appropriate span based on match type
const span = document.createElement('span');
span.textContent = match.text;
if (match.type === 'place') {
span.className = 'globstory-country';
span.dataset.place = match.text;
span.title = `Click or hover to locate ${match.text} on the map`;
} else if (match.type === 'year') {
span.className = 'globstory-year';
span.dataset.year = match.year;
span.title = `Click or hover to set map to year ${match.year}`;
// Add special classes for decade and BC/BCE years
if (match.isDecade) {
span.classList.add('decade');
span.title = `Click or hover to set map to the ${match.text}`;
}
if (match.isBCEra) {
span.classList.add('bc-era');
// Store original text for display purposes
if (match.originalText) {
span.dataset.originalText = match.originalText;
}
}
}
fragment.appendChild(span);
lastIndex = match.index + match.text.length;
});
// Add any remaining text
if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
}
// Replace the original text node with our fragment
if (node.parentNode) {
node.parentNode.replaceChild(fragment, node);
}
});
}
function updateMapLocation(placeName) {
// Use MediaWiki API to fetch coordinates
new mw.Api().get({
action: 'query',
prop: 'coordinates',
titles: placeName,
format: 'json'
}).done(function(data) {
var pages = data.query.pages;
var pageId = Object.keys(pages)[0];
var page = pages[pageId];
if (page.coordinates) {
// Try to get current zoom from iframe first
updateCurrentMapStateFromIframe();
// Now update with new coordinates
currentMapLat = page.coordinates[0].lat;
currentMapLon = page.coordinates[0].lon;
// If we're explicitly navigating to a place, use a specific zoom level
currentMapZoom = "6";
// Get current year from input
var currentYear = yearInput.value;
// Update the map iframe with new state
updateMapWithCurrentState(currentYear);
} else {
// If coordinates not found in Wiki API, try Nominatim as a fallback
searchNominatim(placeName);
}
}).fail(function() {
console.error("Error fetching location coordinates for: " + placeName);
// Try Nominatim as a fallback
searchNominatim(placeName);
});
}
// Fallback to Nominatim for locations not found in MediaWiki API
function searchNominatim(placeName) {
fetch(
`https://nominatim.openstreetmap.org/search?` +
`q=${encodeURIComponent(placeName)}&` +
`format=json&limit=1&addressdetails=1`
)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
if (data && data.length > 0) {
const location = data[0];
// Update current map state
updateCurrentMapStateFromIframe();
currentMapLat = location.lat;
currentMapLon = location.lon;
currentMapZoom = "6";
// Get current year from input
var currentYear = yearInput.value;
// Update the map iframe with new state
updateMapWithCurrentState(currentYear);
} else {
console.error("Location not found in Nominatim:", placeName);
}
})
.catch(error => {
console.error("Error searching Nominatim:", error);
});
}
// Listen for messages from the iframe
window.addEventListener('message', function(event) {
if (event.origin !== 'https://embed.openhistoricalmap.org') return;
if (event.data.type === 'mapLoaded') {
console.log('Map loaded successfully');
}
}, false);
}; // End of initGlobStory function
// Run the initialization
initGlobStory();
});
});