@freereportjs/core 0.10.0 → 0.10.1

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.1";
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.1";
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,48 @@ 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;
3053
3061
  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));
3062
+ if (topOf(q.el) >= pt) break;
3063
+ if (bottomOf(q.el) > pt + 0.01 || leftOf(q.el) >= pr - 0.01 || pl >= rightOf(q.el) - 0.01) continue;
3064
+ if (!nearest3 || bottomOf(q.el) > bottomOf(nearest3.el) + 0.01) nearest3 = q;
3065
+ if (q.collapsed) continue;
3066
+ const by2 = (off.get(q) ?? 0) + growth(q);
3067
+ if (by2 > 0) pushed = Math.max(pushed, by2);
3068
+ floor = Math.max(floor, bottomOf(q.el) + by2 - pt);
3069
+ }
3070
+ if (!nearest3) continue;
3071
+ const base2 = nearest3.collapsed ? topOf(nearest3.el) + (off.get(nearest3) ?? 0) - pt : (off.get(nearest3) ?? 0) + growth(nearest3);
3072
+ const by = Math.max(base2, pushed, floor);
3073
+ if (Math.abs(by) > 1e-9) off.set(p, by);
3074
+ }
3075
+ return off;
3076
+ }
3077
+ function stretchRows(slots) {
3078
+ for (const s of slots) {
3079
+ if (s.el.stretch !== "row" || s.hidden) continue;
3080
+ for (const o of slots) {
3081
+ 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
3082
  }
3058
- if (by > 0) out.set(p, by);
3059
3083
  }
3060
- return out;
3061
3084
  }
3062
3085
  function addAnchor(out, el, ctx, left, top, y, h, hidden) {
3063
3086
  if (!el.id) return;
@@ -3075,29 +3098,47 @@ function addAnchor(out, el, ctx, left, top, y, h, hidden) {
3075
3098
  }
3076
3099
  function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3077
3100
  const bandGrow = band.canGrow ?? (ctx.kind === "detail" || ctx.kind === "groupHeader" || ctx.kind === "groupFooter" || ctx.kind === "summary" || ctx.kind === "title");
3078
- const prepared = [];
3101
+ const slots = [];
3079
3102
  const anchors = ctx.anchors && !ctx.blank ? [] : void 0;
3103
+ let collapsed = false, rows = false;
3080
3104
  for (const el of band.elements) {
3081
3105
  if (el.visible && !ctx.blank && !truthy(evaluate(el.visible, scope))) {
3082
- if (anchors) addAnchor(anchors, el, ctx, left, top, el.y, el.h, true);
3106
+ if (el.collapse) collapsed = true;
3107
+ slots.push({ el, style: resolveStyle(ctx.def, el), needed: el.h, hidden: true, collapsed: !!el.collapse });
3083
3108
  continue;
3084
3109
  }
3085
- prepared.push(prepare(el, scope, ctx, bandGrow));
3110
+ if (el.stretch === "row") rows = true;
3111
+ slots.push(prepare(el, scope, ctx, bandGrow));
3086
3112
  }
3087
- const shift2 = shifts(prepared);
3113
+ if (rows) stretchRows(slots);
3114
+ const shift2 = shifts(slots);
3115
+ const yOf = (s) => shift2 ? s.el.y + (shift2.get(s) ?? 0) : s.el.y;
3088
3116
  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);
3117
+ if (collapsed) {
3118
+ let lowest = 0, lowestNew = 0;
3119
+ for (const s of slots) {
3120
+ lowest = Math.max(lowest, bottomOf(s.el));
3121
+ if (!s.hidden) lowestNew = Math.max(lowestNew, yOf(s) + Math.max(s.needed, 0));
3122
+ }
3123
+ height = Math.max(minHeight, lowestNew + Math.max(0, band.height - lowest));
3124
+ } else for (const s of slots) if (!s.hidden) height = Math.max(height, yOf(s) + s.needed);
3090
3125
  const items = [];
3091
3126
  const bg = ctx.background ?? band.background;
