Mudanças entre as edições de "Widget:MapViewer.js"
Ir para navegação
Ir para pesquisar
| Linha 1: | Linha 1: | ||
(function() { | (function() { | ||
// Verificar se já foi carregado | // Verificar se já foi carregado | ||
Edição das 01h41min de 8 de abril de 2026
(function() {
// Verificar se já foi carregado
if (window.MWMapViewerLoaded) return;
window.MWMapViewerLoaded = true;
// Estilos do widget
const styles = `
<style>
.mw-map-viewer {
position: relative;
background: #0f172a;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.mw-map-viewer .map-viewport {
width: 100%;
height: 100%;
overflow: auto;
cursor: grab;
background: #0f172a;
}
.mw-map-viewer .map-viewport:active {
cursor: grabbing;
}
.mw-map-viewer .map-layers {
position: relative;
min-width: 100%;
min-height: 100%;
}
.mw-map-viewer .map-layer {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
}
.mw-map-viewer .map-image {
display: block;
transform-origin: 0 0;
pointer-events: none;
border-radius: 4px;
}
.mw-map-viewer .map-marker {
position: absolute;
cursor: pointer;
z-index: 100;
transform: translate(-50%, -50%);
transition: transform 0.1s ease;
}
.mw-map-viewer .map-marker:hover {
transform: translate(-50%, -50%) scale(1.15);
z-index: 102;
}
.mw-map-viewer .marker-icon {
display: flex;
align-items: center;
justify-content: center;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
}
.mw-map-viewer .marker-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-8px);
background: #1e293b;
color: #e2e8f0;
padding: 4px 10px;
border-radius: 8px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
border: 1px solid #334155;
}
.mw-map-viewer .map-marker:hover .marker-tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-12px);
}
.mw-map-viewer .marker-badge {
position: absolute;
top: -8px;
right: -8px;
background: #ef4444;
color: white;
font-size: 9px;
font-weight: bold;
min-width: 16px;
height: 16px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
font-family: monospace;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.mw-map-viewer .marker-badge.hidden {
display: none;
}
.mw-map-viewer .viewer-toolbar {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
display: flex;
justify-content: space-between;
z-index: 101;
gap: 10px;
flex-wrap: wrap;
}
.mw-map-viewer .viewer-controls {
display: flex;
gap: 5px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
padding: 5px 10px;
border-radius: 40px;
}
.mw-map-viewer .viewer-controls button {
background: #334155;
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.mw-map-viewer .viewer-controls button:hover {
background: #6366f1;
transform: scale(1.05);
}
.mw-map-viewer .floor-info {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
padding: 5px 15px;
border-radius: 40px;
color: white;
font-size: 12px;
}
.mw-map-viewer .zoom-level {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
padding: 5px 12px;
border-radius: 40px;
color: #a5b4fc;
font-size: 12px;
font-family: monospace;
}
.mw-map-viewer .coordinates-panel {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
padding: 4px 10px;
border-radius: 20px;
color: #a5b4fc;
font-size: 10px;
font-family: monospace;
z-index: 100;
pointer-events: none;
}
.mw-map-viewer .nav-controls {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
gap: 8px;
z-index: 101;
}
.mw-map-viewer .nav-controls button {
background: rgba(0,0,0,0.7);
backdrop-filter: blur(8px);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.mw-map-viewer .nav-controls button:hover {
background: #6366f1;
transform: scale(1.05);
}
.mw-map-viewer-popup {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
justify-content: center;
align-items: center;
}
.mw-map-viewer-popup .popup-content {
background: #1e293b;
border-radius: 16px;
max-width: 350px;
width: 90%;
padding: 20px;
position: relative;
border: 1px solid #334155;
animation: popupSlide 0.3s ease;
}
@keyframes popupSlide {
from { transform: translateY(-30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.mw-map-viewer-popup .popup-close {
position: absolute;
right: 15px;
top: 10px;
font-size: 24px;
cursor: pointer;
color: #94a3b8;
}
.mw-map-viewer-popup .popup-close:hover {
color: #ef4444;
}
.mw-map-viewer-popup h3 {
color: #a5b4fc;
margin-bottom: 12px;
}
.mw-map-viewer-popup p {
color: #cbd5e1;
line-height: 1.5;
font-size: 14px;
}
.mw-map-viewer-popup img {
max-width: 100%;
border-radius: 12px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.mw-map-viewer .viewer-controls button {
width: 28px;
height: 28px;
font-size: 14px;
}
.mw-map-viewer .floor-info {
font-size: 10px;
padding: 4px 12px;
}
.mw-map-viewer .zoom-level {
font-size: 10px;
padding: 4px 10px;
}
}
</style>
`;
class MWMapViewer {
constructor(container, config, options = {}) {
this.container = container;
this.config = this.normalizeConfig(config);
this.width = options.width || '100%';
this.height = options.height || '500px';
this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
this.currentFloor = this.config.mapConfig?.initialFloor || 0;
this.minZoom = this.config.mapConfig?.minZoom || 0.5;
this.maxZoom = this.config.mapConfig?.maxZoom || 3;
this.zoomStep = this.config.mapConfig?.zoomStep || 0.1;
this.layers = [];
this.markers = [];
this.isPanning = false;
this.panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
this.init();
}
normalizeConfig(config) {
if (!config.mapConfig) {
config.mapConfig = { initialFloor: 0, defaultZoom: 1, minZoom: 0.5, maxZoom: 3, zoomStep: 0.1 };
}
if (!config.layers) config.layers = [];
config.layers.forEach(layer => {
if (!layer.alignment) layer.alignment = { offsetX: 0, offsetY: 0, scale: 1 };
if (layer.opacity === undefined) layer.opacity = 100;
if (!layer.markers) layer.markers = [];
layer.markers.forEach(marker => {
if (!marker.actionData) marker.actionData = {};
if (marker.action === 'gotoFloor' && marker.actionData.floorId === undefined) marker.actionData.floorId = 0;
if (marker.action === 'link' && !marker.actionData.url) marker.actionData.url = '#';
if (marker.action === 'popup' && !marker.actionData.text) marker.actionData.text = 'Sem informações';
});
});
return config;
}
init() {
this.createDOM();
this.renderLayers();
this.setupEvents();
this.updateMarkersPosition();
this.initPopup();
}
createDOM() {
this.container.innerHTML = ;
this.container.className = 'mw-map-viewer';
this.container.style.width = this.width;
this.container.style.height = this.height;
this.container.style.position = 'relative';
this.container.style.overflow = 'hidden';
this.container.innerHTML = `
📍 0, 0
`;
this.viewport = this.container.querySelector('.map-viewport');
this.layersContainer = this.container.querySelector('.map-layers');
this.floorNameSpan = this.container.querySelector('.mw-floor-name');
this.zoomLevelSpan = this.container.querySelector('.mw-zoom-level');
this.coordsSpan = this.container.querySelector('.mw-coords');
// Bind events
this.container.querySelector('.mw-zoom-in')?.addEventListener('click', () => this.zoomIn());
this.container.querySelector('.mw-zoom-out')?.addEventListener('click', () => this.zoomOut());
this.container.querySelector('.mw-reset')?.addEventListener('click', () => this.resetView());
this.container.querySelector('.mw-prev-floor')?.addEventListener('click', () => this.prevFloor());
this.container.querySelector('.mw-next-floor')?.addEventListener('click', () => this.nextFloor());
this.setupInteractions();
this.setupKeyboard();
}
setupInteractions() {
// Pan
this.viewport.addEventListener('mousedown', (e) => {
if (e.target.closest('.map-marker')) return;
this.isPanning = true;
this.panStart = {
x: e.clientX,
y: e.clientY,
scrollLeft: this.viewport.scrollLeft,
scrollTop: this.viewport.scrollTop
};
this.viewport.style.cursor = 'grabbing';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!this.isPanning) return;
this.viewport.scrollLeft = this.panStart.scrollLeft - (e.clientX - this.panStart.x);
this.viewport.scrollTop = this.panStart.scrollTop - (e.clientY - this.panStart.y);
});
window.addEventListener('mouseup', () => {
this.isPanning = false;
this.viewport.style.cursor = 'grab';
});
// Zoom com scroll
this.viewport.addEventListener('wheel', (e) => {
if (e.ctrlKey) {
e.preventDefault();
e.deltaY < 0 ? this.zoomIn() : this.zoomOut();
}
});
// Coordenadas
this.viewport.addEventListener('mousemove', (e) => {
const rect = this.viewport.getBoundingClientRect();
const x = (e.clientX - rect.left + this.viewport.scrollLeft) / this.currentZoom;
const y = (e.clientY - rect.top + this.viewport.scrollTop) / this.currentZoom;
if (this.coordsSpan) {
this.coordsSpan.textContent = `📍 ${Math.round(x)}, ${Math.round(y)} | ${Math.round(this.currentZoom * 100)}%`;
}
});
}
setupKeyboard() {
const handler = (e) => {
if (e.target.tagName === 'INPUT') return;
switch(e.key) {
case '+': case '=': e.preventDefault(); this.zoomIn(); break;
case '-': case '_': e.preventDefault(); this.zoomOut(); break;
case 'r': case 'R': e.preventDefault(); this.resetView(); break;
case 'ArrowUp': case 'PageUp': e.preventDefault(); this.prevFloor(); break;
case 'ArrowDown': case 'PageDown': e.preventDefault(); this.nextFloor(); break;
}
};
document.addEventListener('keydown', handler);
}
renderLayers() {
if (!this.layersContainer) return;
this.layersContainer.innerHTML = ;
this.layers = [];
this.markers = [];
if (!this.config.layers || this.config.layers.length === 0) {
this.layersContainer.innerHTML = '
Nenhuma camada carregada
';
return;
}
if (!this.config.layers.find(l => l.id === this.currentFloor)) {
this.currentFloor = this.config.layers[0]?.id || 0;
}
const currentFloorData = this.config.layers.find(l => l.id === this.currentFloor);
if (currentFloorData && this.floorNameSpan) {
this.floorNameSpan.textContent = currentFloorData.name;
}
this.config.layers.forEach((layer) => {
const $layer = document.createElement('div');
$layer.className = 'map-layer';
$layer.setAttribute('data-floor', layer.id);
$layer.style.display = layer.id === this.currentFloor ? 'block' : 'none';
$layer.style.transform = `translate(${layer.alignment?.offsetX || 0}px, ${layer.alignment?.offsetY || 0}px)`;
$layer.style.opacity = (layer.opacity || 100) / 100;
const $img = document.createElement('img');
$img.className = 'map-image';
$img.style.transform = `scale(${this.currentZoom})`;
$img.style.transformOrigin = '0 0';
if (layer.imagePath) {
$img.src = layer.imagePath;
} else {
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#334155';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = '20px Arial';
ctx.fillText(layer.name || `Andar ${layer.id + 1}`, 50, 100);
$img.src = canvas.toDataURL();
}
$img.onload = () => {
this.layersContainer.style.width = `${$img.width}px`;
this.layersContainer.style.height = `${$img.height}px`;
};
$layer.appendChild($img);
const $markersContainer = document.createElement('div');
$markersContainer.style.position = 'absolute';
$markersContainer.style.top = '0';
$markersContainer.style.left = '0';
$markersContainer.style.width = '100%';
$markersContainer.style.height = '100%';
$markersContainer.style.pointerEvents = 'none';
if (layer.markers && layer.markers.length > 0) {
layer.markers.forEach(marker => {
const $marker = this.createMarker(marker, layer.id);
$markersContainer.appendChild($marker);
this.markers.push({ element: $marker, data: marker });
});
}
$layer.appendChild($markersContainer);
this.layersContainer.appendChild($layer);
this.layers.push($layer);
});
}
createMarker(marker, floorId) {
const $marker = document.createElement('div');
$marker.className = 'map-marker';
$marker.setAttribute('data-marker-id', marker.id);
$marker.setAttribute('data-floor', floorId);
$marker.setAttribute('data-x', marker.x);
$marker.setAttribute('data-y', marker.y);
$marker.style.position = 'absolute';
$marker.style.left = `${marker.x * this.currentZoom}px`;
$marker.style.top = `${marker.y * this.currentZoom}px`;
$marker.style.zIndex = '100';
$marker.style.transform = 'translate(-50%, -50%)';
const hasAction = marker.action && marker.action !== 'none';
$marker.style.cursor = hasAction ? 'pointer' : 'default';
const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
let iconHtml = marker.iconBase64 ?
`<img src="${marker.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">` :
`📍`;
let actionText = ;
switch(marker.action) {
case 'popup': actionText = '📋 Informações'; break;
case 'nextFloor': actionText = '⬆️ Próximo andar'; break;
case 'prevFloor': actionText = '⬇️ Andar anterior'; break;
case 'gotoFloor': actionText = '🎯 Ir para andar'; break;
case 'link': actionText = '🔗 Link externo'; break;
default: actionText = '📍 Clique';
}
$marker.innerHTML = `
${marker.name || 'Marcador'}
${actionText}
${marker.hasBadge ? (marker.badgeNumber || marker.number || ) : }
`;
// Hover effects
$marker.addEventListener('mouseenter', () => {
$marker.style.transform = 'translate(-50%, -50%) scale(1.15)';
});
$marker.addEventListener('mouseleave', () => {
$marker.style.transform = 'translate(-50%, -50%) scale(1)';
});
// Clique action
if (hasAction) {
$marker.addEventListener('click', (e) => {
e.stopPropagation();
this.executeAction(marker);
});
}
return $marker;
}
executeAction(marker) {
switch(marker.action) {
case 'popup':
this.showPopup(marker);
break;
case 'nextFloor':
this.nextFloor();
break;
case 'prevFloor':
this.prevFloor();
break;
case 'gotoFloor':
if (marker.actionData?.floorId !== undefined) {
this.goToFloor(marker.actionData.floorId);
}
break;
case 'link':
if (marker.actionData?.url && marker.actionData.url !== '#') {
window.open(marker.actionData.url, marker.actionData.target || '_blank');
}
break;
}
}
initPopup() {
if (document.querySelector('.mw-map-viewer-popup')) return;
const popupHtml = `
`;
document.body.insertAdjacentHTML('beforeend', popupHtml);
this.popup = document.querySelector('.mw-map-viewer-popup');
this.popupBody = this.popup?.querySelector('.popup-body');
this.popup?.querySelector('.popup-close')?.addEventListener('click', () => this.closePopup());
this.popup?.addEventListener('click', (e) => {
if (e.target === this.popup) this.closePopup();
});
}
showPopup(marker) {
if (!this.popup) this.initPopup();
const iconHtml = marker.iconBase64 ?
`<img src="${marker.iconBase64}" style="width:32px; height:32px; vertical-align:middle; margin-right:8px;">` :
`📍`;
this.popupBody.innerHTML = `
${marker.actionData?.image ? `<img src="${marker.actionData.image}" alt="${marker.name}">` : }
${iconHtml}
${marker.name}
${marker.actionData?.text || 'Sem informações'}
`;
this.popup.style.display = 'flex';
}
closePopup() {
if (this.popup) this.popup.style.display = 'none';
}
updateMarkersPosition() {
this.markers.forEach(({ element, data }) => {
if (parseInt(element.getAttribute('data-floor')) === this.currentFloor) {
element.style.left = `${data.x * this.currentZoom}px`;
element.style.top = `${data.y * this.currentZoom}px`;
const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
const iconDiv = element.querySelector('.marker-icon');
if (iconDiv) {
if (data.iconBase64) {
iconDiv.innerHTML = `<img src="${data.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">`;
} else {
iconDiv.innerHTML = `📍`;
}
}
}
});
}
zoomIn() {
if (this.currentZoom < this.maxZoom) {
const oldZoom = this.currentZoom;
this.currentZoom = Math.min(this.currentZoom + this.zoomStep, this.maxZoom);
this.applyZoom(oldZoom);
}
}
zoomOut() {
if (this.currentZoom > this.minZoom) {
const oldZoom = this.currentZoom;
this.currentZoom = Math.max(this.currentZoom - this.zoomStep, this.minZoom);
this.applyZoom(oldZoom);
}
}
applyZoom(oldZoom) {
this.container.querySelectorAll('.map-image').forEach(img => {
img.style.transform = `scale(${this.currentZoom})`;
});
const rect = this.viewport.getBoundingClientRect();
const cx = rect.width / 2, cy = rect.height / 2;
const sx = (this.viewport.scrollLeft + cx) / oldZoom;
const sy = (this.viewport.scrollTop + cy) / oldZoom;
this.viewport.scrollLeft = sx * this.currentZoom - cx;
this.viewport.scrollTop = sy * this.currentZoom - cy;
this.updateMarkersPosition();
if (this.zoomLevelSpan) {
this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
}
}
resetView() {
this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
this.container.querySelectorAll('.map-image').forEach(img => {
img.style.transform = `scale(${this.currentZoom})`;
});
this.viewport.scrollLeft = 0;
this.viewport.scrollTop = 0;
this.updateMarkersPosition();
if (this.zoomLevelSpan) {
this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
}
}
nextFloor() {
const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
if (idx < this.config.layers.length - 1) {
this.goToFloor(this.config.layers[idx + 1].id);
}
}
prevFloor() {
const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
if (idx > 0) {
this.goToFloor(this.config.layers[idx - 1].id);
}
}
goToFloor(floorId) {
this.currentFloor = floorId;
this.layers.forEach(layer => {
const layerFloor = parseInt(layer.getAttribute('data-floor'));
layer.style.display = layerFloor === floorId ? 'block' : 'none';
});
const floorData = this.config.layers.find(l => l.id === floorId);
if (floorData && this.floorNameSpan) {
this.floorNameSpan.textContent = floorData.name;
}
setTimeout(() => this.updateMarkersPosition(), 50);
}
// Método para atualizar configuração dinamicamente
updateConfig(config) {
this.config = this.normalizeConfig(config);
this.renderLayers();
this.updateMarkersPosition();
}
}
// Registrar widget globalmente
window.MWMapViewer = MWMapViewer;
window.MWMapViewerStyles = styles;
})();