warranty: use the shared LocationMapTooltip for the machine preview
The machine map preview was a bespoke popover. LocationMapTooltip already existed and is what the machine detail page uses, so the warranty tables now show the same thing. That is what was asked for - the preview zooms, as it does on the machine page - and it comes with behaviour the bespoke one did not have: scroll-wheel zoom, and staying open while the pointer is on the tooltip itself so it can actually be read and panned. It also solves the clipping properly. The previous commit reached for position: fixed with a hand-rolled flip because .table-container's overflow-x clipped an absolute child; LocationMapTooltip teleports to body, which avoids the clipping context altogether rather than escaping it. All of that positioning code, and the blueprint rendering, is deleted - 64 lines from 140. An unplaced machine keeps its chip and now says why in the native tooltip rather than showing an empty panel: 17 of the 142 linked machines have no map position, and those rows still need their number.
This commit is contained in:
@@ -1,115 +1,53 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="machine"
|
||||
class="mmc"
|
||||
ref="hostEl"
|
||||
@mouseenter="show"
|
||||
@mouseleave="open = false"
|
||||
<!-- Placed machine: reuse the same hover map the machine detail page uses, so
|
||||
the zoom, scroll-wheel behaviour and teleport-out-of-clipping-containers
|
||||
are identical everywhere instead of reimplemented here. -->
|
||||
<LocationMapTooltip
|
||||
v-if="machine && hasPosition"
|
||||
:left="machine.mapx"
|
||||
:top="machine.mapy"
|
||||
:machineName="machine.machinenumber || machine.name || ''"
|
||||
>
|
||||
<!-- Deliberately not a link. /machines/:id is keyed by machineid, not
|
||||
assetid, and resolving one to the other here would make the warranty
|
||||
plugin import the machines plugin (ADR-014). The number plus the map
|
||||
is what someone needs to walk to the bay. -->
|
||||
<span class="mmc-chip">{{ machine.machinenumber || machine.name || '-' }}</span>
|
||||
|
||||
<!-- Hover preview: where to walk to. Rendered only while hovered so a
|
||||
table of 200 rows does not build 200 blueprints. -->
|
||||
<span v-if="open && hasPosition" ref="popEl" class="mmc-pop" :style="popStyle">
|
||||
<span class="mmc-pop-title">{{ machine.name || machine.machinenumber }}</span>
|
||||
<span v-if="machine.locationname" class="mmc-pop-loc">{{ machine.locationname }}</span>
|
||||
<span class="mmc-map">
|
||||
<img :src="blueprint" alt="" class="mmc-map-img" />
|
||||
<span class="mmc-marker" :style="markerStyle"></span>
|
||||
</span>
|
||||
<span class="mmc-chip" :title="hoverTitle">
|
||||
{{ machine.machinenumber || machine.name || '-' }}
|
||||
</span>
|
||||
</LocationMapTooltip>
|
||||
|
||||
<!-- A machine with a number but no map position still deserves the chip;
|
||||
say why there is no picture rather than showing an empty box. -->
|
||||
<span v-else-if="open" ref="popEl" class="mmc-pop mmc-pop-plain" :style="popStyle">
|
||||
<span class="mmc-pop-title">{{ machine.name || machine.machinenumber }}</span>
|
||||
<span class="mmc-pop-loc">{{ machine.locationname || 'Not placed on the floor map' }}</span>
|
||||
</span>
|
||||
<!-- Known machine, never placed on the floor map. Still worth its number:
|
||||
against real data 17 of 142 linked machines have no position, and
|
||||
dropping them would silently lose the column for those rows. -->
|
||||
<span v-else-if="machine" class="mmc-chip" :title="hoverTitle">
|
||||
{{ machine.machinenumber || machine.name || '-' }}
|
||||
</span>
|
||||
|
||||
<span v-else class="muted">-</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { state as mapState, loadMapConfig, blueprintUrlFor } from '@/composables/mapConfig'
|
||||
import { computed } from 'vue'
|
||||
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
|
||||
|
||||
const props = defineProps({
|
||||
machine: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
const hostEl = ref(null)
|
||||
const popEl = ref(null)
|
||||
// Starts hidden: the first tick renders before show() can measure, and an
|
||||
// unpositioned fixed element would flash in the corner of the screen.
|
||||
const popStyle = ref({ visibility: 'hidden' })
|
||||
|
||||
onMounted(() => { loadMapConfig() })
|
||||
|
||||
// position: fixed, placed from the chip's rect. The table sits in a
|
||||
// .table-container with overflow-x: auto, and ANY non-visible overflow clips
|
||||
// absolutely-positioned children - which cut the last row's preview off behind
|
||||
// the pagination. Fixed escapes that clip entirely.
|
||||
async function show() {
|
||||
popStyle.value = { visibility: 'hidden' }
|
||||
open.value = true
|
||||
await nextTick()
|
||||
const host = hostEl.value
|
||||
if (!host) return
|
||||
const rect = host.getBoundingClientRect()
|
||||
const gap = 6
|
||||
// Measure the real popover rather than guessing: its height depends on the
|
||||
// blueprint's aspect ratio, which differs per site.
|
||||
const popHeight = popEl.value ? popEl.value.offsetHeight : 260
|
||||
const popWidth = popEl.value ? popEl.value.offsetWidth : 288
|
||||
|
||||
// Flip above when there is not room below, so the bottom row stays readable.
|
||||
const roomBelow = window.innerHeight - rect.bottom
|
||||
const top = (roomBelow < popHeight + gap && rect.top > popHeight + gap)
|
||||
? rect.top - popHeight - gap
|
||||
: rect.bottom + gap
|
||||
|
||||
// Keep it on screen horizontally too - the column sits well right on a wide
|
||||
// table and the preview is nearly 300px.
|
||||
const left = Math.max(8, Math.min(rect.left, window.innerWidth - popWidth - 8))
|
||||
|
||||
popStyle.value = {
|
||||
position: 'fixed', top: `${top}px`, left: `${left}px`, visibility: 'visible'
|
||||
}
|
||||
}
|
||||
|
||||
const hasPosition = computed(() =>
|
||||
props.machine && props.machine.mapx != null && props.machine.mapy != null)
|
||||
|
||||
// Follow the viewer's theme the same way the full map does, so the preview is
|
||||
// not a white rectangle on a dark page.
|
||||
const blueprint = computed(() => {
|
||||
const dark = document.documentElement.dataset.theme === 'dark'
|
||||
|| (!document.documentElement.dataset.theme
|
||||
&& window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
return blueprintUrlFor(dark ? 'dark' : 'light')
|
||||
})
|
||||
|
||||
// mapx/mapy are pixel coordinates in the blueprint's own space, so they have to
|
||||
// be expressed as a percentage of its configured dimensions - the preview is a
|
||||
// few hundred pixels wide, not the full plan.
|
||||
const markerStyle = computed(() => {
|
||||
if (!hasPosition.value) return {}
|
||||
const width = mapState.width || 3300
|
||||
const height = mapState.height || 2550
|
||||
return {
|
||||
left: `${(props.machine.mapx / width) * 100}%`,
|
||||
top: `${(props.machine.mapy / height) * 100}%`
|
||||
}
|
||||
// The name and location are not in the chip (it has to stay narrow in a table
|
||||
// column), so they go in the native tooltip. An unplaced machine says so, since
|
||||
// otherwise hovering it does nothing and looks broken.
|
||||
const hoverTitle = computed(() => {
|
||||
if (!props.machine) return ''
|
||||
const parts = []
|
||||
if (props.machine.name) parts.push(props.machine.name)
|
||||
if (props.machine.locationname) parts.push(props.machine.locationname)
|
||||
if (!hasPosition.value) parts.push('not placed on the floor map')
|
||||
return parts.join(' - ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mmc { position: relative; display: inline-block; }
|
||||
.mmc-chip {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.5rem;
|
||||
@@ -119,47 +57,8 @@ const markerStyle = computed(() => {
|
||||
color: var(--text);
|
||||
font-size: 0.8rem;
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mmc:hover .mmc-chip { border-color: var(--primary); }
|
||||
|
||||
.mmc-pop {
|
||||
/* Placement is computed in show(); these are only the fallback for the tick
|
||||
before it runs. */
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
/* .table-container sets white-space: nowrap for the table; the preview text
|
||||
must not inherit it or the location line runs off the panel. */
|
||||
white-space: normal;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
width: 18rem;
|
||||
background: var(--bg-card-solid);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mmc-pop-plain { width: 14rem; }
|
||||
.mmc-pop-title { font-weight: 600; font-size: 0.85rem; color: var(--text); }
|
||||
.mmc-pop-loc { font-size: 0.78rem; color: var(--text-light); }
|
||||
|
||||
.mmc-map { position: relative; display: block; line-height: 0; }
|
||||
.mmc-map-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.mmc-marker {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px 0 0 -6px;
|
||||
border-radius: 50%;
|
||||
background: var(--danger);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 2px var(--danger);
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.mmc-chip:hover { border-color: var(--primary); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user