3092
3127
  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) {
3128
+ for (const p of slots) {
3094
3129
  const el = p.el;
3095
- const elY = shift2 ? el.y + (shift2.get(p) ?? 0) : el.y;
3130
+ const elY = yOf(p);
3131
+ if (p.hidden) {
3132
+ if (anchors) addAnchor(anchors, el, ctx, left, top, elY, el.h, true);
3133
+ continue;
3134
+ }
3096
3135
  const touchesBottom = el.y + el.h >= band.height - 0.01;
3136
+ const follows = el.stretch === "row" ? touchesBottom : el.stretch ?? touchesBottom;
3097
3137
  let h = p.needed;
3098
- if ((el.stretch ?? touchesBottom) && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, height - elY);
3138
+ if (follows && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, height - elY);
3139
+ if (el.type === "line") h = el.h === 0 ? 0 : el.stretch === "row" ? p.needed : touchesBottom ? Math.max(el.h, height - elY) : el.h;
3099
3140
  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);
3141
+ if (anchors) addAnchor(anchors, el, ctx, left, top, elY, h);
3101
3142
  switch (el.type) {
3102
3143
  case "text":
3103
3144
  case "field": {
@@ -3112,8 +3153,7 @@ function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3112
3153
  break;
3113
3154
  }
3114
3155
  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 } });
3156
+ 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
3157
  break;
3118
3158
  }
