@freereportjs/core 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -76,14 +76,21 @@ interface ElementBase {
76
76
  canGrow?: boolean;
77
77
  /**
78
78
  * Follow the band's height when it grows. Default: true for elements that touch the bottom
79
- * of the band, so cells of a table row stay the same height.
79
+ * of the band, so cells of a table row stay the same height. 'row': take the height of the
80
+ * tallest element on the same row (same y and h), so a label cell grows with its content cell.
80
81
  */
81
- stretch?: boolean;
82
+ stretch?: boolean | 'row';
82
83
  /**
83
84
  * Move down when an element above it grows (canGrow), by as much as that element grew, so
84
- * long text pushes the rest of the band down instead of overlapping it. Default true.
85
+ * long text pushes the rest of the band down instead of overlapping it. Only elements that
86
+ * overlap it horizontally move. Default true.
85
87
  */
86
88
  shift?: boolean;
89
+ /**
90
+ * When `visible` is false, give its space back: the elements below it move up into its place
91
+ * and the band shrinks. Default false (the space stays empty).
92
+ */
93
+ collapse?: boolean;
87
94
  /** Excel export: column name in "data" mode; ignored elsewhere. */
88
95
  name?: string;
89
96
  }
@@ -511,6 +518,8 @@ interface ElementAnchor {
511
518
  h: number;
512
519
  /** Its `visible` expression was false: the box it would have used. */
513
520
  hidden?: boolean;
521
+ /** It ends below the page's printable area (a band that grew past the page). */
522
+ offPage?: boolean;
514
523
  }
