toolshed/frontend/src/components/LabelLayoutPreview.vue
2026-08-21 21:37:28 +02:00

151 lines
5.2 KiB
Vue

<template>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Label layout</h5>
</div>
<div class="card-body">
<div class="template-grid d-flex flex-wrap align-items-start">
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
:class="{'template-option-disabled': !isAvailable(t)}"
:title="isAvailable(t) ? '' : 'Not available - fill in the fields this layout needs above.'"
role="button" @click="isAvailable(t) && $emit('input', t.id)">
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
class="img-thumbnail template-thumb-canvas"
:class="{'border-primary': value === t.id}"></canvas>
<div class="small"
:class="{'fw-bold text-primary': value === t.id}">
{{ t.name }}
</div>
<div class="small text-muted">{{ t.description }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* This build pins Bootstrap 4 (no gap-* utilities, those are Bootstrap 5.1+), so the spacing
here is plain CSS gap rather than a Bootstrap gap-N class. */
.template-grid {
gap: 1rem;
}
.template-option {
width: 20rem;
}
.template-option-disabled {
opacity: .45;
cursor: not-allowed;
}
.template-thumb-canvas {
display: block;
width: 100%;
height: 10rem;
/* The canvas itself is drawn at whatever size fits its content (see redraw/drawFallbackLabel)
- object-fit scales that down to the thumbnail box the same way it would for an <img>,
no manual zoom math needed. */
object-fit: contain;
background: #fff;
}
</style>
<script>
import {drawFallbackLabel, preloadQrEncoder} from "@/label.js";
import {LABEL_TEMPLATES, templateIsAvailable, templateContent} from "@/label-layouts.js";
export default {
name: "LabelLayoutPreview",
props: {
// Named content fields the templates draw from (see label.js's buildLabelFields)
// - kept in sync by the parent, not owned here. A field missing from this object (rather
// than present-but-empty) means a template that needs it is unavailable right now.
fields: {
type: Object,
required: true
},
// The selected template id.
value: {
type: String,
required: true
},
// Print.vue's global qr/micro-qr/rmqr choice (see label.js's QR_CODE_TYPES) - passed
// straight through to drawFallbackLabel so these thumbnails match whatever symbology the
// main preview is actually using.
codeType: {
type: String,
default: "qr"
}
},
model: {
prop: "value",
event: "input"
},
emits: ["input"],
computed: {
labelTemplates() {
return LABEL_TEMPLATES;
}
},
watch: {
fields() {
this.redraw();
},
codeType() {
this.redraw();
}
},
methods: {
setTemplateCanvasRef(id, el) {
if (el) {
this.templateCanvases[id] = el;
} else {
delete this.templateCanvases[id];
}
},
isAvailable(t) {
return templateIsAvailable(t, this.fields);
},
/* Live per-template thumbnails. Always uses the content-fit fallback renderer (rather
than the tape-fed one), regardless of whether a real printer is connected - these are
illustrative previews sized by CSS object-fit, not the accurate to-be-printed canvas
the main preview is. */
redraw() {
for (const t of LABEL_TEMPLATES) {
const canvas = this.templateCanvases[t.id];
if (!canvas) {
continue;
}
const content = templateContent(t, this.fields);
if (!this.isAvailable(t) || !content) {
canvas.width = 1;
canvas.height = 1;
continue;
}
try {
drawFallbackLabel(canvas, content, "along", this.codeType);
} catch (e) {
// A thumbnail that can't render at this content length just stays blank
// rather than surfacing an error for every keystroke.
}
}
}
},
created() {
// Keyed by template id, populated by setTemplateCanvasRef() - a plain :ref="t.id" string
// inside v-for would still get Vue's refInFor array-collecting behavior even though
// each iteration uses a different name, turning this.$refs[t.id] into a one-element
// array rather than the canvas itself. A function ref sidesteps that entirely.
this.templateCanvases = {};
},
async mounted() {
// See Print.vue's mounted() - every thumbnail here has a "qrcode" leaf, so there's
// nothing worth drawing before this resolves.
await preloadQrEncoder();
this.redraw();
}
}
</script>