3119
3159
  case "rect": {
@@ -3352,7 +3392,10 @@ function paginate(def, data, opts, knownTotal, docHash) {
3352
3392
  const place = (band, kind, scope, extra = {}, pre) => {
3353
3393
  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
3394
  for (const it of r.items) page.items.push(it);
3355
- if (r.anchors) (page.anchors ?? (page.anchors = [])).push(...r.anchors);
3395
+ if (r.anchors) {
3396
+ for (const a of r.anchors) if (a.y + a.h > limit + 0.01) a.offPage = true;
3397
+ (page.anchors ?? (page.anchors = [])).push(...r.anchors);
3398
+ }
3356
3399
  y += r.height;
3357
3400
  hasBody = true;
3358
3401
  return r.height;
@@ -3474,7 +3517,7 @@ function paginate(def, data, opts, knownTotal, docHash) {
3474
3517
  draw(b.pageFooter, "pageFooter", geo.height - mb - footerH);
3475
3518
  p.items.push(...extraItems);
3476
3519
  });
3477
- decoratePages(pages, { watermark: def.watermark, emboss: def.emboss }, geo, (p, i) => ({
3520
+ decoratePages(pages, { watermark: def.watermark, emboss: def.emboss, locale }, geo, (p, i) => ({
3478
3521
  ...allScope,
3479
3522
  rows: pageRows[i],
3480
3523
  pageRows: pageRows[i],
@@ -3873,10 +3916,10 @@ function dataToXlsx(def, data, opts = {}) {
3873
3916
  rows.forEach((row, i) => {
3874
3917
  const scope = { ...base2, row, vars: { ...base2.vars, rowNum: i + 1, rowIndex: i + 1 } };
3875
3918
  out.push(cols.map((c) => {
3876
- if (c.type === "text") return { v: interpolate(c.text, scope) };
3919
+ if (c.type === "text") return { v: interpolate(c.text, scope, opts.locale) };
3877
3920
  const v = evaluate(c.expr, scope);
3878
3921
  if (typeof v === "number" || v instanceof Date) return { v, s: { fmt: excelFormat(c.format) } };
3879
- return { v: v == null ? "" : formatValue(v, c.format) };
3922
+ return { v: v == null ? "" : formatValue(v, c.format, opts.locale) };
3880
3923
  }));
3881
3924
  });
3882
3925
  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 +3939,7 @@ function dataToCsv(def, data, opts = {}) {
3896
3939
  const scope = { ...base2, row, vars: { ...base2.vars, rowNum: i + 1 } };
3897
3940
  lines.push(fields.map((f2) => {
3898
3941
  const v = evaluate(f2.expr, scope);
3899
- return typeof v === "number" ? q(String(v), true) : q(formatValue(v, f2.format));
3942
+ return typeof v === "number" ? q(String(v), true) : q(formatValue(v, f2.format, opts.locale));
3900
3943
  }).join(","));
3901
3944
  });
3902
3945
  return "\uFEFF" + lines.join("\r\n");
@@ -5669,7 +5712,7 @@ function createReport(definition, data = {}, opts = {}) {
5669
5712
  },
5670
5713
  async toXlsx(mode = "form") {
5671
5714
  if (!await allowed(mode === "data" ? "xlsx-data" : "xlsx")) return null;
5672
- const bytes = mode === "data" ? dataToXlsx(def, rows, { params }) : pagesToXlsx(layout(), def);
5715
+ const bytes = mode === "data" ? dataToXlsx(def, rows, { params, locale: opts.locale }) : pagesToXlsx(layout(), def);
5673
5716
  return new Blob([bytes], { type: MIME.xlsx });
5674
5717
  },
5675
5718
  async toHwpx() {
@@ -5678,7 +5721,7 @@ function createReport(definition, data = {}, opts = {}) {
5678
5721
  },
5679
5722
  async toCsv() {
5680
5723
  if (!await allowed("csv")) return null;
5681
- return new Blob([dataToCsv(def, rows, { params })], { type: MIME.csv });
5724
+ return new Blob([dataToCsv(def, rows, { params, locale: opts.locale })], { type: MIME.csv });
5682
5725
  },
5683
5726
  async download(kind, fileName) {
5684
5727
  const base2 = safeName(fileName ?? def.name ?? "report");
@@ -5966,12 +6009,22 @@ function createViewer(el, options) {
5966
6009
  let runSeq = 0;
5967
6010
  let runs = 0;
5968
6011
  let latest = Promise.resolve();
5969
- const setState = (state) => {
5970
- el.dataset.frState = state;
6012
+ let running = false, deciding = 0;
6013
+ let outcome = null;
6014
+ const showState = () => {
6015
+ if (destroyed) return;
6016
+ el.dataset.frState = running || deciding ? "busy" : outcome ?? "busy";
6017
+ };
6018
+ const finish = (how) => {
6019
+ running = false;
6020
+ outcome = how;
6021
+ el.dataset.frRun = String(++runs);
6022
+ showState();
5971
6023
  };
5972
6024
  const runOnce = async (seq2) => {
5973
6025
  busy(t.loading);
5974
- setState("busy");
6026
+ running = true;
6027
+ showState();
5975
6028
  let during = "data";
5976
6029
  const stale = () => seq2 !== runSeq;
5977
6030
  try {
@@ -5988,8 +6041,7 @@ function createViewer(el, options) {
5988
6041
  result = report.layout();
5989
6042
  reported = -1;
5990
6043
  build();
5991
- el.dataset.frRun = String(++runs);
5992
- setState("ready");
6044
+ finish("ready");
5993
6045
  try {
5994
6046
  options.onReady?.({ pages: result.pages.length, rows: result.rowCount, ms: result.ms, params: { ...params } });
5995
6047
  } catch (e) {
@@ -5997,10 +6049,10 @@ function createViewer(el, options) {
5997
6049
  }
5998
6050
  } catch (e) {
5999
6051
  if (stale()) return await latest;
6052
+ if (destroyed) return;
6000
6053
  const err = fail(e, during);
6001
6054
  pagesEl.innerHTML = `<p class="fr-empty">${t.error}: ${escapeHtml(err.message)}</p>`;
6002
- el.dataset.frRun = String(++runs);
6003
- setState("error");
6055
+ finish("error");
6004
6056
  } finally {
6005
6057
  if (!stale()) busy(null);
6006
6058
  }
@@ -6136,18 +6188,20 @@ function createViewer(el, options) {
6136
6188
  const v = inp.value;
6137
6189
  next[inp.name] = type === "number" ? v === "" ? null : Number(v) : type === "boolean" ? v === "true" : v;
6138
6190
  });
6139
- const before = el.dataset.frState;
6140
- setState("busy");
6191
+ deciding++;
6192
+ showState();
6141
6193
  let answer;
6142
6194
  try {
6143
6195
  answer = await options.onParamsSubmit?.({ ...next });
6144
6196
  } catch (x) {
6145
6197
  fail(x, "data");
6146
- setState(before ?? "ready");
6198
+ deciding--;
6199
+ showState();
6147
6200
  return;
6148
6201
  }
6202
+ deciding--;
6149
6203
  if (answer === false) {
6150
- setState(before ?? "ready");
6204
+ showState();
6151
6205
  return;
6152
6206
  }
6153
6207
  params = answer && typeof answer === "object" ? { ...next, ...answer } : next;
@@ -6428,7 +6482,13 @@ var L = {
6428
6482
  suppress: "\uAC19\uC740 \uAC12 \uBC18\uBCF5 \uC228\uAE30\uAE30",
6429
6483
  visible: "\uD45C\uC2DC \uC870\uAC74 (\uC2DD)",
6430
6484
  canGrow: "\uB0B4\uC6A9\uC5D0 \uB9DE\uCDB0 \uB298\uB9AC\uAE30",
6431
- stretch: "\uBC34\uB4DC \uB192\uC774 \uB530\uB77C\uAC00\uAE30",
6485
+ stretch: "\uB192\uC774 \uB9DE\uCD94\uAE30",
6486
+ stretchAuto: "\uAE30\uBCF8 (\uBC34\uB4DC \uBC14\uB2E5\uC5D0 \uB2FF\uC73C\uBA74 \uBC34\uB4DC \uB192\uC774)",
6487
+ stretchBand: "\uBC34\uB4DC \uB192\uC774",
6488
+ stretchRow: "\uAC19\uC740 \uC904\uC758 \uAC00\uC7A5 \uB192\uC740 \uCE78",
6489
+ stretchNone: "\uB9DE\uCD94\uC9C0 \uC54A\uC74C",
6490
+ shift: "\uC704 \uC694\uC18C\uAC00 \uB298\uBA74 \uD568\uAED8 \uB0B4\uB824\uAC00\uAE30",
6491
+ collapse: "\uC228\uC73C\uBA74 \uC790\uB9AC \uC811\uAE30 (\uC544\uB798 \uC694\uC18C\uAC00 \uC62C\uB77C\uC634)",
6432
6492
  style: "\uC2A4\uD0C0\uC77C",
6433
6493
  bold: "\uAD75\uAC8C",
6434
6494
  italic: "\uAE30\uC6B8\uC784",
@@ -6542,7 +6602,13 @@ var L = {
6542
6602
  suppress: "Hide repeated values",
6543
6603
  visible: "Visible when (expression)",
6544
6604
  canGrow: "Grow to fit content",
6545
- stretch: "Follow band height",
6605
+ stretch: "Height",
6606
+ stretchAuto: "Default (band height when touching its bottom)",
6607
+ stretchBand: "Band height",
6608
+ stretchRow: "Tallest cell on the same row",
6609
+ stretchNone: "Own height",
6610
+ shift: "Move down when an element above grows",
6611
+ collapse: "Give the space back when hidden",
6546
6612
  style: "Style",
6547
6613
  bold: "Bold",
6548
6614
  italic: "Italic",
@@ -6801,7 +6867,16 @@ function elementFields(els, t, def) {
6801
6867
  out.push({ label: t.shrink, kind: "check", get: () => !!same((e) => !!e.style?.shrinkToFit), set: (v) => styleSet("shrinkToFit")(v) });
6802
6868
  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
6869
  }
6870
+ out.push({
6871
+ label: t.stretch,
6872
+ kind: "select",
6873
+ options: [{ value: "", label: t.stretchAuto }, { value: "band", label: t.stretchBand }, { value: "row", label: t.stretchRow }, { value: "none", label: t.stretchNone }],
6874
+ get: () => same((e) => e.stretch === void 0 ? "" : e.stretch === "row" ? "row" : e.stretch ? "band" : "none"),
6875
+ set: (v) => setAll((e) => v === "row" ? e.stretch = "row" : v === "band" ? e.stretch = true : v === "none" ? e.stretch = false : delete e.stretch)
6876
+ });
6877
+ 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
6878
  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) });
6879
+ out.push({ label: t.collapse, kind: "check", get: () => same((e) => !!e.collapse), set: (v) => setAll((e) => v ? e.collapse = true : delete e.collapse) });
6805
6880
  return out;
6806
6881
  }
6807
6882
  function bandFields(band, label, t, group, isDetail) {
@@ -7967,7 +8042,7 @@ async function renderPdf(def, data = {}, opts = {}) {
7967
8042
  return writePdf(layout.pages, { title: def.name, creator: "FreeReport", ...opts.pdf, fonts: { default: first, ...fonts } });
7968
8043
  }
7969
8044
  async function renderXlsx(def, data = {}, opts = {}) {
7970
- if (opts.mode === "data") return dataToXlsx(def, data, { params: opts.params });
8045
+ if (opts.mode === "data") return dataToXlsx(def, data, { params: opts.params, locale: opts.locale });
7971
8046
  const { layout } = await prepare2(def, data, opts);
7972
8047
  return pagesToXlsx(layout, def);
7973
8048
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freereportjs/core",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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",