/* Tagwren app: products -> label sheet preview -> print / PDF. Everything stays in this browser. * (c) 2026 CyberMax. All rights reserved. */ (function () { "use strict"; var T = window.Tagwren, PDF = window.TagwrenPDF, PRO = window.CMXPro; var KEY = "tagwren:v1", FREE_LIMIT = 30; var $ = function (id) { return document.getElementById(id); }; function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); } function isPro() { return !!(PRO && PRO.isPro()); } function toast(m) { if (PRO) PRO.toast(m); } var EXAMPLE = [ "Handle,Title,Option1 Name,Option1 Value,Variant SKU,Variant Price,Variant Compare At Price,Variant Barcode,Variant Inventory Qty,Image Src", "oat-honey-soap,Oat & Honey Soap Bar,Title,Default Title,SOAP-OAT,6.50,,2003100000114,14,", "beeswax-candle,Beeswax Pillar Candle,Size,Small,CND-S,14.00,18.00,2003100000282,6,", "beeswax-candle,,Size,Large,CND-L,22.00,28.00,2003100000350,4,", "beeswax-candle,,,,,,,,,https://example.com/candle-2.jpg", "linen-tea-towel,Linen Tea Towel,Color,Sage,TT-SAGE,12.00,,200310000046,9,", "linen-tea-towel,,Color,Oat,TT-OAT,12.00,,200310000053,7,", "speckled-mug,Speckled Stoneware Mug,Title,Default Title,MUG-SPK,24.00,,,5,", "wildflower-seeds,Wildflower Seed Packet,Title,Default Title,SEED-WF,4.25,,200310000060,30,", "cotton-tote,Organic Cotton Tote Bag,Title,Default Title,TOTE-ORG,18.00,22.00,200310000077,8," ].join("\n"); var CURRENCIES = ["USD", "CAD", "GBP", "EUR", "AUD", "NZD", "JPY", "INR", "ZAR", "CHF", "SEK", "NOK", "DKK", "MXN", "BRL", "SGD", "AED"]; var LOCALES = [["en-US", "1,234.50 (US)"], ["en-GB", "1,234.50 (UK)"], ["en-CA", "1,234.50 (Canada)"], ["en-AU", "1,234.50 (Australia)"], ["en-IN", "1,234.50 (India)"], ["de-DE", "1.234,50 (Germany)"], ["fr-FR", "1 234,50 (France)"], ["es-ES", "1234,50 (Spain)"], ["it-IT", "1.234,50 (Italy)"], ["nl-NL", "1.234,50 (Netherlands)"], ["sv-SE", "1 234,50 (Sweden)"], ["ja-JP", "1,234 (Japan)"]]; var FIELD_LABEL = { name: "Name", variant: "Variant / option", price: "Price", compare: "Compare-at (was) price", sku: "SKU", barcode: "Barcode (UPC/EAN/GTIN)", qty: "Inventory quantity", custom: "Custom text" }; var PRO_FIELDS = { compare: 1, qty: 1 }; function defaults() { return { items: [], table: null, mapping: null, sourceName: "", st: { sheet: "5160", custom: { unit: "in", paper: "letter", w: 2, h: 1, cols: 3, rows: 10, left: 0.25, top: 0.5, hpitch: 2.75, vpitch: 1 }, barcode: "auto", source: "barcode", fields: { name: true, variant: true, price: true, compare: false, save: false, sku: false, custom: false, hri: true }, customText: "", currency: "USD", locale: "en-US", start: 0, offX: 0, offY: 0, outlines: false, qtyMode: "fixed", qtyAll: 3 }, setups: [], mappings: {} }; } var state = defaults(); try { var saved = JSON.parse(localStorage.getItem(KEY) || "null"); if (saved && saved.st) { state = Object.assign(defaults(), saved); state.st = Object.assign(defaults().st, saved.st); state.st.fields = Object.assign(defaults().st.fields, saved.st.fields || {}); } } catch (e) { /* private mode or bad data: start fresh */ } var saveTimer = null; function flush() { clearTimeout(saveTimer); saveTimer = null; try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { try { var s = Object.assign({}, state, { table: null }); localStorage.setItem(KEY, JSON.stringify(s)); } catch (e2) { /* full or private mode */ } } } function save() { clearTimeout(saveTimer); saveTimer = setTimeout(flush, 150); } window.addEventListener("pagehide", function () { if (saveTimer) flush(); }); /** Settings with Pro-only options switched off for free users. */ function eff() { var st = JSON.parse(JSON.stringify(state.st)), pro = isPro(); var sheet = T.sheetById(st.sheet, st.custom); if (!pro && !sheet.free) st.sheet = "5160"; if (!pro) { st.fields.compare = false; st.fields.save = false; st.qtyMode = "fixed"; } return st; } /* ------------------------------------------------------------ controls setup */ function fillSelects() { var sel = $("sheet"), free = T.SHEETS.filter(function (s) { return s.free; }), pro = T.SHEETS.filter(function (s) { return !s.free; }); function opt(s, p) { return '"; } sel.innerHTML = '' + free.map(function (s) { return opt(s, false); }).join("") + '' + pro.map(function (s) { return opt(s, true); }).join("") + ''; $("currency").innerHTML = CURRENCIES.map(function (c) { return ""; }).join(""); $("locale").innerHTML = LOCALES.map(function (l) { return '"; }).join(""); } function syncControls() { var st = state.st; $("sheet").value = st.sheet; $("bctype").value = st.barcode; $("bcsrc").value = st.source; document.querySelectorAll(".fields input[data-f]").forEach(function (i) { i.checked = !!st.fields[i.getAttribute("data-f")]; }); $("custom-text").value = st.customText || ""; $("currency").value = st.currency; $("locale").value = st.locale; $("start").value = (st.start || 0) + 1; $("off-x").value = st.offX || 0; $("off-y").value = st.offY || 0; $("outlines").checked = !!st.outlines; $("qty-all").value = st.qtyAll; document.querySelectorAll("input[name=qtymode]").forEach(function (r) { r.checked = r.value === st.qtyMode; }); var c = st.custom; [["c-unit", "unit"], ["c-paper", "paper"], ["c-w", "w"], ["c-h", "h"], ["c-cols", "cols"], ["c-rows", "rows"], ["c-left", "left"], ["c-top", "top"], ["c-hp", "hpitch"], ["c-vp", "vpitch"]] .forEach(function (p) { $(p[0]).value = c[p[1]]; }); $("custom-box").hidden = st.sheet !== "custom"; } function proGate(reason, from) { if (isPro()) return true; if (PRO) PRO.upsell(reason, from); return false; } function bind() { $("btn-example").onclick = $("btn-example-top").onclick = function (e) { loadExample(); if (e.currentTarget.id === "btn-example-top") $("app").scrollIntoView({ behavior: "smooth", block: "start" }); }; function onFile(e) { var f = e.target.files && e.target.files[0]; if (!f) return; var r = new FileReader(); r.onload = function () { importText(String(r.result), f.name); $("app").scrollIntoView({ behavior: "smooth", block: "start" }); }; r.readAsText(f); e.target.value = ""; } $("file").onchange = onFile; $("file-top").onchange = onFile; $("btn-paste").onclick = function () { importText($("paste").value, "pasted rows"); }; $("btn-add").onclick = function () { state.items.push({ name: "New product", variant: "", price: null, compare: null, sku: "", barcode: "", custom: "", inv: null, qty: state.st.qtyAll || 1 }); if (!state.sourceName) state.sourceName = "typed in"; changedItems(); setTimeout(function () { var ins = document.querySelectorAll("#items tbody tr:last-child input[data-k=name]"); if (ins[0]) { ins[0].focus(); ins[0].select(); } }, 30); }; $("btn-clear").onclick = function () { if (state.items.length && !confirm("Remove all products from this browser?")) return; state.items = []; state.table = null; state.mapping = null; state.sourceName = ""; changedItems(); renderMapping(); }; $("btn-qty-all").onclick = function () { var q = Math.max(0, Math.floor(+$("qty-all").value || 0)); state.st.qtyAll = q; state.items.forEach(function (it) { it.qty = q; }); state.st.qtyMode = "fixed"; syncControls(); changedItems(); }; $("qty-all").onchange = function () { state.st.qtyAll = Math.max(0, Math.floor(+this.value || 0)); save(); }; document.querySelectorAll("input[name=qtymode]").forEach(function (r) { r.onchange = function () { if (r.value === "column") { if (!proGate("Label quantities from your inventory column are part of Tagwren Pro.", "qty")) { syncControls(); return; } if (!state.mapping || state.mapping.qty == null) { toast("Pick the inventory column under Column mapping first."); $("map-box").open = true; } state.st.qtyMode = "column"; state.items.forEach(function (it) { if (it.inv != null) it.qty = it.inv; }); } else state.st.qtyMode = "fixed"; changedItems(); }; }); $("sheet").onchange = function () { var v = this.value, s = T.sheetById(v, state.st.custom); if ((v === "custom" || !s.free) && !proGate("The " + (v === "custom" ? "custom size" : s.short) + " template is part of Tagwren Pro. Free: 5160, 5163 and the 2.25 × 1.25 in roll.", "sheet")) { this.value = state.st.sheet; return; } state.st.sheet = v; state.st.start = 0; syncControls(); changed(); }; ["c-unit", "c-paper", "c-w", "c-h", "c-cols", "c-rows", "c-left", "c-top", "c-hp", "c-vp"].forEach(function (id) { $(id).oninput = $(id).onchange = function () { var k = { "c-unit": "unit", "c-paper": "paper", "c-w": "w", "c-h": "h", "c-cols": "cols", "c-rows": "rows", "c-left": "left", "c-top": "top", "c-hp": "hpitch", "c-vp": "vpitch" }[id]; if (id === "c-unit" && this.value !== state.st.custom.unit) { var f = this.value === "mm" ? 25.4 : 1 / 25.4, c = state.st.custom; ["w", "h", "left", "top", "hpitch", "vpitch"].forEach(function (p) { c[p] = Math.round(c[p] * f * 100) / 100; }); c.unit = this.value; syncControls(); } else state.st.custom[k] = this.value; changed(); }; }); $("bctype").onchange = function () { state.st.barcode = this.value; changed(); renderTableStatus(); }; $("bcsrc").onchange = function () { state.st.source = this.value; if (this.value === "custom") { state.st.fields.custom = state.st.fields.custom; } changed(); }; document.querySelectorAll(".fields input[data-f]").forEach(function (i) { i.onchange = function () { var f = i.getAttribute("data-f"); if ((f === "compare" || f === "save") && i.checked && !proGate("Sale price tags (old price struck through) are part of Tagwren Pro.", "sale")) { i.checked = false; return; } state.st.fields[f] = i.checked; changed(); }; }); $("custom-text").oninput = function () { state.st.customText = this.value; changed(); }; $("currency").onchange = function () { state.st.currency = this.value; changed(); }; $("locale").onchange = function () { state.st.locale = this.value; changed(); }; $("start").oninput = function () { state.st.start = Math.max(0, (Math.floor(+this.value) || 1) - 1); changed(); }; $("off-x").oninput = function () { state.st.offX = +this.value || 0; changed(); }; $("off-y").oninput = function () { state.st.offY = +this.value || 0; changed(); }; $("outlines").onchange = function () { state.st.outlines = this.checked; changed(); }; $("btn-print").onclick = function () { output("print"); }; $("btn-pdf").onclick = function () { output("pdf"); }; $("btn-batch").onclick = batchCSV; $("btn-save-setup").onclick = saveSetup; $("btn-export").onclick = function () { var data = { app: "tagwren", v: 1, exported: new Date().toISOString(), items: state.items, st: state.st, setups: state.setups, mappings: state.mappings, sourceName: state.sourceName, mapping: state.mapping, table: state.table }; PRO.download("tagwren-backup-" + new Date().toISOString().slice(0, 10) + ".json", JSON.stringify(data, null, 1), "application/json"); }; $("file-backup").onchange = function (e) { var f = e.target.files && e.target.files[0]; if (!f) return; var r = new FileReader(); r.onload = function () { try { var d = JSON.parse(String(r.result)); if (!d || d.app !== "tagwren") throw new Error("not a Tagwren backup"); var base = defaults(); state = Object.assign(base, { items: d.items || [], st: Object.assign(base.st, d.st || {}), setups: d.setups || [], mappings: d.mappings || {}, sourceName: d.sourceName || "backup", mapping: d.mapping || null, table: d.table || null }); state.st.fields = Object.assign(defaults().st.fields, (d.st || {}).fields || {}); syncControls(); renderMapping(); changedItems(); renderSetups(); toast("Backup restored: " + state.items.length + " products."); } catch (err) { toast("That file is not a Tagwren backup."); } }; r.readAsText(f); e.target.value = ""; }; $("pages").onclick = function (e) { var slot = e.target.closest && e.target.closest("[data-slot]"); if (!slot) return; state.st.start = +slot.getAttribute("data-slot"); $("start").value = state.st.start + 1; changed(); toast("Printing starts at label " + (state.st.start + 1) + " of the first sheet."); }; $("pages").onkeydown = function (e) { if ((e.key === "Enter" || e.key === " ") && e.target.hasAttribute("data-slot")) { e.preventDefault(); e.target.click(); } }; window.addEventListener("beforeprint", function () { if (!printing) $("print-root").innerHTML = ''; }); window.addEventListener("afterprint", function () { printing = false; document.body.classList.remove("printing"); }); } /* ------------------------------------------------------------ import + mapping */ function loadExample() { importText(EXAMPLE, "example Shopify export", { example: true }); $("paste").value = EXAMPLE; } function importText(text, name, o) { o = o || {}; var table = T.parseCSV(text); if (table.length < 2) { toast("No product rows found. The first row should be the column names."); return; } var headers = table[0].map(function (h) { return String(h).trim(); }); table[0] = headers; var sig = headers.join("|").toLowerCase(); var remembered = isPro() && state.mappings[sig]; state.mapping = remembered ? Object.assign({}, remembered) : T.guessMapping(headers); state.table = table; state.sourceName = name; if (o.example) { state.st.qtyAll = 3; state.st.start = 0; } rebuildItems(); var mapped = state.mapping; var shopify = headers.indexOf("Handle") > -1 && headers.indexOf("Variant Price") > -1; var square = headers.some(function (h) { return /^item name$/i.test(h); }) && headers.some(function (h) { return /^variation name$/i.test(h); }); $("src-status").textContent = state.items.length + " products from " + name + (shopify ? " · Shopify export recognised" : square ? " · Square export recognised" : "") + (remembered ? " · your saved column mapping used" : "") + "."; if (mapped.name == null && mapped.price == null) toast("Check the column mapping: no name or price column was recognised."); renderMapping(); $("map-box").open = !o.example && !shopify; syncControls(); changedItems(); } function rebuildItems() { if (!state.table) return; var col = state.st.qtyMode === "column" && isPro(); state.items = T.normalizeRows(state.table, state.mapping, { qty: state.st.qtyAll, qtyFromColumn: col }); } function renderMapping() { var g = $("map-grid"); if (!state.table) { g.innerHTML = '

Import a CSV to map its columns.

'; $("map-sum").textContent = ""; return; } var headers = state.table[0], n = 0; g.innerHTML = T.FIELDS.map(function (f) { var v = state.mapping[f]; if (v != null) n++; return '
"; }).join("") + (isPro() ? '

Pro remembers this mapping for files with the same columns.

' : '

Pro remembers your mapping for the next file with the same columns.

'); $("map-sum").textContent = "(" + n + " of " + T.FIELDS.length + " mapped)"; g.querySelectorAll("select[data-map]").forEach(function (s) { s.onchange = function () { var f = s.getAttribute("data-map"); state.mapping[f] = s.value === "" ? null : +s.value; if (isPro()) state.mappings[state.table[0].join("|").toLowerCase()] = Object.assign({}, state.mapping); rebuildItems(); changedItems(); renderMapping(); }; }); } /* ------------------------------------------------------------ product table */ var TABLE_MAX = 300; function fmtNum(v) { return v == null ? "" : String(v); } function renderTable() { var tb = document.querySelector("#items tbody"), items = state.items; $("table-wrap").hidden = !items.length; tb.innerHTML = items.slice(0, TABLE_MAX).map(function (it, i) { return "" + '' + '' + '' + '' + '' + '' + '' + '' + ''; }).join(""); $("table-note").textContent = items.length > TABLE_MAX ? "Showing the first " + TABLE_MAX + " of " + items.length + " products here; all of them are printed." : ""; tb.oninput = function (e) { var t = e.target, i = +t.getAttribute("data-i"), k = t.getAttribute("data-k"); if (!k || !state.items[i]) return; var v = t.value; if (k === "qty") v = Math.max(0, Math.floor(+v || 0)); else if (k === "price" || k === "compare") v = T.parsePrice(v); state.items[i][k] = v; changed(); renderTableStatus(); }; tb.onclick = function (e) { var b = e.target.closest && e.target.closest("[data-del]"); if (!b) return; state.items.splice(+b.getAttribute("data-del"), 1); changedItems(); }; renderTableStatus(); } function renderTableStatus() { var st = eff(); document.querySelectorAll("#items td[data-st]").forEach(function (td) { var it = state.items[+td.getAttribute("data-st")]; if (!it) return; var c = T.labelContent(it, st, isPro()), b = c.bc; if (!b) { td.innerHTML = ""; return; } if (b.empty) td.innerHTML = 'none'; else if (!b.ok) td.innerHTML = '⚠ fix'; else td.innerHTML = '' + esc(T.TYPES[b.type]) + (b.note ? " *" : " ✓") + ""; }); } /* ------------------------------------------------------------ preview */ var raf = 0, printing = false, lastPlan = null; function changed() { save(); if (!raf) raf = requestAnimationFrame(function () { raf = 0; render(); }); } function changedItems() { renderTable(); changed(); if (!state.items.length) $("src-status").textContent = "No products yet. Load the example or import a CSV."; } /** Everything needed to draw a batch: settings, sheet, plan and one layout per product. */ function batch() { var st = eff(), pro = isPro(), sheet = T.sheetById(st.sheet, st.custom); var refs = T.expand(state.items), plan = T.planBatch(refs, sheet, st.start); var wPt = sheet.w * 72, hPt = sheet.h * 72, cache = {}; function lay(i) { if (!cache[i]) { var c = T.labelContent(state.items[i], st, pro), L = T.layoutLabel(c, wPt, hPt); cache[i] = { c: c, ops: L.ops, warnings: L.warnings }; } return cache[i]; } return { st: st, pro: pro, sheet: sheet, refs: refs, plan: plan, lay: lay, wPt: wPt, hPt: hPt }; } function render() { var B = batch(), st = B.st, sheet = B.sheet, plan = B.plan, pro = B.pro; var per = T.perPage(sheet), n = plan.count; var pw = sheet.page[0], ph = sheet.page[1], ox = (st.offX || 0) / 25.4, oy = (st.offY || 0) / 25.4; var maxPages = sheet.roll ? 12 : 4; var pages = plan.pages.length ? plan.pages : [{ labels: [] }]; var html = []; var svgCache = {}; function svgFor(ref) { if (!svgCache[ref]) svgCache[ref] = T.toSVG(B.lay(ref).ops, B.wPt, B.hPt, ' class="lbl" aria-hidden="true"'); return svgCache[ref]; } var markOK = !pro && !sheet.roll && T.bottomMargin(sheet) >= 0.15; pages.slice(0, maxPages).forEach(function (p, pi) { var bySlot = {}; p.labels.forEach(function (l) { bySlot[l.slot] = l; }); var cells = []; for (var k = 0; k < per; k++) { var col = k % sheet.cols, row = Math.floor(k / sheet.cols); var x = sheet.x0 + col * sheet.dx + ox, y = sheet.y0 + row * sheet.dy + oy; var style = "left:" + (x / pw * 100) + "%;top:" + (y / ph * 100) + "%;width:" + (sheet.w / pw * 100) + "%;height:" + (sheet.h / ph * 100) + "%;border-radius:" + (sheet.r / sheet.w * 100) + "%/" + (sheet.r / sheet.h * 100) + "%"; var l = bySlot[k], used = pi === 0 && k < plan.skipped; var bad = l && B.lay(l.ref).warnings.length; var attrs = !sheet.roll && pi === 0 ? ' data-slot="' + k + '" tabindex="0" role="button" aria-label="Start at label ' + (k + 1) + '"' : ""; cells.push('
" + (l ? svgFor(l.ref) : "") + "
"); } html.push('
' + cells.join("") + (markOK ? 'Tagwren by CyberMax' : "") + "
"); }); $("pages").innerHTML = html.join(""); $("pages").className = "pages" + (sheet.roll ? " rolls" : ""); var more = plan.pages.length - maxPages; $("pages-more").textContent = more > 0 ? "+ " + more + " more " + (sheet.roll ? "labels" : "sheets") + " (all included when you print)." : ""; var nPages = plan.pages.length; var left = sheet.roll || !n ? 0 : nPages * per - n - plan.skipped; $("prev-sum").textContent = n ? n + " label" + (n === 1 ? "" : "s") + " · " + (sheet.roll ? n + " roll label" + (n === 1 ? "" : "s") : nPages + " sheet" + (nPages === 1 ? "" : "s") + " of " + sheet.short + (left ? " · " + left + " spare spot" + (left === 1 ? "" : "s") + " on the last sheet" : "")) : "Add products to see your labels. Click a spot to start there on a used sheet."; // warnings (one line per product) var warns = [], seen = {}; B.refs.forEach(function (r) { if (seen[r]) return; seen[r] = 1; var L = B.lay(r), it = state.items[r]; L.warnings.forEach(function (w) { warns.push("
  • " + esc(it.name || "Row " + (r + 1)) + (it.variant ? " · " + esc(it.variant) : "") + ": " + esc(w) + "
  • "); }); if (L.c.bc && L.c.bc.empty && (st.barcode === "ean13" || st.barcode === "upca")) warns.push("
  • " + esc(it.name || "Row " + (r + 1)) + ": no barcode value, so no barcode is printed.
  • "); }); if (sheet.id === "custom" && !T.sheetFits(sheet)) warns.unshift("
  • Custom size: the labels run off the page. Check the margins and pitch.
  • "); $("warns").innerHTML = warns.slice(0, 12).join("") + (warns.length > 12 ? "
  • … and " + (warns.length - 12) + " more.
  • " : ""); // limit line var ll = $("limit-line"); if (pro) { ll.className = "limit pro"; ll.textContent = "Pro: unlimited labels and pages, no margin mark."; } else if (n > FREE_LIMIT) { ll.className = "limit over"; ll.innerHTML = "This batch has " + n + " labels. Free prints up to " + FREE_LIMIT + " per print or PDF: lower the quantities or go Pro for unlimited."; } else { ll.className = "limit"; ll.innerHTML = "Free: " + n + " of " + FREE_LIMIT + " labels per print or PDF used by this batch."; } $("mid-cta").hidden = pro || !n; $("btn-print").disabled = $("btn-pdf").disabled = !n; lastPlan = B; } /* ------------------------------------------------------------ output */ function allowed(B, what) { if (!B.plan.count) { toast("Add products (or load the example) first."); return false; } if (!B.pro && B.plan.count > FREE_LIMIT) { PRO.upsell("Free " + what + " up to " + FREE_LIMIT + " labels at a time; this batch has " + B.plan.count + ". Pro prints unlimited labels.", "limit"); return false; } return true; } function pageOps(B, p) { var sheet = B.sheet, st = B.st, ops = [], ox = (st.offX || 0) / 25.4 * 72, oy = (st.offY || 0) / 25.4 * 72; p.labels.forEach(function (l) { var bx = l.x * 72 + ox, by = l.y * 72 + oy; if (st.outlines) ops.push({ t: "frame", x: bx, y: by, w: B.wPt, h: B.hPt, lw: 0.4, c: "#999999" }); B.lay(l.ref).ops.forEach(function (o) { var c = Object.assign({}, o); if (o.t === "line") { c.x1 += bx; c.x2 += bx; c.y1 += by; c.y2 += by; } else { c.x += bx; c.y += by; } ops.push(c); }); }); if (!B.pro && !sheet.roll && T.bottomMargin(sheet) >= 0.15) { ops.push({ t: "text", x: sheet.page[0] * 72 / 2, y: (sheet.page[1] - T.bottomMargin(sheet) / 2) * 72 + 2, s: 6, b: false, a: "middle", str: "Tagwren by CyberMax", c: "#8A8A8A" }); } return ops; } function buildPDF(B) { B = B || batch(); var pages = B.plan.pages.map(function (p) { return { w: B.sheet.page[0] * 72, h: B.sheet.page[1] * 72, ops: pageOps(B, p) }; }); return PDF.build(pages, { title: "Labels · " + B.sheet.short }); } function output(kind) { var B = batch(); if (!allowed(B, kind === "pdf" ? "PDFs hold" : "prints take")) return; if (kind === "pdf") { var bytes = buildPDF(B); PRO.download("tagwren-labels-" + B.sheet.id + "-" + new Date().toISOString().slice(0, 10) + ".pdf", new Blob([bytes], { type: "application/pdf" }), "application/pdf"); toast("PDF ready: " + B.plan.count + " labels on " + B.plan.pages.length + (B.sheet.roll ? " pages" : " sheet" + (B.plan.pages.length === 1 ? "" : "s")) + ". Print it at 100% / Actual size."); } else { buildPrintRoot(B); printing = true; document.body.classList.add("printing"); setTimeout(function () { window.print(); }, 50); } lastPlan = B; lastPrinted = B; } function buildPrintRoot(B) { var sheet = B.sheet, st = B.st, pw = sheet.page[0], ph = sheet.page[1], ox = (st.offX || 0) / 25.4, oy = (st.offY || 0) / 25.4; $("page-style").textContent = "@page{size:" + pw + "in " + ph + "in;margin:0}"; var svgCache = {}, mark = !B.pro && !sheet.roll && T.bottomMargin(sheet) >= 0.15; $("print-root").innerHTML = B.plan.pages.map(function (p) { return '
    ' + p.labels.map(function (l) { if (!svgCache[l.ref]) svgCache[l.ref] = T.toSVG(B.lay(l.ref).ops, B.wPt, B.hPt, ' width="' + sheet.w + 'in" height="' + sheet.h + 'in"'); return '
    ' + svgCache[l.ref] + "
    "; }).join("") + (mark ? 'Tagwren by CyberMax' : "") + "
    "; }).join(""); } var lastPrinted = null; function batchCSV() { if (!proGate("A CSV of each printed batch is part of Tagwren Pro.", "batch-csv")) return; var B = lastPrinted || batch(); if (!B.plan.count) { toast("Nothing printed yet."); return; } var rows = [["label", "sheet", "row", "column", "name", "variant", "price", "compare_at", "sku", "barcode_type", "barcode", "template"]]; B.plan.pages.forEach(function (p, pi) { p.labels.forEach(function (l) { var it = state.items[l.ref] || {}, bc = B.lay(l.ref).c.bc; rows.push([l.n + 1, pi + 1, l.row + 1, l.col + 1, it.name, it.variant, it.price, it.compare, it.sku, bc && bc.ok ? T.TYPES[bc.type] : "", bc && bc.ok ? bc.code : "", B.sheet.short]); }); }); PRO.download("tagwren-batch-" + new Date().toISOString().slice(0, 10) + ".csv", T.toCSV(rows)); } /* ------------------------------------------------------------ saved setups (Pro) */ function saveSetup() { if (!proGate("Saved setups and column mappings are part of Tagwren Pro.", "setup")) return; var name = ($("setup-name").value || "").trim() || (T.sheetById(state.st.sheet, state.st.custom).short + " setup"); state.setups = state.setups.filter(function (s) { return s.name !== name; }); state.setups.unshift({ name: name, st: JSON.parse(JSON.stringify(state.st)), mapping: state.mapping ? Object.assign({}, state.mapping) : null, at: new Date().toISOString() }); state.setups = state.setups.slice(0, 30); $("setup-name").value = ""; save(); renderSetups(); toast("Saved setup “" + name + "”."); } function renderSetups() { var el = $("setups"); if (!state.setups.length) { el.innerHTML = ""; return; } el.innerHTML = '

    Saved setups:

    ' + state.setups.map(function (s, i) { return ''; }).join(""); el.onclick = function (e) { var l = e.target.closest("[data-load]"), r = e.target.closest("[data-rm]"); if (l) { if (!proGate("Saved setups are part of Tagwren Pro.", "setup")) return; var s = state.setups[+l.getAttribute("data-load")]; state.st = Object.assign(defaults().st, JSON.parse(JSON.stringify(s.st))); if (s.mapping && state.table) { state.mapping = Object.assign({}, s.mapping); rebuildItems(); renderMapping(); } syncControls(); changedItems(); toast("Loaded “" + s.name + "”."); } if (r) { state.setups.splice(+r.getAttribute("data-rm"), 1); save(); renderSetups(); } }; } /* ------------------------------------------------------------ PWA */ var deferred = null; window.addEventListener("beforeinstallprompt", function (e) { e.preventDefault(); deferred = e; $("btn-install").hidden = false; }); $("btn-install").onclick = function () { if (!deferred) return; deferred.prompt(); deferred.userChoice.then(function () { deferred = null; $("btn-install").hidden = true; }); }; window.addEventListener("appinstalled", function () { $("btn-install").hidden = true; }); if ("serviceWorker" in navigator && (location.protocol === "https:" || /^(localhost|127\.0\.0\.1)$/.test(location.hostname))) { window.addEventListener("load", function () { navigator.serviceWorker.register("sw.js").catch(function () { /* offline cache is optional */ }); }); } /* ------------------------------------------------------------ start */ fillSelects(); syncControls(); bind(); renderMapping(); renderTable(); renderSetups(); if (state.items.length) $("src-status").textContent = state.items.length + " products" + (state.sourceName ? " from " + state.sourceName : "") + " (saved in this browser)."; render(); if (PRO) PRO.onChange(function (pro) { var pill = $("pro-pill"); pill.textContent = pro ? "Pro on" : "Pro"; pill.classList.toggle("on", pro); document.documentElement.classList.toggle("is-pro", pro); if (state.table) renderMapping(); renderTableStatus(); render(); }); window.TagwrenApp = { state: function () { return state; }, render: render, buildPDF: function () { return buildPDF(); }, importText: importText, loadExample: loadExample, batch: batch }; })();