515
524
  interface LayoutResult {
516
525
  pages: ReportPage[];
@@ -921,10 +930,12 @@ declare function pagesToXlsx(layout: LayoutResult, def: ReportDefinition, opts?:
921
930
  declare function dataToXlsx(def: ReportDefinition, data: ReportData, opts?: {
922
931
  params?: Record<string, unknown>;
923
932
  sheetName?: string;
933
+ locale?: string;
924
934
  }): Uint8Array;
925
935
  /** CSV of the same columns as `dataToXlsx` (UTF-8 with BOM so Excel opens Korean correctly). */
926
936
  declare function dataToCsv(def: ReportDefinition, data: ReportData, opts?: {
927
937
  params?: Record<string, unknown>;
938
+ locale?: string;
928
939
  }): string;
929
940
 
930
941
  /** Export laid-out pages as an HWPX document (한/글 2018+, 한컴오피스 뷰어). */
@@ -1185,7 +1196,7 @@ declare function verifyLicense(key: string, host: string, opts?: {
1185
1196
  declare function setLicenseKey(key: string): void;
1186
1197
 
1187
1198
  /** FreeReport version (kept in sync with releases.json by scripts/release.mjs). */
1188
- declare const version = "0.10.0";
1199
+ declare const version = "0.10.2";
1189
1200
 
1190
1201
  type Lang = 'ko' | 'en';
1191
1202
 
package/dist/index.js CHANGED
@@ -368,7 +368,7 @@ function formatValue(v, format, locale = "ko") {
368
368
  }
369
369
 
370
370
  // src/version.ts
371
- var version = "0.10.0";
371
+ var version = "0.10.2";
372
372
  var releaseDate = "2026-10-19";
373
373
 
374
374
  // src/license.ts
@@ -1038,8 +1038,8 @@ function textMark(text, cx, cy, size, opts) {
1038
1038
  source: { band: "watermark" }
1039
1039
  };
1040
1040
  }
1041
- function watermarkItems(wm, page, scope, measure) {
1042
- const text = wm.text ? interpolate(wm.text, scope) : "";
1041
+ function watermarkItems(wm, page, scope, measure, locale) {
1042
+ const text = wm.text ? interpolate(wm.text, scope, locale) : "";
1043
1043
  const opacity = wm.opacity ?? 0.12;
1044
1044
  const angle = wm.angle ?? -35;
1045
1045
  const color = wm.color ?? "#808080";
@@ -1191,7 +1191,7 @@ var shift = (d, dx, dy) => {
1191
1191
  }
1192
1192
  return out;
1193
1193
  };
1194
- function embossItems(em, page, scope, measure) {
1194
+ function embossItems(em, page, scope, measure, locale) {
1195
1195
  const w = em.w ?? 32, h = em.h ?? (em.shape === "rect" ? 20 : w);
1196
1196
  const x = em.x ?? page.width - page.margins[1] - w - 4;
1197
1197
  const y = em.y ?? page.height - page.margins[2] - h - 12;
@@ -1210,8 +1210,8 @@ function embossItems(em, page, scope, measure) {
1210
1210
  } else if (shape === "rect") {
1211
1211
  shapes.push(rectRing(x, y, w, h, Math.max(0.6, Math.min(w, h) * 0.05)));
1212
1212
  }
1213
- const text = em.text ? interpolate(em.text, scope) : "";
1214
- const center = em.centerText ? interpolate(em.centerText, scope) : "";
1213
+ const text = em.text ? interpolate(em.text, scope, locale) : "";
1214
+ const center = em.centerText ? interpolate(em.centerText, scope, locale) : "";
1215
1215
  if (shape === "circle" && text) {
1216
1216
  const r = Math.min(w, h) / 2;
1217
1217
  const size = em.fontSize ?? Math.max(5, r * 0.62);
@@ -1250,11 +1250,11 @@ function decoratePages(pages, marks, geometry, scopeFor, measure) {
1250
1250
  const back = [], front = [];
1251
1251
  for (const wm of wms) {
1252
1252
  if (!onPage(wm.pages, i, pages.length) || !wm.text && !wm.image) continue;
1253
- (wm.layer === "front" ? front : back).push(...watermarkItems(wm, page, scope, measure));
1253
+ (wm.layer === "front" ? front : back).push(...watermarkItems(wm, page, scope, measure, marks.locale));
1254
1254
  }
1255
1255
  for (const em of ems) {
1256
1256
  if (!onPage(em.pages, i, pages.length)) continue;
1257
- front.push(...embossItems(em, { ...page, margins: geometry.margins }, scope, measure));
1257
+ front.push(...embossItems(em, { ...page, margins: geometry.margins }, scope, measure, marks.locale));
1258
1258
  }
1259
1259
  page.items = [...back, ...page.items, ...front];
1260
1260
  });
@@ -1353,7 +1353,7 @@ function textSvg(t, clipId) {
1353
1353
  }
1354
1354
  const overflow = t.source?.band !== "watermark" && t.lines.some((l) => l.x < t.x - 0.05 || l.x + l.width > t.x + t.w + 0.05 || l.y - sizeMm * 0.8 < t.y - 0.05 || l.y + sizeMm * 0.2 > t.y + t.h + 0.05);
1355
1355
  const transform = t.rotate ? ` transform="rotate(${n(t.rotate)} ${n(t.x + t.w / 2)} ${n(t.y + t.h / 2)})"` : "";
1356
- const hooks = sourceAttrs(t) + (t.overflow ? ' data-fr-overflow="1"' : "");
1356
+ const hooks = sourceAttrs(t) + (t.overflow && t.source?.band !== "watermark" ? ' data-fr-overflow="1"' : "");
1357
1357
  if (!overflow) return { defs: "", body: `<g ${attrs}${hooks}${transform}${opacityAttr(t.opacity)}>${body}</g>` };
1358
1358
  const id = clipId();
1359
1359
  return {
@@ -1408,7 +1408,7 @@ function pageToSvg(page, opts = {}) {
1408
1408
  const r = itemSvg(it, clipId);
1409
1409
  defs += r.defs;
1410
1410
  const hooks = it.kind === "text" ? "" : sourceAttrs(it);
1411
- body += hooks && r.body ? `<g${hooks}>${r.body}</g>` : r.body;
1411
+ body += !hooks || !r.body ? r.body : r.body.indexOf("<", 1) < 0 ? r.body.replace(/^<(\w+)/, (_, tag) => `<${tag}${hooks}`) : `<g${hooks}>${r.body}</g>`;
1412
1412
  }
1413
1413
  const size = opts.scale ? `width="${n(page.width * opts.scale)}" height="${n(page.height * opts.scale)}"` : `width="${n(page.width)}mm" height="${n(page.height)}mm"`;
1414
1414
  return `<svg xmlns="http://www.w3.org/2000/svg" ${size} viewBox="0 0 ${n(page.width)} ${n(page.height)}"${opts.className ? ` class="${opts.className}"` : ""} text-rendering="geometricPrecision">${defs ? `<defs>${defs}</defs>` : ""}<rect width="100%" height="100%" fill="#fff"/>${body}</svg>`;
@@ -2548,7 +2548,10 @@ function renderChart(el, scope, box, ctx) {
2548
2548
  const labelStyle = { ...style, padding: [0, 0, 0, 0], letterSpacing: 0, border: {} };
2549
2549
  const lh = fontSize * PT * 1.3;
2550
2550
  const items = [];
2551
- const label = (text, x2, y2, w2, align, bold = false, color = "#444444") => items.push({ ...textItem(x2, y2, w2, lh, [text], { ...font, bold }, { ...labelStyle, align, valign: "middle", color }, ctx.measure), source: { band: ctx.kind, elementId: el.id } });
2551
+ const label = (text, x2, y2, w2, align, bold = false, color = "#444444") => (
2552
+ // chart labels are allowed to run past their slot: not an overflow of the element's text
2553
+ items.push({ ...textItem(x2, y2, w2, lh, [text], { ...font, bold }, { ...labelStyle, align, valign: "middle", color }, ctx.measure), overflow: void 0, source: { band: ctx.kind, elementId: el.id } })
2554
+ );
2552
2555
  let { x, y, w, h } = box;
2553
2556
  if (el.title) {
2554
2557
  label(el.title, x, y, w, "center", true, style.color);
@@ -2952,7 +2955,7 @@ function textItem(x, y, w, h, lines, font, st, measure) {
2952
2955
  else if (st.align === "right") lx = x + w - pr - width;
2953
2956
  return { text, x: lx, y: top + i * lineH + (lineH - font.size * PT) / 2 + font.size * PT * ASCENT, width };
2954
2957
  });
2955
- const overflow = total > innerH + 0.05 || out.some((l) => l.width > w - pl - pr + 0.05);
2958
+ const overflow = top < y - 0.05 || top + total > y + h + 0.05 || out.some((l) => l.x < x - 0.05 || l.x + l.width > x + w + 0.05);
2956
2959
  return {
2957
2960
  kind: "text",
2958
2961
  x,
@@ -3036,28 +3039,52 @@ function barcodeItems(el, x, y, w, h, scope, ctx) {
3036
3039
  return [textItem(x, y, w, h, ["(barcode?)"], { family: "sans-serif", size: 7, bold: false, italic: false }, { ...resolveStyle(ctx.def, el), color: "#cc0000" }, ctx.measure)];
3037
3040
  }
3038
3041
  }
3039
- function shifts(prepared) {
3040
- let grew = false;
3041
- for (const p of prepared) if (p.needed > p.el.h + 0.01) {
3042
- grew = true;
3042
+ var topOf = (el) => Math.min(el.y, el.y + el.h);
3043
+ var bottomOf = (el) => Math.max(el.y, el.y + el.h);
3044
+ var leftOf = (el) => Math.min(el.x, el.x + el.w);
3045
+ var rightOf = (el) => Math.max(el.x, el.x + el.w);
3046
+ function shifts(slots) {
3047
+ let changed = false;
3048
+ for (const s of slots) if (s.collapsed || !s.hidden && s.needed > s.el.h + 0.01) {
3049
+ changed = true;
3043
3050
  break;
3044
3051
  }
3045
- if (!grew) return null;
3046
- const out = /* @__PURE__ */ new Map();
3047
- const top = (p) => Math.min(p.el.y, p.el.y + p.el.h);
3048
- const bottom = (p) => Math.max(p.el.y, p.el.y + p.el.h);
3049
- const order = [...prepared].sort((a, b) => top(a) - top(b));
3052
+ if (!changed) return null;
3053
+ const off = /* @__PURE__ */ new Map();
3054
+ const order = [...slots].sort((a, b) => topOf(a.el) - topOf(b.el));
3055
+ const growth = (q) => q.hidden ? 0 : Math.max(0, q.needed - q.el.h);
3050
3056
  for (const p of order) {
3051
3057
  if (p.el.shift === false) continue;
3052
- let by = 0;
3058
+ const pt = topOf(p.el), pl = leftOf(p.el), pr = rightOf(p.el);
3059
+ let nearest3 = null;
3060
+ let floor = -Infinity, pushed = -Infinity;
3061
+ const above = [];
3053
3062
  for (const q of order) {
3054
- if (top(q) >= top(p)) break;
3055
- if (bottom(q) > top(p) + 0.01) continue;
3056
- by = Math.max(by, (out.get(q) ?? 0) + Math.max(0, q.needed - q.el.h));
3063
+ if (topOf(q.el) >= pt) break;
3064
+ if (bottomOf(q.el) > pt + 0.01 || leftOf(q.el) >= pr - 0.01 || pl >= rightOf(q.el) - 0.01) continue;
3065
+ above.push(q);
3066
+ if (!nearest3 || bottomOf(q.el) > bottomOf(nearest3.el) + 0.01) nearest3 = q;
3067
+ }
3068
+ for (const q of above) {
3069
+ if (q.collapsed) continue;
3070
+ const by2 = (off.get(q) ?? 0) + growth(q);
3071
+ if (by2 > 0 && !above.some((c) => c.collapsed && topOf(c.el) >= bottomOf(q.el) - 0.01)) pushed = Math.max(pushed, by2);
3072
+ floor = Math.max(floor, bottomOf(q.el) + by2 - pt);
3073
+ }
3074
+ if (!nearest3) continue;
3075
+ const base2 = nearest3.collapsed ? topOf(nearest3.el) + (off.get(nearest3) ?? 0) - pt : (off.get(nearest3) ?? 0) + growth(nearest3);
3076
+ const by = Math.max(base2, pushed, floor);
3077
+ if (Math.abs(by) > 1e-9) off.set(p, by);
3078
+ }
3079
+ return off;
3080
+ }
3081
+ function stretchRows(slots) {
3082
+ for (const s of slots) {
3083
+ if (s.el.stretch !== "row" || s.hidden) continue;
3084
+ for (const o of slots) {
3085
+ if (!o.hidden && Math.abs(o.el.y - s.el.y) < 0.01 && Math.abs(o.el.h - s.el.h) < 0.01) s.needed = Math.max(s.needed, o.needed);
3057
3086
  }
3058
- if (by > 0) out.set(p, by);
3059
3087
  }
3060
- return out;
3061
3088
  }
3062
3089
  function addAnchor(out, el, ctx, left, top, y, h, hidden) {
3063
3090
  if (!el.id) return;
@@ -3075,29 +3102,47 @@ function addAnchor(out, el, ctx, left, top, y, h, hidden) {
3075
3102
  }
3076
3103
  function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3077
3104
  const bandGrow = band.canGrow ?? (ctx.kind === "detail" || ctx.kind === "groupHeader" || ctx.kind === "groupFooter" || ctx.kind === "summary" || ctx.kind === "title");
3078
- const prepared = [];
3105
+ const slots = [];
3079
3106
  const anchors = ctx.anchors && !ctx.blank ? [] : void 0;
3107
+ let collapsed = false, rows = false;
3080
3108
  for (const el of band.elements) {
3081
3109
  if (el.visible && !ctx.blank && !truthy(evaluate(el.visible, scope))) {
3082
- if (anchors) addAnchor(anchors, el, ctx, left, top, el.y, el.h, true);
3110
+ if (el.collapse) collapsed = true;
3111
+ slots.push({ el, style: resolveStyle(ctx.def, el), needed: el.h, hidden: true, collapsed: !!el.collapse });
3083
3112
  continue;
3084
3113
  }
3085
- prepared.push(prepare(el, scope, ctx, bandGrow));
3114
+ if (el.stretch === "row") rows = true;
3115
+ slots.push(prepare(el, scope, ctx, bandGrow));
3086
3116
  }
3087
- const shift2 = shifts(prepared);
3117
+ if (rows) stretchRows(slots);
3118
+ const shift2 = shifts(slots);
3119
+ const yOf = (s) => shift2 ? s.el.y + (shift2.get(s) ?? 0) : s.el.y;
3088
3120
  let height = Math.max(band.height, minHeight);
3089
- for (const p of prepared) height = Math.max(height, p.el.y + (shift2?.get(p) ?? 0) + p.needed);
3121
+ if (collapsed) {
3122
+ let lowest = 0, lowestNew = 0;
3123
+ for (const s of slots) {
3124
+ lowest = Math.max(lowest, bottomOf(s.el));
3125
+ if (!s.hidden) lowestNew = Math.max(lowestNew, yOf(s) + Math.max(s.needed, 0));
3126
+ }
3127
+ height = Math.max(minHeight, lowestNew + Math.max(0, band.height - lowest));
3128
+ } else for (const s of slots) if (!s.hidden) height = Math.max(height, yOf(s) + s.needed);
3090
3129
  const items = [];
3091
3130
  const bg = ctx.background ?? band.background;
3092
3131
  if (bg) items.push({ kind: "box", x: left, y: top, w: ctx.width, h: height, fill: bg, source: { band: ctx.kind, row: ctx.rowIndex } });
3093
- for (const p of prepared) {
3132
+ for (const p of slots) {
3094
3133
  const el = p.el;
3095
- const elY = shift2 ? el.y + (shift2.get(p) ?? 0) : el.y;
3134
+ const elY = yOf(p);
3135
+ if (p.hidden) {
3136
+ if (anchors) addAnchor(anchors, el, ctx, left, top, elY, el.h, true);
3137
+ continue;
3138
+ }
3096
3139
  const touchesBottom = el.y + el.h >= band.height - 0.01;
3140
+ const follows = el.stretch === "row" ? touchesBottom : el.stretch ?? touchesBottom;
3097
3141
  let h = p.needed;
3098
- if ((el.stretch ?? touchesBottom) && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, height - elY);
3142
+ if (follows && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, height - elY);
3143
+ if (el.type === "line") h = el.h === 0 ? 0 : el.stretch === "row" ? p.needed : touchesBottom ? Math.max(el.h, height - elY) : el.h;
3099
3144
  const x = left + el.x, y = top + elY;
3100
- if (anchors) addAnchor(anchors, el, ctx, left, top, elY, el.type === "line" ? el.h === 0 ? 0 : el.y + el.h >= band.height - 0.01 ? Math.max(el.h, height - elY) : el.h : h);
3145
+ if (anchors) addAnchor(anchors, el, ctx, left, top, elY, h);
3101
3146
  switch (el.type) {
3102
3147
  case "text":
3103
3148
  case "field": {
@@ -3112,8 +3157,7 @@ function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3112
3157
  break;
3113
3158
  }
3114
3159
  case "line": {
3115
- const lh = el.h === 0 ? 0 : el.y + el.h >= band.height - 0.01 ? Math.max(el.h, height - elY) : el.h;
3116
- items.push({ kind: "line", x1: x, y1: y, x2: x + el.w, y2: y + lh, width: el.width ?? 0.2, color: el.color ?? "#000000", dash: el.dash, source: { band: ctx.kind, elementId: el.id } });
3160
+ items.push({ kind: "line", x1: x, y1: y, x2: x + el.w, y2: y + h, width: el.width ?? 0.2, color: el.color ?? "#000000", dash: el.dash, source: { band: ctx.kind, elementId: el.id } });
3117
3161
  break;
3118
3162
  }
3119
3163
  case "rect": {
@@ -3352,7 +3396,10 @@ function paginate(def, data, opts, knownTotal, docHash) {
3352
3396
  const place = (band, kind, scope, extra = {}, pre) => {
3353
3397
  const r = pre ? { height: pre.height, items: shiftItems(pre.items, y), anchors: pre.anchors?.map((a) => ({ ...a, y: a.y + y })) } : renderBand(band, scope, ctxFor(kind, extra), ml, y);
3354
3398
  for (const it of r.items) page.items.push(it);
3355
- if (r.anchors) (page.anchors ?? (page.anchors = [])).push(...r.anchors);
3399
+ if (r.anchors) {
3400
+ for (const a of r.anchors) if (a.y + a.h > limit + 0.01) a.offPage = true;
3401
+ (page.anchors ?? (page.anchors = [])).push(...r.anchors);
3402
+ }
3356
3403
  y += r.height;
3357
3404
  hasBody = true;
3358
3405
  return r.height;
@@ -3474,7 +3521,7 @@ function paginate(def, data, opts, knownTotal, docHash) {
3474
3521
  draw(b.pageFooter, "pageFooter", geo.height - mb - footerH);
3475
3522
  p.items.push(...extraItems);
3476
3523
  });
3477
- decoratePages(pages, { watermark: def.watermark, emboss: def.emboss }, geo, (p, i) => ({
3524
+ decoratePages(pages, { watermark: def.watermark, emboss: def.emboss, locale }, geo, (p, i) => ({
3478
3525
  ...allScope,
3479
3526
  rows: pageRows[i],
3480
3527
  pageRows: pageRows[i],
@@ -3873,10 +3920,10 @@ function dataToXlsx(def, data, opts = {}) {
3873
3920
  rows.forEach((row, i) => {
3874
3921
  const scope = { ...base2, row, vars: { ...base2.vars, rowNum: i + 1, rowIndex: i + 1 } };
3875
3922
  out.push(cols.map((c) => {
3876
- if (c.type === "text") return { v: interpolate(c.text, scope) };
3923
+ if (c.type === "text") return { v: interpolate(c.text, scope, opts.locale) };
3877
3924
  const v = evaluate(c.expr, scope);
3878
3925
  if (typeof v === "number" || v instanceof Date) return { v, s: { fmt: excelFormat(c.format) } };
3879
- return { v: v == null ? "" : formatValue(v, c.format) };
3926
+ return { v: v == null ? "" : formatValue(v, c.format, opts.locale) };
3880
3927
  }));
3881
3928
  });
3882
3929
  const widths = cols.map((c, i) => Math.min(60, Math.max(6, String(header[i].v ?? "").length * 2, c.w / MM_PER_CHAR)));
@@ -3896,7 +3943,7 @@ function dataToCsv(def, data, opts = {}) {
3896
3943
  const scope = { ...base2, row, vars: { ...base2.vars, rowNum: i + 1 } };
3897
3944
  lines.push(fields.map((f2) => {
3898
3945
  const v = evaluate(f2.expr, scope);
3899
- return typeof v === "number" ? q(String(v), true) : q(formatValue(v, f2.format));
3946
+ return typeof v === "number" ? q(String(v), true) : q(formatValue(v, f2.format, opts.locale));
3900
3947
  }).join(","));
3901
3948
  });
3902
3949
  return "\uFEFF" + lines.join("\r\n");
@@ -5669,7 +5716,7 @@ function createReport(definition, data = {}, opts = {}) {
5669
5716
  },
5670
5717
  async toXlsx(mode = "form") {
5671
5718
  if (!await allowed(mode === "data" ? "xlsx-data" : "xlsx")) return null;
5672
- const bytes = mode === "data" ? dataToXlsx(def, rows, { params }) : pagesToXlsx(layout(), def);
5719
+ const bytes = mode === "data" ? dataToXlsx(def, rows, { params, locale: opts.locale }) : pagesToXlsx(layout(), def);
5673
5720
  return new Blob([bytes], { type: MIME.xlsx });
5674
5721
  },
5675
5722
  async toHwpx() {
@@ -5678,7 +5725,7 @@ function createReport(definition, data = {}, opts = {}) {
5678
5725
  },
5679
5726
  async toCsv() {
5680
5727
  if (!await allowed("csv")) return null;
5681
- return new Blob([dataToCsv(def, rows, { params })], { type: MIME.csv });
5728
+ return new Blob([dataToCsv(def, rows, { params, locale: opts.locale })], { type: MIME.csv });
5682
5729
  },
5683
5730
  async download(kind, fileName) {
5684
5731
  const base2 = safeName(fileName ?? def.name ?? "report");
@@ -5966,12 +6013,22 @@ function createViewer(el, options) {
5966
6013
  let runSeq = 0;
5967
6014
  let runs = 0;
5968
6015
  let latest = Promise.resolve();
5969
- const setState = (state) => {
5970
- el.dataset.frState = state;
6016
+ let running = false, deciding = 0;
6017
+ let outcome = null;
6018
+ const showState = () => {
6019
+ if (destroyed) return;
6020
+ el.dataset.frState = running || deciding ? "busy" : outcome ?? "busy";
6021
+ };
6022
+ const finish = (how) => {
6023
+ running = false;
6024
+ outcome = how;
6025
+ el.dataset.frRun = String(++runs);
6026
+ showState();
5971
6027
  };
5972
6028
  const runOnce = async (seq2) => {
5973
6029
  busy(t.loading);
5974
- setState("busy");
6030
+ running = true;
6031
+ showState();
5975
6032
  let during = "data";
5976
6033
  const stale = () => seq2 !== runSeq;
5977
6034
  try {
@@ -5988,8 +6045,7 @@ function createViewer(el, options) {
5988
6045
  result = report.layout();
5989
6046
  reported = -1;
5990
6047
  build();
5991
- el.dataset.frRun = String(++runs);
5992
- setState("ready");
6048
+ finish("ready");
5993
6049
  try {
5994
6050
  options.onReady?.({ pages: result.pages.length, rows: result.rowCount, ms: result.ms, params: { ...params } });
5995
6051
  } catch (e) {
@@ -5997,10 +6053,10 @@ function createViewer(el, options) {
5997
6053
  }
5998
6054
  } catch (e) {
5999
6055
  if (stale()) return await latest;
6056
+ if (destroyed) return;
6000
6057
  const err = fail(e, during);
6001
6058
  pagesEl.innerHTML = `<p class="fr-empty">${t.error}: ${escapeHtml(err.message)}</p>`;
6002
- el.dataset.frRun = String(++runs);
6003
- setState("error");
6059
+ finish("error");
6004
6060
  } finally {
6005
6061
  if (!stale()) busy(null);
6006
6062
  }
@@ -6136,18 +6192,20 @@ function createViewer(el, options) {
6136
6192
  const v = inp.value;
6137
6193
  next[inp.name] = type === "number" ? v === "" ? null : Number(v) : type === "boolean" ? v === "true" : v;
6138
6194
  });
6139
- const before = el.dataset.frState;
6140
- setState("busy");
6195
+ deciding++;
6196
+ showState();
6141
6197
  let answer;
6142
6198
  try {
6143
6199
  answer = await options.onParamsSubmit?.({ ...next });
6144
6200
  } catch (x) {
6145
6201
  fail(x, "data");
6146
- setState(before ?? "ready");
6202
+ deciding--;
6203
+ showState();
6147
6204
  return;
6148
6205
  }
6206
+ deciding--;
6149
6207
  if (answer === false) {
6150
- setState(before ?? "ready");
6208
+ showState();
6151
6209
  return;
6152
6210
  }
6153
6211
  params = answer && typeof answer === "object" ? { ...next, ...answer } : next;
@@ -6428,7 +6486,13 @@ var L = {
6428
6486
  suppress: "\uAC19\uC740 \uAC12 \uBC18\uBCF5 \uC228\uAE30\uAE30",
6429
6487
  visible: "\uD45C\uC2DC \uC870\uAC74 (\uC2DD)",
6430
6488
  canGrow: "\uB0B4\uC6A9\uC5D0 \uB9DE\uCDB0 \uB298\uB9AC\uAE30",
6431
- stretch: "\uBC34\uB4DC \uB192\uC774 \uB530\uB77C\uAC00\uAE30",
6489
+ stretch: "\uB192\uC774 \uB9DE\uCD94\uAE30",
6490
+ stretchAuto: "\uAE30\uBCF8 (\uBC34\uB4DC \uBC14\uB2E5\uC5D0 \uB2FF\uC73C\uBA74 \uBC34\uB4DC \uB192\uC774)",
6491
+ stretchBand: "\uBC34\uB4DC \uB192\uC774",
6492
+ stretchRow: "\uAC19\uC740 \uC904\uC758 \uAC00\uC7A5 \uB192\uC740 \uCE78",
6493
+ stretchNone: "\uB9DE\uCD94\uC9C0 \uC54A\uC74C",
6494
+ shift: "\uC704 \uC694\uC18C\uAC00 \uB298\uBA74 \uD568\uAED8 \uB0B4\uB824\uAC00\uAE30",
6495
+ collapse: "\uC228\uC73C\uBA74 \uC790\uB9AC \uC811\uAE30 (\uC544\uB798 \uC694\uC18C\uAC00 \uC62C\uB77C\uC634)",
6432
6496
  style: "\uC2A4\uD0C0\uC77C",
6433
6497
  bold: "\uAD75\uAC8C",
6434
6498
  italic: "\uAE30\uC6B8\uC784",
@@ -6542,7 +6606,13 @@ var L = {
6542
6606
  suppress: "Hide repeated values",
6543
6607
  visible: "Visible when (expression)",
6544
6608
  canGrow: "Grow to fit content",
6545
- stretch: "Follow band height",
6609
+ stretch: "Height",
6610
+ stretchAuto: "Default (band height when touching its bottom)",
6611
+ stretchBand: "Band height",
6612
+ stretchRow: "Tallest cell on the same row",
6613
+ stretchNone: "Own height",
6614
+ shift: "Move down when an element above grows",
6615
+ collapse: "Give the space back when hidden",
6546
6616
  style: "Style",
6547
6617
  bold: "Bold",
6548
6618
  italic: "Italic",
@@ -6801,7 +6871,16 @@ function elementFields(els, t, def) {
6801
6871
  out.push({ label: t.shrink, kind: "check", get: () => !!same((e) => !!e.style?.shrinkToFit), set: (v) => styleSet("shrinkToFit")(v) });
6802
6872
  out.push({ label: t.canGrow, kind: "check", get: () => same((e) => e.canGrow !== false), set: (v) => setAll((e) => v ? delete e.canGrow : e.canGrow = false) });
6803
6873
  }
6874
+ out.push({
6875
+ label: t.stretch,
6876
+ kind: "select",
6877
+ options: [{ value: "", label: t.stretchAuto }, { value: "band", label: t.stretchBand }, { value: "row", label: t.stretchRow }, { value: "none", label: t.stretchNone }],
6878
+ get: () => same((e) => e.stretch === void 0 ? "" : e.stretch === "row" ? "row" : e.stretch ? "band" : "none"),
6879
+ set: (v) => setAll((e) => v === "row" ? e.stretch = "row" : v === "band" ? e.stretch = true : v === "none" ? e.stretch = false : delete e.stretch)
6880
+ });
6881
+ out.push({ label: t.shift, kind: "check", get: () => same((e) => e.shift !== false), set: (v) => setAll((e) => v ? delete e.shift : e.shift = false) });
6804
6882
  out.push({ label: t.visible, kind: "expr", wide: true, get: () => same((e) => e.visible ?? "") ?? "", set: (v) => setAll((e) => v ? e.visible = String(v) : delete e.visible) });
6883
+ out.push({ label: t.collapse, kind: "check", get: () => same((e) => !!e.collapse), set: (v) => setAll((e) => v ? e.collapse = true : delete e.collapse) });
6805
6884
  return out;
6806
6885
  }
6807
6886
  function bandFields(band, label, t, group, isDetail) {
@@ -7967,7 +8046,7 @@ async function renderPdf(def, data = {}, opts = {}) {
7967
8046
  return writePdf(layout.pages, { title: def.name, creator: "FreeReport", ...opts.pdf, fonts: { default: first, ...fonts } });
7968
8047
  }
7969
8048
  async function renderXlsx(def, data = {}, opts = {}) {
7970
- if (opts.mode === "data") return dataToXlsx(def, data, { params: opts.params });
8049
+ if (opts.mode === "data") return dataToXlsx(def, data, { params: opts.params, locale: opts.locale });
7971
8050
  const { layout } = await prepare2(def, data, opts);
7972
8051
  return pagesToXlsx(layout, def);
7973
8052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freereportjs/core",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "FreeReport — a web reporting engine (free for personal use): banded report designer, paged viewer, printing, PDF with embedded Korean fonts, Excel export.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",