/* 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 = ''; $("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 = '
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 "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 }; })();